-
Notifications
You must be signed in to change notification settings - Fork 286
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
aya,aya-obj,integration-test: add better support in ProgramInfo
& MapInfo
for old kernels
#1007
Conversation
✅ Deploy Preview for aya-rs-docs ready!Built without sensitive environment variables
To edit notification comments on pull requests, go to your Netlify site configuration. |
Hey @alessandrod, this pull request changes the Aya Public API and requires your review. |
f6ff1c8
to
e76da67
Compare
eaa9670
to
be61084
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for doing this work! Overall looks great, left some comments
8afa126
to
7da2f2a
Compare
7da2f2a
to
7614878
Compare
@tyrone-wu, this pull request is now in conflict and requires a rebase. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Super solid work as always, just a couple of comments then we're ready to go :)
Improves the existing integraiton tests for `loaded_programs()` and `loaded_maps()` in consideration for older kernels: - Opt for `SocketFilter` program in tests since XDP requires v4.8 and fragments requires v5.18. - For assertion tests, first perform the assertion, if the assertion fails, then it checks the host kernel version to see if it is above the minimum version requirement. If not, then continue with test, otherwise fail. For assertions that are skipped, they're logged in stderr which can be observed with `-- --nocapture`. This also fixes the `bpf_prog_get_info_by_fd()` call for kernels below v4.15. If calling syscall on kernels below v4.15, it can produce an `E2BIG` error because `check_uarg_tail_zero()` expects the entire struct to all-zero bytes (which is caused from the map info). Instead, we first attempt the syscall with the map info filled, if it returns `E2BIG`, then perform syscall again with empty closure. Also adds doc for which version a kernel feature was introduced for better awareness. The tests have been verified kernel versions: - 4.13.0 - 4.15.0 - 6.1.0
7614878
to
ea690e9
Compare
ea690e9
to
c716e9b
Compare
Add conversion from u32 to program type, link type, and attach type. Additionally, remove duplicate match statement for u32 conversion to `BPF_MAP_TYPE_BLOOM_FILTER` & `BPF_MAP_TYPE_CGRP_STORAGE`. New error `InvalidTypeBinding<T>` is created to represent when a parsed/received value binding to a type is invalid. This is used in the new conversions added here, and also replaces `InvalidMapTypeError` in `TryFrom` for `bpf_map_type`.
Purpose of this commit is to add detections for whether a field is available in `ProgramInfo`. - For `program_type()`, we return the new enum `ProgramType` instead of the integer representation. - For fields that we know cannot be zero, we return `Option<NonZero*>` type. - For `name_as_str()`, it now also uses the feature probe `bpf_name()` to detect if field is available or not. - Two additional feature probes are added for the fields: - `prog_info_map_ids()` probe -> `map_ids()` field - `prog_info_gpl_compatible()` probe -> `gpl_compatible()` field With the `prog_info_map_ids()` probe, the previous implementation that I had for `bpf_prog_get_info_by_fd()` is shortened to use the probe instead of having to make 2 potential syscalls. The `test_loaded_at()` test is also moved into info tests since it is better related to the info tests. `aya::programs::Programs::prog_type(&self)` now returns `ProgramType` instead of the generated FFI from aya-obj. Also previously, `loaded_programs()` could be accessed either through `aya` or `aya::programs`. To avoid confusion and duplicate export of the item, the function should now only be exposed through `aya::programs`.
Adds detection for whether a field is available in `MapInfo`: - For `map_type()`, we treturn new enum `MapType` instead of the integer representation. - For fields that can't be zero, we return `Option<NonZero*>` type. - For `name_as_str()`, it now uses the feature probe `bpf_name()` to detect if field is available. Although the feature probe checks for program name, it can also be used for map name since they were both introduced in the same commit.
c716e9b
to
fbb0930
Compare
let map = program_info | ||
.map_ids() | ||
.map_err(Error::ProgramError)? | ||
.expect("`map_ids` field in `bpf_prog_info` not available") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is it ok to panic here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I definitely shouldn't be panic in the library. I'll have that resolved soon.
.find(|map_info| match map_info.name_as_str() { | ||
Some(name) => name == MAP_NAME, | ||
None => false, | ||
}) | ||
.ok_or(Error::MapNotFound)?; | ||
let map = MapData::from_id(map.id()).map_err(Error::MapError)?; | ||
let map = MapData::from_id(map.id().unwrap().get()).map_err(Error::MapError)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is it ok to unwrap here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I should have mapped that into an error. I'll have that resolved soon.
@@ -137,19 +136,21 @@ impl EbpfLogger { | |||
) -> Result<EbpfLogger, Error> { | |||
let program_info = loaded_programs() | |||
.filter_map(|info| info.ok()) | |||
.find(|info| info.id() == program_id) | |||
.find(|info| info.id().is_some_and(|id| id.get() == program_id)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a more complicated (and less backwards-compatible) version of info.id() == Some(program_id)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahhhhh you're right. 😓
} else { | ||
// Continue with tests since host is not expected to have feat | ||
eprintln!( | ||
r#"ignoring assertion at {}:{} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we shouldn't ignore the assertion - we should instead assert that the feature is indeed missing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I did some experimenting with bpf_obj_get_info_by_fd()
after examining the code some more. I tested that I can use bpf_check_uarg_tail_zero()
to check if a field gives E2BIG
if I pass the struct with the field filled with some arbitrary value, however, this would require exposing bpf_obj_get_info_by_fd()
for the test
crate to access.
Ok(Some( | ||
map_ids | ||
.into_iter() | ||
.map(|id| NonZeroU32::new(id).unwrap()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be .expect with a string that mentions the version detection above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that I think about it, I think I might of turned the API into an unwrapping nightmare with all the Option<NonZero*>
. 😵
I'm currently thinking of having fields that were introduced in the same kernel version as the struct not be options (just the direct value). What do you think?
For map_ids()
, it looks like it's reasonable to assume the map id would never be zero based on what I traced:
- https://elixir.bootlin.com/linux/v4.15/source/kernel/bpf/syscall.c#L1590: how map IDs are getting filled in
bpf_prog_get_info_by_fd()
- https://elixir.bootlin.com/linux/v4.15/source/kernel/bpf/verifier.c#L4735: how map info is filled from
bpf_verifier_env
intobpf_prog_aux
- https://elixir.bootlin.com/linux/v4.15/source/kernel/bpf/verifier.c#L4249: how map is set in
bpf_verifier_env
before checking if map is valid
Some of the code is shifted in kernel v6.10, but the logic should still be the same. https://elixir.bootlin.com/linux/v6.10/source/kernel/bpf/verifier.c#L18565
cc'ing @alessandrod for thoughts as well
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm currently thinking of having fields that were introduced in the same kernel version as the struct not be options (just the direct value). What do you think?
Yep this sounds good.
For map_ids(), it looks like it's reasonable to assume the map id would never be zero based on what I traced:
I don't think that any bpf ids can be 0 regardless of type (map, program, etc)?
In fact I'd probably change all these to return Option. I think NonZero* is nice to accept as input to enforce safety, but as a return value in these utils seems more annoying than anything else.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think that any bpf ids can be 0 regardless of type (map, program, etc)?
It should not be possible yes
In fact I'd probably change all these to return Option. I think NonZero* is nice to accept as input to enforce safety, but as a return value in these utils seems more annoying than anything else.
Yeah I do agree with you, I would prefer Option
over NonZero*
on returns too.
@@ -694,6 +703,62 @@ pub(crate) fn is_prog_name_supported() -> bool { | |||
bpf_prog_load(&mut attr).is_ok() | |||
} | |||
|
|||
/// Tests whether `nr_map_ids` & `map_ids` fields in `bpf_prog_info` is available. | |||
pub(crate) fn is_info_map_ids_supported() -> bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
whole lot of repetition in these functions :/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall, I agree with @alessandrod, we shouldn't use Option<NonZero*>
as return values but Option<IntegerType>
instead (0 mapping to None)
Some code pattern could also be simplified I feel here and there but the logic related to those are usually small so that's fine in any cases.
Nice to see all the new comments around kernel version here, I didn't dig too much if the version were appropriate everywhere but if you want I can look more closely at it later 👍
fn try_from(link_type: u32) -> Result<Self, Self::Error> { | ||
use bpf_link_type::*; | ||
Ok(match link_type { | ||
x if x == BPF_LINK_TYPE_UNSPEC as u32 => BPF_LINK_TYPE_UNSPEC, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kind of a bit nit here but I would instead transform link_type to bpf_link_type::Type
that would remove all the as u32 boilerplate around 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you elaborate on ::Type
? I'm not familiar with the usage of that syntax. 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those BPF_LINK_TYPE_
values have a type of bpf_link_type::Type
(that you can find defined here for x86_64)
So the idea would be to cast link_type to bpf_link_type::Type
instead of casting each BPF_LINK_TYPE_
values to u32.
|
||
fn try_from(attach_type: u32) -> Result<Self, Self::Error> { | ||
use bpf_attach_type::*; | ||
Ok(match attach_type { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment as earlier but this time with bpf_attach_type::Type
type Error = InvalidTypeBinding<u32>; | ||
|
||
fn try_from(prog_type: u32) -> Result<Self, Self::Error> { | ||
Ok(match prog_type { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment as earlier but this time with bpf_prog_type::Type
pub fn name_as_str(&self) -> Option<&str> { | ||
let name = std::str::from_utf8(self.name()).ok(); | ||
if let Some(name_str) = name { | ||
if FEATURES.bpf_name() || !name_str.is_empty() { | ||
return name; | ||
} | ||
} | ||
None | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pub fn name_as_str(&self) -> Option<&str> { | |
let name = std::str::from_utf8(self.name()).ok(); | |
if let Some(name_str) = name { | |
if FEATURES.bpf_name() || !name_str.is_empty() { | |
return name; | |
} | |
} | |
None | |
} | |
pub fn name_as_str(&self) -> Option<&str> { | |
let name = std::str::from_utf8(self.name()).ok()?; | |
if FEATURES.bpf_name() || !name.is_empty() { | |
return Some(name); | |
} | |
None | |
} |
impl Display for KernelVersion { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
write!(f, "{}.{}.{}", self.major, self.minor, self.patch) | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice thing to see added 👍
Why is that better? If we know they can't be zero it's better to communicate that in the type. |
Because API ergonomics matter.
|
Then why expose the scalar at all? If the value doesn't matter, make it opaque. |
We should definitely have types for all these IDs, but babysteps. Even without stronger ids this PR was an improvement over what was there before, and removing NonZeroU32 is an improvement over what's there, imo. |
Addresses the feedback from aya-rs#1007: - remove panic from `unwrap` and `expect` - Option<NonZero*> => Option<int> with `0` mapping to `None` - use `aya_ebpf::binding` in `u32` conversion in prog, link, and attach type for more straightforward matching Refs: aya-rs#1007
Addresses the feedback from aya-rs#1007: - remove panic from `unwrap` and `expect` - Option<NonZero*> => Option<int> with `0` mapping to `None` - use `aya_ebpf::binding` in `u32` conversion in prog, link, and attach type for more straightforward matching Refs: aya-rs#1007
Addresses the feedback from aya-rs#1007: - remove panic from `unwrap` and `expect` - Option<NonZero*> => Option<int> with `0` mapping to `None` - use `aya_ebpf::binding` in `u32` conversion in prog, link, and attach type for more straightforward matching Refs: aya-rs#1007
Addresses the feedback from aya-rs#1007: - remove panic from `unwrap` and `expect` - Option<NonZero*> => Option<int> with `0` mapping to `None` Refs: aya-rs#1007
This adds support for the field availability in
ProgramInfo
andMapInfo
.Srry this is long, let me know and I can try to TL;DR it.
For
ProgramInfo
:program_type()
to returnbpf_prog_type
instead of the integer representation.0
, returnOption<NonZero*>
type to indicateNone
as field being unavailable.name_as_str()
use thebpf_name()
feature probe as an additional check for whether field is available. Besides invalid unicode,None
is returned if probe does not detect feature.mad_ids()
:Option<Vec<NonZeroU32>>
, withNone
indicating field is unavailable.E2BIG
error sincecheck_uarg_tail_zero()
expects the entire struct to be zero bytes. I initially worked around this by havingbpf_prog_get_info_by_fd()
do an additional syscall if the first failed withE2BIG
.map_ids()
andbpf_prog_get_info_by_fd()
both uses a new feature probeprog_info_map_ids()
to detect if field available.gpl_compatible()
, it now returnsOption<bool>
, withNone
indicating field is unavailable. It uses a new feature probeprog_info_gpl_compatible()
to detect for field availability.There was a redundant export of
loaded_programs()
which gave it 2 method of access:aya::loaded_programs
andaya::programs::loaded_programs
. To avoid confusion, it should now only beaya::programs::loaded_programs
.For
MapInfo
:map_type()
to returnbpf_map_type
instead of the integer representation.0
, returnOption<NonZero*>
type to indicateNone
as field being unavailable.name_as_str()
, it uses thebpf_name()
feature probe to detect for field availability. Although the probe performs the check on program name, map name was introduced in the same commit as program name so it should suffice.Integration tests for
ProgramInfo
andMapInfo
are updated to usekernel_assert
/kernel_assert_eq
macro.The macro first performs the assertion check. If assertion passes, continue with test. If failed, then check host's kernel version.
If host is below the feature version, then continue with test and log it in stderr (can be observed with
-- --nocapture
). Otherwise, panic 😰.I verified that this passes on versions (i use ubuntu mainline images for my integration tests):