feat(aya): add support for map-of-maps (HashOfMaps, ArrayOfMaps)#1446
feat(aya): add support for map-of-maps (HashOfMaps, ArrayOfMaps)#1446Brskt wants to merge 17 commits intoaya-rs:mainfrom
Conversation
✅ Deploy Preview for aya-rs-docs ready!Built without sensitive environment variables
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
@codex review |
There was a problem hiding this comment.
Pull request overview
This PR adds comprehensive support for BPF map-of-maps (BPF_MAP_TYPE_HASH_OF_MAPS and BPF_MAP_TYPE_ARRAY_OF_MAPS) to the Aya framework, building upon the foundation from PR #70.
Changes:
- Added
innerattribute to#[map]macro for declaring map-of-maps templates in eBPF code - Implemented
HashMapOfMapsandArrayOfMapstypes withget(),iter(), and other helper methods - Added support for program array population via
EbpfLoader::set_prog_array_entry()andEbpf::populate_prog_arrays()
Reviewed changes
Copilot reviewed 43 out of 43 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| xtask/public-api/aya.txt | Updated public API surface with new map-of-maps types and test run functionality |
| xtask/public-api/aya-obj.txt | Added inner map bindings and map creation helpers to object parser API |
| xtask/public-api/aya-ebpf.txt | Introduced InnerMap trait and map-of-maps types for eBPF side |
| test/integration-test/src/tests/prog_array.rs | Added integration tests for program array population |
| test/integration-test/src/tests/map_of_maps.rs | Added integration tests for map-of-maps functionality |
| test/integration-ebpf/src/prog_array.rs | eBPF test program for tail calls using program arrays |
| test/integration-ebpf/src/map_of_maps.rs | eBPF test program demonstrating map-of-maps usage |
| ebpf/aya-ebpf/src/maps/*.rs | Implemented InnerMap trait across all compatible map types |
| aya/src/sys/bpf.rs | Added inner_map_fd parameter to map creation and test run functionality |
| aya/src/maps/of_maps/*.rs | Implemented HashMapOfMaps and ArrayOfMaps with iterators |
| aya/src/maps/mod.rs | Added map-of-maps variants to Map enum and error handling |
| aya/src/bpf.rs | Enhanced loader to handle map-of-maps creation and program array population |
| aya-obj/src/obj.rs | Added parsing for .maps.inner section and inner map bindings |
| aya-obj/src/maps.rs | Extended map definitions with inner map support and helper constructors |
| aya-ebpf-macros/src/map.rs | Implemented inner attribute processing in map macro |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f2593b39d1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let value: Option<u32> = | ||
| bpf_map_lookup_elem(fd, key, flags).map_err(|io_error| SyscallError { | ||
| call: "bpf_map_lookup_elem", | ||
| io_error, | ||
| })?; | ||
| if let Some(id) = value { | ||
| let inner_fd = bpf_map_get_fd_by_id(id)?; | ||
| let info = MapInfo::new_from_fd(inner_fd.as_fd())?; | ||
| let map_data = MapData::from_id(info.id())?; |
There was a problem hiding this comment.
Treat map-of-maps lookup result as an FD, not an ID
Here get() treats the value returned by bpf_map_lookup_elem as a map ID and calls bpf_map_get_fd_by_id, but this API inserts raw map FDs (insert passes value.as_fd().as_raw_fd()), so the lookup is expected to return an FD in common map-in-map setups. In that case this path will fail (EINVAL/ENOENT) or open a different map whose ID happens to match the FD integer. Consider constructing MapData directly from the returned FD (or otherwise aligning with the stored value type) instead of resolving it as an ID.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
This implementation is correct. For map-of-maps types, the kernel uses an asymmetric API:
- Update (
bpf_map_update_elem): expects the FD of the inner map - Lookup (
bpf_map_lookup_elem): returns the ID of the inner map
This is documented behavior in the Linux kernel: https://docs.kernel.org/bpf/map_of_maps.html
The lookup value must be converted to an FD using bpf_map_get_fd_by_id, which is exactly what this code does
There was a problem hiding this comment.
Perhaps this deserves some inline comments?
| let value: Option<u32> = | ||
| bpf_map_lookup_elem(fd, index, flags).map_err(|io_error| SyscallError { | ||
| call: "bpf_map_lookup_elem", | ||
| io_error, | ||
| })?; | ||
| if let Some(id) = value { | ||
| let inner_fd = bpf_map_get_fd_by_id(id)?; | ||
| let info = MapInfo::new_from_fd(inner_fd.as_fd())?; | ||
| let map_data = MapData::from_id(info.id())?; |
There was a problem hiding this comment.
Treat map-of-maps lookup result as an FD, not an ID
This get() path interprets the lookup value as a map ID and calls bpf_map_get_fd_by_id, but set() stores raw map FDs in the outer array. If the kernel returns the stored FD (as it commonly does for map-in-map values), bpf_map_get_fd_by_id will fail or resolve the wrong map. Using MapData::from_fd on the returned value would keep the value interpretation consistent with set().
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Same as above - this is the correct behavior. The kernel's map-of-maps syscall API is asymmetric by design:
BPF_MAP_UPDATE_ELEMtakes an FDBPF_MAP_LOOKUP_ELEMreturns an ID
See: https://docs.ebpf.io/linux/map-type/BPF_MAP_TYPE_ARRAY_OF_MAPS/
Using bpf_map_get_fd_by_id(id) to convert the returned ID to an FD is the intended pattern.
There was a problem hiding this comment.
Perhaps this deserves some inline comments?
tamird
left a comment
There was a problem hiding this comment.
@tamird made 3 comments.
Reviewable status: 0 of 43 files reviewed, 3 unresolved discussions (waiting on @Brskt).
-- commits line 70 at r35:
The commits in this PR are mostly a mess, but e.g. this one looks useful on its own. Did you intend for the commits history to be preserved? If yes, we will need you to rewrite it into something coherent. If not, then this PR is 3k lines that have to be reviewed in one shot, which is quite difficult.
Code quote:
New commits in r8 on 1/17/2026 at 4:21 PM:
- d1f0cb8: feat(aya): Add prog_array population support for tail calls
Add EbpfLoader::set_prog_array_entry() to declaratively specify which
programs should be placed in program arrays at which indices.
Add Ebpf::populate_prog_arrays() to populate the declared entries with
loaded program file descriptors after programs are loaded.
This enables easier setup of tail call jump tables without manually
managing program array entries.
| let value: Option<u32> = | ||
| bpf_map_lookup_elem(fd, index, flags).map_err(|io_error| SyscallError { | ||
| call: "bpf_map_lookup_elem", | ||
| io_error, | ||
| })?; | ||
| if let Some(id) = value { | ||
| let inner_fd = bpf_map_get_fd_by_id(id)?; | ||
| let info = MapInfo::new_from_fd(inner_fd.as_fd())?; | ||
| let map_data = MapData::from_id(info.id())?; |
There was a problem hiding this comment.
Perhaps this deserves some inline comments?
| let value: Option<u32> = | ||
| bpf_map_lookup_elem(fd, key, flags).map_err(|io_error| SyscallError { | ||
| call: "bpf_map_lookup_elem", | ||
| io_error, | ||
| })?; | ||
| if let Some(id) = value { | ||
| let inner_fd = bpf_map_get_fd_by_id(id)?; | ||
| let info = MapInfo::new_from_fd(inner_fd.as_fd())?; | ||
| let map_data = MapData::from_id(info.id())?; |
There was a problem hiding this comment.
Perhaps this deserves some inline comments?
f2593b3 to
333e272
Compare
Brskt
left a comment
There was a problem hiding this comment.
@Brskt made 3 comments and resolved 2 discussions.
Reviewable status: 0 of 43 files reviewed, 1 unresolved discussion (waiting on @tamird).
Previously, tamird (Tamir Duberstein) wrote…
The commits in this PR are mostly a mess, but e.g. this one looks useful on its own. Did you intend for the commits history to be preserved? If yes, we will need you to rewrite it into something coherent. If not, then this PR is 3k lines that have to be reviewed in one shot, which is quite difficult.
Yes, I've kept the commit history and rewritten it as requested:
- aya-ebpf: eBPF-side map-of-maps implementation
- aya-ebpf-macros: inner attribute for #[map] macro
- aya-obj: Map constructors and .maps.inner parsing
- aya: userspace map-of-maps support
- tests: integration and unit tests
- public API updates
Should be easier to review now.
| let value: Option<u32> = | ||
| bpf_map_lookup_elem(fd, index, flags).map_err(|io_error| SyscallError { | ||
| call: "bpf_map_lookup_elem", | ||
| io_error, | ||
| })?; | ||
| if let Some(id) = value { | ||
| let inner_fd = bpf_map_get_fd_by_id(id)?; | ||
| let info = MapInfo::new_from_fd(inner_fd.as_fd())?; | ||
| let map_data = MapData::from_id(info.id())?; |
| let value: Option<u32> = | ||
| bpf_map_lookup_elem(fd, key, flags).map_err(|io_error| SyscallError { | ||
| call: "bpf_map_lookup_elem", | ||
| io_error, | ||
| })?; | ||
| if let Some(id) = value { | ||
| let inner_fd = bpf_map_get_fd_by_id(id)?; | ||
| let info = MapInfo::new_from_fd(inner_fd.as_fd())?; | ||
| let map_data = MapData::from_id(info.id())?; |
tamird
left a comment
There was a problem hiding this comment.
@tamird partially reviewed 15 files and made 2 comments.
Reviewable status: 1 of 43 files reviewed, 2 unresolved discussions (waiting on @Brskt).
Previously, Brskt wrote…
Yes, I've kept the commit history and rewritten it as requested:
- aya-ebpf: eBPF-side map-of-maps implementation
- aya-ebpf-macros: inner attribute for #[map] macro
- aya-obj: Map constructors and .maps.inner parsing
- aya: userspace map-of-maps support
- tests: integration and unit tests
- public API updates
Should be easier to review now.
it's still just one big blob, right? the commits are now cut along which crates they touch, which is maybe easier for review but they need to be squashed on merge. do I understand correctly?
ebpf/aya-ebpf/src/maps/mod.rs line 47 at r36 (raw file):
/// /// Only implement this trait for map types that can be safely used as inner maps. pub unsafe trait InnerMap {}
🤔 does this need to be pub?
Brskt
left a comment
There was a problem hiding this comment.
@Brskt made 2 comments.
Reviewable status: 1 of 43 files reviewed, 2 unresolved discussions (waiting on @tamird).
Previously, tamird (Tamir Duberstein) wrote…
it's still just one big blob, right? the commits are now cut along which crates they touch, which is maybe easier for review but they need to be squashed on merge. do I understand correctly?
Yes, that's correct. Split for easier review, feel free to squash on merge.
ebpf/aya-ebpf/src/maps/mod.rs line 47 at r36 (raw file):
Previously, tamird (Tamir Duberstein) wrote…
🤔 does this need to be pub?
I tested with pub(crate) and here's what happens:
Build results:
cargo build:⚠️ 3 warnings (private_bounds)cargo clippy -D warnings: ❌ 3 errors - fails CIcargo test: ✅ passintegration tests: ✅ 127 passedpublic-api check: ❌ fails (InnerMap removed from API)
Why it fails:
InnerMap is used as a trait bound on public types:
pub struct ArrayOfMaps<T: InnerMap> { ... }A private trait in a public bound triggers private_bounds, which becomes an error with -D warnings.
Conclusion:
pub is required to pass CI. The unsafe marker already discourages external implementations, and the kernel validates map types at load time anyway.
tamird
left a comment
There was a problem hiding this comment.
@tamird partially reviewed 42 files, made 8 comments, and resolved 1 discussion.
Reviewable status: 19 of 43 files reviewed, 8 unresolved discussions (waiting on @Brskt).
ebpf/aya-ebpf/src/maps/mod.rs line 47 at r36 (raw file):
Previously, Brskt wrote…
I tested with
pub(crate)and here's what happens:Build results:
cargo build:⚠️ 3 warnings (private_bounds)cargo clippy -D warnings: ❌ 3 errors - fails CIcargo test: ✅ passintegration tests: ✅ 127 passedpublic-api check: ❌ fails (InnerMap removed from API)Why it fails:
InnerMapis used as a trait bound on public types:pub struct ArrayOfMaps<T: InnerMap> { ... }A private trait in a public bound triggers private_bounds, which becomes an error with -D warnings.
Conclusion:
pub is required to pass CI. The unsafe marker already discourages external implementations, and the kernel validates map types at load time anyway.
I see. This should be a sealed trait then since we don't want external implementations.
-- commits line 25 at r38:
this commit is ...bad. it's adding a bunch of code that is unused, making review impossible.
ebpf/aya-ebpf/src/maps/hash_of_maps.rs line 14 at r36 (raw file):
pub struct HashOfMaps<K, V> { def: UnsafeCell<bpf_map_def>, _k: PhantomData<K>,
See #1447; use a single phantom plz
aya-ebpf-macros/src/map.rs line 20 at r37 (raw file):
let mut args = syn::parse2(attrs)?; let name = name_arg(&mut args).unwrap_or_else(|| item.ident.to_string()); let inner = pop_string_arg(&mut args, "inner");
while you're here, please add err_on_unknown_args(args)?; (see #1448)
aya-ebpf-macros/src/map.rs line 40 at r37 (raw file):
#[used] static #binding_ident: [u8; #binding_len] = [#(#binding_bytes),*]; }
are we following libbpf conventions here? needs citations
Code quote:
// Create a unique identifier for the binding
let binding_ident = format_ident!("__inner_map_binding_{}", name);
// Format: "outer_name\0inner_name\0" (null-terminated strings)
let binding_value = format!("{}\0{}\0", name, inner);
let binding_len = binding_value.len();
let binding_bytes = binding_value.as_bytes();
quote! {
#[unsafe(link_section = ".maps.inner")]
#[used]
static #binding_ident: [u8; #binding_len] = [#(#binding_bytes),*];
}aya-ebpf-macros/src/map.rs line 42 at r37 (raw file):
} } else { quote! {}
we can drop this b/c Options impls ToTokens
https://docs.rs/quote/latest/quote/trait.ToTokens.html#impl-ToTokens-for-Option%3CT%3E
aya-ebpf-macros/src/map.rs line 118 at r37 (raw file):
); // "OUTER\0INNER_TEMPLATE\0" = 21 bytes assert!(expanded_str.contains("21usize"), "expected 21 bytes");
these assertions are problematic because they emit no information on failure
Code quote:
assert!(
expanded_str.contains(".maps.inner"),
"expected .maps.inner section"
);
assert!(
expanded_str.contains("__inner_map_binding_OUTER"),
"expected binding identifier"
);
// "OUTER\0INNER_TEMPLATE\0" = 21 bytes
assert!(expanded_str.contains("21usize"), "expected 21 bytes");ebpf/aya-ebpf/src/maps/array_of_maps.rs line 19 at r36 (raw file):
unsafe impl<T: InnerMap> Sync for ArrayOfMaps<T> {} impl<T: InnerMap> ArrayOfMaps<T> {
let's reduce some of this boilerplate, see #1447
333e272 to
60f6d7c
Compare
Brskt
left a comment
There was a problem hiding this comment.
@Brskt made 8 comments.
Reviewable status: 19 of 43 files reviewed, 8 unresolved discussions (waiting on @tamird).
Previously, tamird (Tamir Duberstein) wrote…
this commit is ...bad. it's adding a bunch of code that is unused, making review impossible.
Done, is this the way u wanted ?
aya-ebpf-macros/src/map.rs line 20 at r37 (raw file):
Previously, tamird (Tamir Duberstein) wrote…
while you're here, please add
err_on_unknown_args(args)?;(see #1448)
Done.
aya-ebpf-macros/src/map.rs line 40 at r37 (raw file):
Previously, tamird (Tamir Duberstein) wrote…
are we following libbpf conventions here? needs citations
No, this is not following libbpf conventions. Documentation has been added to clarify this.
libbpf uses BTF relocations within the .maps section for inner map bindings (see https://patchwork.ozlabs.org/comment/2418417/), where u declare .values = { [0] = &inner_map, ... } and libbpf processes the relocations.
The .maps.inner section is an aya-specific mechanism. This approach was chosen because:
- aya-ebpf doesn't require BTF for map definitions
- It provides a simpler mechanism that works with both legacy and BTF-style maps
The format is now documented in both aya-ebpf-macros/src/map.rs and aya-obj/src/obj.rs with references to the libbpf implementation.
aya-ebpf-macros/src/map.rs line 42 at r37 (raw file):
Previously, tamird (Tamir Duberstein) wrote…
we can drop this b/c Options impls ToTokens
https://docs.rs/quote/latest/quote/trait.ToTokens.html#impl-ToTokens-for-Option%3CT%3E
Done.
aya-ebpf-macros/src/map.rs line 118 at r37 (raw file):
Previously, tamird (Tamir Duberstein) wrote…
these assertions are problematic because they emit no information on failure
Done.
ebpf/aya-ebpf/src/maps/array_of_maps.rs line 19 at r36 (raw file):
Previously, tamird (Tamir Duberstein) wrote…
let's reduce some of this boilerplate, see #1447
Acknowledged. This PR can be rebased on top of #1447 once it's merged to use the MapDef abstraction, which will eliminate the duplicated UnsafeCell<bpf_map_def> wrapper, unsafe impl Sync, and constructor boilerplate.
Should I wait for #1447 to land first, or would you prefer I implement a similar pattern in this PR?
ebpf/aya-ebpf/src/maps/hash_of_maps.rs line 14 at r36 (raw file):
Previously, tamird (Tamir Duberstein) wrote…
See #1447; use a single phantom plz
Done.
ebpf/aya-ebpf/src/maps/mod.rs line 47 at r36 (raw file):
Previously, tamird (Tamir Duberstein) wrote…
I see. This should be a sealed trait then since we don't want external implementations.
Done.
Each map type now implements Sealed (e.g., impl<T> Sealed for Array<T> {}), preventing external implementations while keeping InnerMap public to satisfy the trait bounds on ArrayOfMaps<T: InnerMap> and HashOfMaps<K, V: InnerMap>.
60f6d7c to
581a00e
Compare
tamird
left a comment
There was a problem hiding this comment.
@tamird partially reviewed 26 files, made 3 comments, and resolved 3 discussions.
Reviewable status: 3 of 43 files reviewed, 7 unresolved discussions (waiting on @Brskt).
aya-ebpf-macros/src/map.rs line 40 at r37 (raw file):
Previously, Brskt wrote…
No, this is not following libbpf conventions. Documentation has been added to clarify this.
libbpf uses BTF relocations within the .maps section for inner map bindings (see https://patchwork.ozlabs.org/comment/2418417/), where u declare
.values = { [0] = &inner_map, ... }and libbpf processes the relocations.The .maps.inner section is an aya-specific mechanism. This approach was chosen because:
- aya-ebpf doesn't require BTF for map definitions
- It provides a simpler mechanism that works with both legacy and BTF-style maps
The format is now documented in both aya-ebpf-macros/src/map.rs and aya-obj/src/obj.rs with references to the libbpf implementation.
Doesn't this mean that libbpf can't load aya programs that use map-in-map, and vice versa? That's generally not the approach we have taken.
A better link: torvalds/linux@646f02ffdd49
aya-ebpf-macros/src/map.rs line 106 at r47 (raw file):
#[test] fn test_map_with_inner() {
the tests above check for the exact generated code, can we follow the same style? if not, please add a comment explaining why
aya-ebpf-macros/src/map.rs line 171 at r47 (raw file):
), ); assert!(result.is_err());
pretty weak assertion
581a00e to
cf9be0a
Compare
Brskt
left a comment
There was a problem hiding this comment.
@Brskt made 3 comments.
Reviewable status: 2 of 48 files reviewed, 7 unresolved discussions (waiting on @tamird).
aya-ebpf-macros/src/map.rs line 40 at r37 (raw file):
Previously, tamird (Tamir Duberstein) wrote…
Doesn't this mean that libbpf can't load aya programs that use map-in-map, and vice versa? That's generally not the approach we have taken.
A better link: torvalds/linux@646f02ffdd49
Done. #[btf_map] with btf_maps::ArrayOfMaps/HashOfMaps now works with both aya and libbpf loaders (tested both). Uses [*const V; 0] for the values field per the BTF relocation format libbpf expects.
Legacy #[map(inner = "...")] remains aya-specific but is now documented as such.
Does this address your concern ?
aya-ebpf-macros/src/map.rs line 106 at r47 (raw file):
Previously, tamird (Tamir Duberstein) wrote…
the tests above check for the exact generated code, can we follow the same style? if not, please add a comment explaining why
Done.
aya-ebpf-macros/src/map.rs line 171 at r47 (raw file):
Previously, tamird (Tamir Duberstein) wrote…
pretty weak assertion
Done.
tamird
left a comment
There was a problem hiding this comment.
@tamird partially reviewed 41 files and made 2 comments.
Reviewable status: 2 of 48 files reviewed, 9 unresolved discussions (waiting on @Brskt).
ebpf/aya-ebpf/src/btf_maps/array.rs line 11 at r53 (raw file):
/// /// This map type stores elements of type `T` indexed by `u32` keys. /// The struct layout is designed to be compatible with both aya and libbpf loaders.
what does that mean?
ebpf/aya-ebpf/src/btf_maps/array.rs line 23 at r53 (raw file):
#[repr(C)] #[allow(dead_code)] pub struct Array<T, const M: usize, const F: usize = 0> {
why did we need to toss bpf_map_def!?
cf9be0a to
fd9cb5b
Compare
Brskt
left a comment
There was a problem hiding this comment.
@Brskt made 2 comments.
Reviewable status: 2 of 48 files reviewed, 9 unresolved discussions (waiting on @tamird).
ebpf/aya-ebpf/src/btf_maps/array.rs line 11 at r53 (raw file):
Previously, tamird (Tamir Duberstein) wrote…
what does that mean?
I've improved the comments. Is it clearer now ?
ebpf/aya-ebpf/src/btf_maps/array.rs line 23 at r53 (raw file):
Previously, tamird (Tamir Duberstein) wrote…
why did we need to toss
bpf_map_def!?
The existing btf_maps that use btf_map_def! (RingBuf, SkStorage) aren't libbpf-compatible either - they only work with aya's loader. For this PR, you requested that map-of-maps be loadable by both aya and libbpf, so I used flat #[repr(C)] structs instead.
fd9cb5b to
0e4c970
Compare
tamird
left a comment
There was a problem hiding this comment.
@tamird made 1 comment.
Reviewable status: 2 of 48 files reviewed, 9 unresolved discussions (waiting on @Brskt).
ebpf/aya-ebpf/src/btf_maps/array.rs line 23 at r53 (raw file):
Previously, Brskt wrote…
The existing btf_maps that use
btf_map_def!(RingBuf, SkStorage) aren't libbpf-compatible either - they only work with aya's loader. For this PR, you requested that map-of-maps be loadable by both aya and libbpf, so I used flat#[repr(C)]structs instead.
Ah, yeah this is also #1455. Would you be willing to send a separate PR to fix that for all the maps?
|
Previously, tamird (Tamir Duberstein) wrote…
Or at least a separate commit would be great. |
Brskt
left a comment
There was a problem hiding this comment.
@Brskt made 1 comment.
Reviewable status: 2 of 48 files reviewed, 9 unresolved discussions (waiting on @tamird and @vadorovsky).
ebpf/aya-ebpf/src/btf_maps/array.rs line 23 at r53 (raw file):
Previously, vadorovsky (Michal R) wrote…
Or at least a separate commit would be great.
Done in #1457
c3d2e4a to
eef6947
Compare
Brskt
left a comment
There was a problem hiding this comment.
@Brskt made 2 comments.
Reviewable status: 47 of 55 files reviewed, 2 unresolved discussions (waiting on alessandrod, tamird, and vadorovsky).
aya/src/maps/mod.rs line 125 at r116 (raw file):
Previously, tamird (Tamir Duberstein) wrote…
The trait is better than giving each one an inherent method, I think.
Done, reverted to the sealed trait approach, same pattern as FromMapData/InnerMap.
ebpf/aya-ebpf/src/btf_maps/mod.rs line 45 at r116 (raw file):
Previously, tamird (Tamir Duberstein) wrote…
i think you can revert the type ascription in the callers now
Done, reverted the type ascription in the callers.
0582c32 to
6dac3d2
Compare
tamird
left a comment
There was a problem hiding this comment.
@tamird reviewed 8 files and all commit messages, and resolved 2 discussions.
Reviewable status:complete! all files reviewed, all discussions resolved (waiting on alessandrod and vadorovsky).
|
@codex review |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 43 out of 43 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /// Missing inner map BTF definition for a map-of-maps. | ||
| #[error( | ||
| "map `{outer_name}` is a map-of-maps but has no inner map definition; \ | ||
| use #[btf_map] with a BTF-typed map-of-maps that includes an inner map type" |
There was a problem hiding this comment.
MapError::MissingInnerMapDefinition (and its doc/error text) is BTF-specific, but this error is also emitted for legacy map-of-maps when the .maps.inner/#[map(inner = ...)] binding is missing. Suggest updating the variant doc + error message to describe the general requirement (“map-of-maps must have an inner map template”) and mention both supported ways to provide it (BTF values spec via #[btf_map] or legacy inner binding).
| /// Missing inner map BTF definition for a map-of-maps. | |
| #[error( | |
| "map `{outer_name}` is a map-of-maps but has no inner map definition; \ | |
| use #[btf_map] with a BTF-typed map-of-maps that includes an inner map type" | |
| /// Missing inner map template definition for a map-of-maps. | |
| #[error( | |
| "map `{outer_name}` is a map-of-maps but has no inner map template; \ | |
| provide an inner map definition via BTF (#[btf_map] values spec) or \ | |
| the legacy inner binding (.maps.inner / #[map(inner = ...)])" |
There was a problem hiding this comment.
Legacy map-of-maps support was intentionally removed in this PR (BTF-only, per review feedback). The error message correctly references only #[btf_map] since that's the only supported path now.
There was a problem hiding this comment.
Interesting that copilot picked up on legacy maps at all. Where did it find that? Perhaps in the PR description?
There was a problem hiding this comment.
It can be, yup. I have updated the description to match the PR.
| // The kernel requires an inner map fd when creating a map-of-maps. | ||
| let btf_inner_map; | ||
| let inner_map_fd = if is_map_of_maps { | ||
| let inner = map_obj.inner().ok_or_else(|| { | ||
| EbpfError::MapError(MapError::MissingInnerMapDefinition { | ||
| outer_name: name.clone(), | ||
| }) | ||
| })?; | ||
| btf_inner_map = MapData::create(inner, &format!("{name}.inner"), btf_fd)?; | ||
| Some(btf_inner_map.fd().as_fd()) | ||
| } else { |
There was a problem hiding this comment.
For map-of-maps with pinning (map_pin_path_by_name or PinningType::ByName), the loader always creates a temporary inner map (btf_inner_map) before attempting to open an already-pinned outer map. This adds unnecessary syscalls and can cause load failures in environments where creating new maps is disallowed even though opening the pinned map would succeed. Consider deferring inner template creation until after you know you need to create the outer map (i.e., only on the “not already pinned” path).
This adds support for BPF_MAP_TYPE_ARRAY_OF_MAPS and BPF_MAP_TYPE_HASH_OF_MAPS on the eBPF side. Implements: - ArrayOfMaps and HashOfMaps for legacy maps - ArrayOfMaps and HashOfMaps for BTF maps - InnerMap sealed trait to mark types usable as inner maps
Adds the `inner` attribute to specify the inner map type for map-of-maps (ArrayOfMaps and HashOfMaps). Example usage: #[map] static OUTER: ArrayOfMaps<Array<u32>, 4> = ArrayOfMaps::new(0); #[map(inner = "OUTER")] static INNER: Array<u32> = Array::with_max_entries(1, 0);
Adds userspace support for BPF_MAP_TYPE_ARRAY_OF_MAPS and BPF_MAP_TYPE_HASH_OF_MAPS. Key changes: - aya-obj: track inner map definitions and initial map FDs - aya: Array and HashMap of_maps modules with get/set/iter - aya: populate inner maps during EbpfLoader::load() - Automatic inner map creation from BTF map definitions
Tests for ArrayOfMaps and HashOfMaps: - Legacy maps with manual inner map setup - BTF maps with automatic inner map creation - Dynamic inner map allocation at runtime - Also adds prog_array tests for ProgramArray
- Add sealed InnerMap trait; set()/insert() now take &impl InnerMap instead of &MapFd for compile-time validation - Implement InnerMap for all kernel-supported inner map types, MapData, and MapFd - Add pub(crate) map_fd() to PerfEventArray and RingBuf for InnerMap impls (different field layout than other map types) - Remove fd()/map_data() from Array, HashMap; remove fd() from ArrayOfMaps, HashOfMaps - Flatten nested if in bpf.rs inner_map_fd logic - Update integration tests to pass &map instead of map.fd()
Enrich the sealed Map trait with Key and Value associated types so that map-of-maps containers can perform fused two-level lookups without intermediate struct indirection. This reduces BPF verifier state explosion in tight loops. eBPF side: - Add Key/Value to private::Map and public Map with blanket forwarding. - Introduce impl_private_map! macro to replace per-file boilerplate. - Add get_value/get_value_ptr_mut to ArrayOfMaps and HashOfMaps. Userspace side: - Restructure inner map BTF/fallback logic in bpf.rs. - Add V type parameter to ArrayOfMaps and HashOfMaps. - Refactor impl_try_from_map! with @impl internal rule and add impl_try_from_map_of_maps! for unconstrained V.
Test fused lookups on both ArrayOfMaps and HashOfMaps: userspace pre-populates inner maps, the eBPF program reads via get_value and writes via get_value_ptr_mut, then userspace verifies the results.
Replace `map_fd()` with `map_data()` on `RingBuf` and `PerfEventArray`, returning `&MapData`. Update sealed `InnerMap` impls to use `map_data().fd()`. Also clean up `of_maps` docs/tests: - remove untyped-handle wording - remove redundant type ascriptions/default type parameters - use typed literals where inference needs help (`1u32`, `&1u32`)
Remove redundant `Key`/`Value` associated types from the public `Map` trait; they resolve through the sealed `private::Map` supertrait. Drop `impl_private_map!` in favor of explicit `private::Map` impls in map modules, and simplify projections from `<T as Map>::Key` to `T::Key` (and same for `Value`).
Simplify FromMapData and InnerMap into pure marker traits, moving methods into their sealed supertraits. Rename inner_map_fd to fd and error field name to outer_name for clarity. Constrain V to InnerMap in map-of-maps TryFrom impls. Remove explicit MapData type params from of_maps tests in favor of defaults. In integration tests, rename OUTER to ARRAY_OF_MAPS, replace Array<u32, 4> with Array<TestResult, 1> for named fields, and make trigger functions const extern "C" fn.
- Drop legacy map-of-maps support - Replace impl_create_map macro with a CreatableMap trait - Unify PerfEventArray/RingBuf into impl_from_map_data via accessor arm - Add fused lookups (get_value/get_value_ptr_mut) for BTF map-of-maps - Rename tests to btf_map_of_maps
- Rename new_legacy to new_from_params to reflect actual usage - Move CreatableMap into sealed module with public wrapper trait - Replace impl_create_map macro and 8 manual trait impls with impl_creatable_map macro
- Remove CreatableMap trait, create() is now a direct inherent method - Make lookup_inner generic over M: MapDef instead of separate K, V - Revert ArrayOfMaps test annotations to turbofish form
…r turbofish - Restore CreatableMap as a sealed trait with public wrapper and blanket impl, matching the FromMapData/InnerMap pattern - Revert type ascription on lookup_inner/lookup_inner_ptr_mut callers now that inner_map is NonNull<M>
- Defer inner map template creation in the loader until we know the outer map needs to be created - Change create_pinned_by_name to accept the inner map definition instead of a pre-created fd, creating it only when bpf_get_object fails
6dac3d2 to
85183c6
Compare
tamird
left a comment
There was a problem hiding this comment.
@tamird reviewed 2 files and all commit messages, and made 1 comment.
Reviewable status: all files reviewed, 2 unresolved discussions (waiting on alessandrod, Brskt, and vadorovsky).
| /// Missing inner map BTF definition for a map-of-maps. | ||
| #[error( | ||
| "map `{outer_name}` is a map-of-maps but has no inner map definition; \ | ||
| use #[btf_map] with a BTF-typed map-of-maps that includes an inner map type" |
There was a problem hiding this comment.
Interesting that copilot picked up on legacy maps at all. Where did it find that? Perhaps in the PR description?
| /// This is used to create inner maps for map-of-maps types. | ||
| /// | ||
| /// This trait is sealed and cannot be implemented outside of this crate. | ||
| pub trait CreatableMap: sealed::CreatableMap { |
There was a problem hiding this comment.
why do we need this trait? doesn't seem to be used as a bound anywhere and API
wise it's better if I can call create() without having to import a trait
There was a problem hiding this comment.
I don't know, @tamird said "The trait is better than giving each one an inherent method, I think.". Which way do I put ?
There was a problem hiding this comment.
I don't think that makes sense?
The trait still has to be implemented for each implementor, so code wise it's even more code. It's not used as a bound anywhere. And - and worst part - now users have to import the trait to call create.
The trait is effectively unused today I think we should remove it. If we ever need to pass a generic type based on the existence of create we can add it back.
There was a problem hiding this comment.
Can we use it as a bound? Traits at least give a clue that API should be implemented by all maps and give a name to the common surface. A macro can do this as well, it's just a little more fragile.
There was a problem hiding this comment.
I don't think it makes sense? Why are we leaking extra API to users that doesn't bring any benefits to them?
If we need to internally ensure we don't forget to implement something (and we probably should now that we support so much of the ebpf API!), we should figure out a way to do so without impacting the public API.
There was a problem hiding this comment.
I took a closer look; maybe the issue is that we have 3 traits that are implemented for all the same types?
impl_from_map_data!((V) {
Array, BloomFilter, PerCpuArray,
Queue, SockHash, SkStorage, Stack,
});
impl_from_map_data!((K, V) {
HashMap, LpmTrie, PerCpuHashMap,
});
...
impl_creatable_map!(Array<MapData, V: Pod>,
bpf_map_type::BPF_MAP_TYPE_ARRAY, size_of::<u32>() as u32, size_of::<V>() as u32, "standalone_array");
impl_creatable_map!(PerCpuArray<MapData, V: Pod>,
bpf_map_type::BPF_MAP_TYPE_PERCPU_ARRAY, size_of::<u32>() as u32, size_of::<V>() as u32, "standalone_percpu_array");
impl_creatable_map!(BloomFilter<MapData, V: Pod>,
bpf_map_type::BPF_MAP_TYPE_BLOOM_FILTER, 0, size_of::<V>() as u32, "standalone_bloom_filter");
impl_creatable_map!(Queue<MapData, V: Pod>,
bpf_map_type::BPF_MAP_TYPE_QUEUE, 0, size_of::<V>() as u32, "standalone_queue");
impl_creatable_map!(Stack<MapData, V: Pod>,
bpf_map_type::BPF_MAP_TYPE_STACK, 0, size_of::<V>() as u32, "standalone_stack");
impl_creatable_map!(HashMap<MapData, K: Pod, V: Pod>,
bpf_map_type::BPF_MAP_TYPE_HASH, size_of::<K>() as u32, size_of::<V>() as u32, "standalone_hash");
impl_creatable_map!(PerCpuHashMap<MapData, K: Pod, V: Pod>,
bpf_map_type::BPF_MAP_TYPE_PERCPU_HASH, size_of::<K>() as u32, size_of::<V>() as u32, "standalone_percpu_hash");
impl_creatable_map!(LpmTrie<MapData, K: Pod, V: Pod>,
bpf_map_type::BPF_MAP_TYPE_LPM_TRIE, size_of::<lpm_trie::Key<K>>() as u32, size_of::<V>() as u32, "standalone_lpm_trie");
the first macro impls FromMapData and InnerMap and the second impls CreatableMap. Perhaps we just need one trait? Note also that CreatableMap (the outer one) still has an unnecessary trampoline function to the sealed trait's function.
There was a problem hiding this comment.
Why do we need CreatableMap? What do we need to parametrize based on the existence of ::create?
There was a problem hiding this comment.
I don't understand your question as a reply to my comment. Can we just make all these traits one trait which represents a map that isn't of-maps?
There was a problem hiding this comment.
your comment does a bunch of impl_creatable_map and it's not clear to me why the CreatableMap trait needs to exist to begin with
there is no use for API users, it shouldn't be in the API
| @@ -0,0 +1,72 @@ | |||
| use core::ptr::NonNull; | |||
There was a problem hiding this comment.
OCD nit: would be great if we could keep the module names consistent with
userspace?
userspace is aya::maps::of_maps::ArrayOfMaps
this one is aya_ebpf::btf_maps::array_of_maps::ArrayOfMaps
There was a problem hiding this comment.
There are two submodules in aya_ebpf: aya_ebpf::maps (for legacy maps) and aya_ebpf::btf_maps (for BTF ones). We could change the latter to something else, if you want (there is no release containing the BTF maps yet), but that would be a separate change. This is consistent with what we have on main currently.
|
This also needs a rebase now I'm afraid |
Summary
This PR is a continuation of #70, rebased onto the current main branch and extended with additional functionality.
It adds BTF-only support for BPF map-of-maps (
BPF_MAP_TYPE_HASH_OF_MAPSandBPF_MAP_TYPE_ARRAY_OF_MAPS):btf_maps::ArrayOfMapsandbtf_maps::HashOfMapswith fusedget_value/get_value_ptr_mutlookupsMapDeftrait for inner map type constraintsvaluesfieldArrayOfMapsandHashOfMapsuserspace types withget(),set()/insert(),keys()methodsCreatableMaptrait for standalone inner map creationFromMapDataandInnerMaptraits (pure markers)Example usage (eBPF side)
Example usage (userspace side)
Test plan
This change is