Skip to content

feat(aya): add support for map-of-maps (HashOfMaps, ArrayOfMaps)#1446

Open
Brskt wants to merge 13 commits intoaya-rs:mainfrom
Brskt:hashmapofmaps-new
Open

feat(aya): add support for map-of-maps (HashOfMaps, ArrayOfMaps)#1446
Brskt wants to merge 13 commits intoaya-rs:mainfrom
Brskt:hashmapofmaps-new

Conversation

@Brskt
Copy link
Contributor

@Brskt Brskt commented Jan 17, 2026

Summary

This PR is a continuation of #70, rebased onto the current main branch and extended with additional functionality.

It adds comprehensive support for BPF map-of-maps (BPF_MAP_TYPE_HASH_OF_MAPS and BPF_MAP_TYPE_ARRAY_OF_MAPS):

  • aya-ebpf: Add btf_maps::ArrayOfMaps and btf_maps::HashOfMaps (libbpf-compatible, uses BTF relocations)
  • aya-ebpf: Add maps::ArrayOfMaps and maps::HashOfMaps (legacy)
  • aya-ebpf-macros: Add inner attribute to #[map] macro for specifying inner map templates (uses .maps.inner section)
  • aya-obj: Parse .maps.inner section for inner map bindings
  • aya: Use explicit inner map bindings in EbpfLoader
  • aya: Add ArrayOfMaps and HashOfMaps userspace types with get(), set()/insert(), keys(), fd() methods
  • aya: Add Array::create() and HashMap::create() for dynamic inner map creation
  • aya: Add unit tests for both map types

Example usage (eBPF side)

BTF (libbpf-compatible) - Recommended:

use aya_ebpf::{btf_maps::{Array, ArrayOfMaps}, macros::btf_map};

// Inner map definition is parsed automatically from BTF `values` field.
#[btf_map]
static OUTER: ArrayOfMaps<Array<u32, 10>, 4> = ArrayOfMaps::new();

Legacy (aya-only):

#[map(inner = "INNER_MAP")]
static OUTER_MAP: HashOfMaps<u32, HashMap<u32, u32>> = HashOfMaps::with_max_entries(4, 0);

#[map]
static INNER_MAP: HashMap<u32, u32> = HashMap::with_max_entries(128, 0);

Example usage (userspace side)

// Set inner maps via FD
let mut outer: ArrayOfMaps<&mut MapData> = ebpf.map_mut("OUTER").unwrap().try_into()?;
outer.set(0, &inner_fd, 0)?;

// Create inner maps dynamically
let inner: HashMap<MapData, u32, u32> = HashMap::create(10, 0)?;

Test plan

  • Unit tests for HashOfMaps (10 tests)
  • Unit tests for ArrayOfMaps (9 tests)
  • Integration tests for legacy map-of-maps
  • Integration tests for BTF map-of-maps
  • Verified BTF map-of-maps with automatic inner map resolution

This change is Reviewable

@netlify
Copy link

netlify bot commented Jan 17, 2026

Deploy Preview for aya-rs-docs ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit e4d474a
🔍 Latest deploy log https://app.netlify.com/projects/aya-rs-docs/deploys/69b01d3c8178b80008b21b70
😎 Deploy Preview https://deploy-preview-1446--aya-rs-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@tamird
Copy link
Member

tamird commented Jan 17, 2026

@codex review

@tamird tamird requested a review from Copilot January 17, 2026 21:47
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 inner attribute to #[map] macro for declaring map-of-maps templates in eBPF code
  • Implemented HashMapOfMaps and ArrayOfMaps types with get(), iter(), and other helper methods
  • Added support for program array population via EbpfLoader::set_prog_array_entry() and Ebpf::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.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +49 to +57
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())?;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this deserves some inline comments?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines +65 to +73
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())?;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above - this is the correct behavior. The kernel's map-of-maps syscall API is asymmetric by design:

  • BPF_MAP_UPDATE_ELEM takes an FD
  • BPF_MAP_LOOKUP_ELEM returns 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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this deserves some inline comments?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Member

@tamird tamird left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Comment on lines +65 to +73
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())?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this deserves some inline comments?

Comment on lines +49 to +57
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())?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this deserves some inline comments?

@Brskt Brskt force-pushed the hashmapofmaps-new branch from f2593b3 to 333e272 Compare January 18, 2026 13:10
Copy link
Contributor Author

@Brskt Brskt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Brskt made 3 comments and resolved 2 discussions.
Reviewable status: 0 of 43 files reviewed, 1 unresolved discussion (waiting on @tamird).


-- commits line 70 at r35:

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:

  1. aya-ebpf: eBPF-side map-of-maps implementation
  2. aya-ebpf-macros: inner attribute for #[map] macro
  3. aya-obj: Map constructors and .maps.inner parsing
  4. aya: userspace map-of-maps support
  5. tests: integration and unit tests
  6. public API updates

Should be easier to review now.

Comment on lines +65 to +73
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())?;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines +49 to +57
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())?;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Member

@tamird tamird left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tamird partially reviewed 15 files and made 2 comments.
Reviewable status: 1 of 43 files reviewed, 2 unresolved discussions (waiting on @Brskt).


-- commits line 70 at r35:

Previously, Brskt wrote…

Yes, I've kept the commit history and rewritten it as requested:

  1. aya-ebpf: eBPF-side map-of-maps implementation
  2. aya-ebpf-macros: inner attribute for #[map] macro
  3. aya-obj: Map constructors and .maps.inner parsing
  4. aya: userspace map-of-maps support
  5. tests: integration and unit tests
  6. 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?

Copy link
Contributor Author

@Brskt Brskt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Brskt made 2 comments.
Reviewable status: 1 of 43 files reviewed, 2 unresolved discussions (waiting on @tamird).


-- commits line 70 at r35:

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 CI
  • cargo test: ✅ pass
  • integration tests: ✅ 127 passed
  • public-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.

Copy link
Member

@tamird tamird left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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 CI
  • cargo test: ✅ pass
  • integration tests: ✅ 127 passed
  • public-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.

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

@Brskt Brskt force-pushed the hashmapofmaps-new branch from 333e272 to 60f6d7c Compare January 18, 2026 17:28
Copy link
Contributor Author

@Brskt Brskt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Brskt made 8 comments.
Reviewable status: 19 of 43 files reviewed, 8 unresolved discussions (waiting on @tamird).


-- commits line 25 at r38:

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>.

@Brskt Brskt force-pushed the hashmapofmaps-new branch from 60f6d7c to 581a00e Compare January 20, 2026 17:40
Copy link
Member

@tamird tamird left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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

@Brskt Brskt force-pushed the hashmapofmaps-new branch from 581a00e to cf9be0a Compare January 20, 2026 22:42
Copy link
Contributor Author

@Brskt Brskt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Copy link
Member

@tamird tamird left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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!?

@Brskt Brskt force-pushed the hashmapofmaps-new branch from cf9be0a to fd9cb5b Compare January 20, 2026 23:27
Copy link
Contributor Author

@Brskt Brskt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

@Brskt Brskt force-pushed the hashmapofmaps-new branch from fd9cb5b to 0e4c970 Compare January 22, 2026 20:58
Copy link
Member

@tamird tamird left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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?

@vadorovsky
Copy link
Member

ebpf/aya-ebpf/src/btf_maps/array.rs line 23 at r53 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

Ah, yeah this is also #1455. Would you be willing to send a separate PR to fix that for all the maps?

Or at least a separate commit would be great.

Copy link
Contributor Author

@Brskt Brskt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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

@Brskt Brskt force-pushed the hashmapofmaps-new branch 5 times, most recently from c3d2e4a to eef6947 Compare January 27, 2026 16:12
@zz85
Copy link

zz85 commented Feb 21, 2026

I tried on my end and see no slowdown at all; the maps, loading or executing the program. Can u describe more?

it seems that the Array::get() method, when called on an inner map reference returned from ArrayOfMaps::get(), generates BPF bytecode with extra intermediate instructions between the outer and inner bpf_map_lookup_elem calls.
In my case it seems the the different code structure causes the BPF verifier's state tracking to explode, because I have a binary search loop going over that. I'll share a fix I'll do on my end, and see if there's room for improvement here

@Brskt the workaround I have is in zz85/profile-bee#74

The ArrayOfMaps<Array> results in a 2 step lookup in the call chain which LLVM seem to be generating different/larger BPF instructions. In my use case there's a binary search of up to 17 interaction so the additional instructions overshoots.

One suggest for your change would be to perform both the outer and inner lookup in a single call, so the inner map value gets returned without going through the Array::get() indirection. eg.

impl ArrayOfMaps<T> {
   get_inner_value(outer, inner) {
        unsafe {
            let inner_map_ptr = bpf_map_lookup_elem(outer);
           let value = bpf_map_lookup_elm(inner);
       ....
        }
   }
}

Copy link
Contributor Author

@Brskt Brskt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Brskt made 3 comments.
Reviewable status: 42 of 53 files reviewed, 4 unresolved discussions (waiting on alessandrod, tamird, and vadorovsky).


aya/src/bpf.rs line 562 at r110 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

ah i see what this is doing now. can we write

// The kernel ...
let inner_map = if is_map_of_maps {
  // Try using a BTF definition of the inner map.
  map_ob.inner().map(|inner| MapData::create(inner, &format!("{name}.inner"), btf_fd)).or_else(|| {
                    // No BTF inner definition; fall back to the `.maps.inner` binding.
                let inner_name = obj.inner_map_binding(&name).ok_or_else(|| {
                    EbpfError::MapError(MapError::MissingInnerMapBinding { name: name.clone() })
                })?;
                let inner_map = maps.get(inner_name).ok_or_else(|| {
                    EbpfError::MapError(MapError::InnerMapNotFound {
                        name: name.clone(),
                        inner_name: inner_name.to_owned(),
                    })
                })?;
                Ok(inner_map)
  }).transpose()?
} else {
  None
}

something like that?

Done. Used if let instead of or_else since the BTF path produces an owned MapData (for lifetime) while the fallback borrows from maps.


aya/src/maps/of_maps/array.rs line 85 at r108 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

But why that and not the specific map type? We know what T is, why can't we just use &T?

Yup, we can. Added V type parameter, set()/insert() now take &V instead of &impl InnerMap.


aya/src/maps/of_maps/array.rs line 109 at r108 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

you didn't remove map_data

Done, removed.

@Brskt
Copy link
Contributor Author

Brskt commented Feb 21, 2026

@Brskt the workaround I have is in zz85/profile-bee#74

The ArrayOfMaps results in a 2 step lookup in the call chain which LLVM seem to be generating different/larger BPF instructions. In my use case there's a binary search of up to 17 interaction so the additional instructions overshoots.

One suggest for your change would be to perform both the outer and inner lookup in a single call, so the inner map value gets returned without going through the Array::get() indirection. eg.

@zz85 Some changes have been made about this; it should be better.

@zz85
Copy link

zz85 commented Feb 21, 2026

@Brskt - thanks! I can confirm that the array.get_value() api works for me.

Copy link
Member

@tamird tamird left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tamird reviewed 23 files and all commit messages, made 10 comments, and resolved 3 discussions.
Reviewable status: 43 of 53 files reviewed, 11 unresolved discussions (waiting on alessandrod, Brskt, and vadorovsky).


aya/src/bpf.rs line 539 at r96 (raw file):

                            .unwrap_or_else(|| Path::new("/sys/fs/bpf"));
                        let path = path.join(&name);

nit: restore


aya/src/maps/ring_buf.rs line 115 at r111 (raw file):

impl RingBuf<MapData> {
    pub(crate) const fn map_fd(&self) -> &MapFd {

why do you need this? most of our maps already define fn map(&self) -> &MapData { which we should probably centralize and then just use to get the FD. I am confused by all the new code that seems to duplicate existing functionality.


aya/src/maps/of_maps/array.rs line 64 at r111 (raw file):

    ///
    /// The inner map type `V` is determined by the type parameter on the
    /// `ArrayOfMaps` itself. Use `MapData` to retrieve an untyped handle.

why are we telling folks how to get an untyped handle?


aya/src/maps/of_maps/array.rs line 154 at r111 (raw file):

        let map = new_map(test_utils::new_obj_map::<u8>(BPF_MAP_TYPE_ARRAY_OF_MAPS));
        assert_matches!(
            ArrayOfMaps::<_, MapData>::new(&map),

this is the default value of the parameter, is it needed? here and everywhere


aya/src/maps/of_maps/array.rs line 251 at r111 (raw file):

    fn test_get_not_found() {
        let map = new_map(new_obj_map());
        let arr: ArrayOfMaps<_> = ArrayOfMaps::new(&map).unwrap();

why do you need this ascription?


aya/src/maps/of_maps/hash_map.rs line 60 at r111 (raw file):

    ///
    /// The inner map type `V` is determined by the type parameter on the
    /// `HashOfMaps` itself. Use `MapData` to retrieve an untyped handle.

ditto


aya/src/maps/of_maps/hash_map.rs line 172 at r111 (raw file):

            aya_obj::generated::bpf_map_type::BPF_MAP_TYPE_HASH,
        ));
        let mut hm: HashOfMaps<_, u32, MapData> = HashOfMaps::new(&mut map).unwrap();

same question here about all the unnecessary ascription


aya/src/maps/perf/perf_event_array.rs line 187 at r111 (raw file):

impl PerfEventArray<MapData> {
    pub(crate) fn map_fd(&self) -> &MapFd {

same as the ring_buf comment


ebpf/aya-ebpf/src/maps/mod.rs line 79 at r111 (raw file):

}

macro_rules! impl_private_map {

unclear how much this macro helps, though if you move fn map into it, that would be nice (see other comments)


ebpf/aya-ebpf/src/maps/mod.rs line 149 at r111 (raw file):

    type Key;
    /// The value type declared in this map's definition.
    type Value;

do we need these? we can't just use them through the supertrait?

Code quote:

    /// The key type declared in this map's definition.
    type Key;
    /// The value type declared in this map's definition.
    type Value;

Copy link
Contributor Author

@Brskt Brskt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Brskt made 10 comments and resolved 1 discussion.
Reviewable status: 21 of 53 files reviewed, 10 unresolved discussions (waiting on alessandrod, tamird, and vadorovsky).


aya/src/bpf.rs line 539 at r96 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

nit: restore

Done.


aya/src/maps/ring_buf.rs line 115 at r111 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

why do you need this? most of our maps already define fn map(&self) -> &MapData { which we should probably centralize and then just use to get the FD. I am confused by all the new code that seems to duplicate existing functionality.

Done. Replaced map_fd() with map_data() -> &MapData on both types, which mirrors the IterableMap::map() pattern. They don't implement IterableMap (not iterable), so this fills that gap.


aya/src/maps/of_maps/array.rs line 64 at r111 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

why are we telling folks how to get an untyped handle?

Should not, removed.


aya/src/maps/of_maps/array.rs line 154 at r111 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

this is the default value of the parameter, is it needed? here and everywhere

Right, removed the explicit default everywhere.


aya/src/maps/of_maps/array.rs line 251 at r111 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

why do you need this ascription?

Removed the local type ascription. Type inference is now driven by typed usage in the test (u32 keys), and I kept explicit type hints only where inference would otherwise be ambiguous.


aya/src/maps/of_maps/hash_map.rs line 60 at r111 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

ditto

Done.


aya/src/maps/of_maps/hash_map.rs line 172 at r111 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

same question here about all the unnecessary ascription

Done.


aya/src/maps/perf/perf_event_array.rs line 187 at r111 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

same as the ring_buf comment

Done.


ebpf/aya-ebpf/src/maps/mod.rs line 79 at r111 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

unclear how much this macro helps, though if you move fn map into it, that would be nice (see other comments)

Agreed, reverted. The macro didn’t provide enough value as-is. I kept explicit impl private::Map blocks in this PR; fn map() centralization can be a separate cleanup.


ebpf/aya-ebpf/src/maps/mod.rs line 149 at r111 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

do we need these? we can't just use them through the supertrait?

Yep, good point. We can use the associated types via the supertrait, so I removed Key/Value from the public Map trait and updated the signatures accordingly.

@Brskt Brskt requested a review from tamird March 6, 2026 18:37
Copy link
Member

@tamird tamird left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tamird reviewed 29 files and all commit messages, made 20 comments, and resolved 8 discussions.
Reviewable status: 50 of 53 files reviewed, 20 unresolved discussions (waiting on alessandrod, Brskt, and vadorovsky).


aya/src/maps/of_maps/array.rs line 154 at r111 (raw file):

Previously, Brskt wrote…

Right, removed the explicit default everywhere.

still here isn't it?


aya/src/maps/mod.rs line 107 at r112 (raw file):

///
/// This is used by map-of-maps types ([`ArrayOfMaps`], [`HashOfMaps`]) to
/// let callers specify the expected inner map type when retrieving entries.

this description does not seem correct

Code quote:

/// This is used by map-of-maps types ([`ArrayOfMaps`], [`HashOfMaps`]) to
/// let callers specify the expected inner map type when retrieving entries.

aya/src/maps/mod.rs line 117 at r112 (raw file):

impl<T: sealed::FromMapData> FromMapData for T {
    fn from_map_data(map_data: MapData) -> Result<Self, MapError> {
        <Self as sealed::FromMapData>::from_map_data(map_data)

if the supertrait already has this method then why do we need it on the subtrait + this trampoline?


aya/src/maps/mod.rs line 124 at r112 (raw file):

///
/// Types implementing this trait can be passed to
/// [`ArrayOfMaps::set`] and [`HashOfMaps::insert`].

this comment is stale since those functions now take the outer map's V

Code quote:

/// Types implementing this trait can be passed to
/// [`ArrayOfMaps::set`] and [`HashOfMaps::insert`].

aya/src/maps/mod.rs line 129 at r112 (raw file):

pub trait InnerMap: sealed::InnerMap {
    /// Returns the map file descriptor.
    fn fd(&self) -> &MapFd;

ditto, why do we need this trampoline?


aya/src/maps/mod.rs line 164 at r112 (raw file):

    MissingInnerMapBinding {
        /// The map name.
        name: String,

can this be outer_name to avoid ambiguity? ditto below on InnerMapNotFound


aya/src/maps/mod.rs line 291 at r112 (raw file):

    }

    /// Creates a new instance that shares the same underlying file description as `self`.

👏 props to claude for knowing the difference between descriptor and description here.


aya/src/maps/mod.rs line 641 at r112 (raw file):

});

// ArrayOfMaps and HashOfMaps need an unconstrained V in TryFrom conversions.

unconstrained, why? shouldn't it be constrained to InnerMap?


aya/src/maps/mod.rs line 660 at r112 (raw file):

// Implements `sealed::FromMapData` for map types that the kernel supports as inner maps.
// Excluded: ProgramArray (no map_meta_equal), ArrayOfMaps/HashOfMaps (multi-level nesting
// forbidden).

from the midway point of the first sentence until the end of this comment, it is describing the invocation (below) rather than the macro.

Code quote:

// Implements `sealed::FromMapData` for map types that the kernel supports as inner maps.
// Excluded: ProgramArray (no map_meta_equal), ArrayOfMaps/HashOfMaps (multi-level nesting
// forbidden).

aya/src/maps/mod.rs line 684 at r112 (raw file):

});

// PerfEventArray and RingBuf use a different field layout, so InnerMap is

but don't they all have map_data()? couldn't you use this shape for all of them?


aya/src/maps/mod.rs line 721 at r112 (raw file):

        Ok(map_data)
    }
}

erhm, why?

Code quote:

impl sealed::FromMapData for MapData {
    fn from_map_data(map_data: MapData) -> Result<Self, MapError> {
        Ok(map_data)
    }
}

aya/src/maps/mod.rs line 733 at r112 (raw file):

        self
    }
}

why?

Code quote:

impl sealed::InnerMap for MapData {
    fn inner_map_fd(&self) -> &MapFd {
        self.fd()
    }
}

impl sealed::InnerMap for MapFd {
    fn inner_map_fd(&self) -> &MapFd {
        self
    }
}

aya/src/maps/mod.rs line 777 at r112 (raw file):

    PerCpuHashMap(BPF_MAP_TYPE_PERCPU_HASH, size_of::<K>() as u32, V, "standalone_percpu_hash"),
    LpmTrie(BPF_MAP_TYPE_LPM_TRIE, size_of::<lpm_trie::Key<K>>() as u32, V, "standalone_lpm_trie"),
});

this feels like significant coupling at a distance. in other words, the types and constants encoded here need to match these same ones encoded in the implementation of each map. can we avoid this fragile duplication?

Code quote:

// Implements `create()` for map types that can be created standalone for use as inner maps in
// map-of-maps. Each invocation specifies the kernel map type, key size expression, the type
// parameter used for value size, and a name for the created map.
macro_rules! impl_create_map {
    ($ty_param:tt {
        $($ty:ident($map_type:ident, $key_size:expr, $val:ident, $name:literal)),+ $(,)?
    }) => {
        $(impl_create_map!(<$ty_param> $ty($map_type, $key_size, $val, $name));)+
    };
    (<($($ty_param:ident),+)> $ty:ident($map_type:ident, $key_size:expr, $val:ident, $name:literal)) => {
        impl<$($ty_param: Pod),+> $ty<MapData, $($ty_param),+> {
            /// Creates a standalone map, not loaded from an eBPF object file.
            ///
            /// This is useful for creating inner maps to insert into map-of-maps
            /// types like [`ArrayOfMaps`](crate::maps::ArrayOfMaps) or
            /// [`HashOfMaps`](crate::maps::HashOfMaps).
            pub fn create(max_entries: u32, flags: u32) -> Result<Self, MapError> {
                let obj = aya_obj::Map::new_legacy(
                    aya_obj::generated::bpf_map_type::$map_type as u32,
                    $key_size,
                    size_of::<$val>() as u32,
                    max_entries,
                    flags,
                );
                Self::new(MapData::create(obj, $name, None)?)
            }
        }
    };
}

impl_create_map!((V) {
    Array(BPF_MAP_TYPE_ARRAY, size_of::<u32>() as u32, V, "standalone_array"),
    PerCpuArray(BPF_MAP_TYPE_PERCPU_ARRAY, size_of::<u32>() as u32, V, "standalone_percpu_array"),
    BloomFilter(BPF_MAP_TYPE_BLOOM_FILTER, 0, V, "standalone_bloom_filter"),
    Queue(BPF_MAP_TYPE_QUEUE, 0, V, "standalone_queue"),
    Stack(BPF_MAP_TYPE_STACK, 0, V, "standalone_stack"),
});

impl_create_map!((K, V) {
    HashMap(BPF_MAP_TYPE_HASH, size_of::<K>() as u32, V, "standalone_hash"),
    PerCpuHashMap(BPF_MAP_TYPE_PERCPU_HASH, size_of::<K>() as u32, V, "standalone_percpu_hash"),
    LpmTrie(BPF_MAP_TYPE_LPM_TRIE, size_of::<lpm_trie::Key<K>>() as u32, V, "standalone_lpm_trie"),
});

aya-ebpf-macros/src/map.rs line 29 at r112 (raw file):

        let item = &self.item;

        // Aya-specific mechanism for inner map bindings (legacy, NOT libbpf-compatible).

can we just take the same approach as libbpf and NOT support legacy maps-in-maps?


aya-obj/src/maps.rs line 287 at r112 (raw file):

    ///
    /// This is useful for creating inner maps dynamically for map-of-maps types.
    pub const fn new_legacy(

reminder to remove if we agree not to support legacy maps of maps


aya-obj/src/obj.rs line 902 at r112 (raw file):

    }

    /// Parses the `.maps.inner` section which contains outer->inner map bindings.

same comment here as elsewhere (and reminder to remove associated error types if we agree)


ebpf/aya-ebpf/src/maps/array_of_maps.rs line 66 at r112 (raw file):

    /// Same as [`get_value`](Self::get_value) but returns a mutable pointer.
    #[inline(always)]
    pub fn get_value_ptr_mut(&self, outer_index: u32, inner_key: &T::Key) -> Option<*mut T::Value> {

here and in the hash map - why return a mut pointer rather than a mut ref?


test/integration-ebpf/src/btf_map_of_maps.rs line 21 at r112 (raw file):

// The inner map definition is parsed from the BTF `values` field at load time.
#[btf_map]
static OUTER: ArrayOfMaps<Array<u32, 10>, 4> = ArrayOfMaps::new();

probably no longer makes sense to call this OUTER?


test/integration-ebpf/src/btf_map_of_maps.rs line 25 at r112 (raw file):

// Result array to verify values from userspace.
#[btf_map]
static RESULTS: Array<u32, 4> = Array::new();

why 4? why not an array of 1 element containing a structure?


test/integration-ebpf/src/btf_map_of_maps.rs line 31 at r112 (raw file):

#[expect(
    clippy::missing_const_for_fn,
    reason = "extern functions cannot be const"

uh, i think this is is not true. it can be const

@Brskt Brskt force-pushed the hashmapofmaps-new branch from 8492aae to 6ba0d85 Compare March 9, 2026 16:49
Copy link
Contributor Author

@Brskt Brskt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Brskt made 17 comments.
Reviewable status: 50 of 53 files reviewed, 20 unresolved discussions (waiting on alessandrod, tamird, and vadorovsky).


aya/src/maps/mod.rs line 107 at r112 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

this description does not seem correct

Removed.


aya/src/maps/mod.rs line 117 at r112 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

if the supertrait already has this method then why do we need it on the subtrait + this trampoline?

Done, from_map_data now lives only on the sealed supertrait.


aya/src/maps/mod.rs line 124 at r112 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

this comment is stale since those functions now take the outer map's V

Removed.


aya/src/maps/mod.rs line 129 at r112 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

ditto, why do we need this trampoline?

Done, same as FromMapData, removed the trampoline, renamed inner_map_fd to fd on the sealed supertrait.


aya/src/maps/mod.rs line 164 at r112 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

can this be outer_name to avoid ambiguity? ditto below on InnerMapNotFound

Done, renamed to outer_name.


aya/src/maps/mod.rs line 641 at r112 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

unconstrained, why? shouldn't it be constrained to InnerMap?

It can, constrained V to InnerMap in the TryFrom impls.


aya/src/maps/mod.rs line 660 at r112 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

from the midway point of the first sentence until the end of this comment, it is describing the invocation (below) rather than the macro.

Done, moved the exclusion list to the invocation site.


aya/src/maps/mod.rs line 684 at r112 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

but don't they all have map_data()? couldn't you use this shape for all of them?

Not all map types have map_data(), only PerfEventArray and RingBuf (different field layout: Arc<T> / T vs inner: T). Unifying would mean adding map_data() to all map types. Want me to do that here or in a follow-up?


aya/src/maps/mod.rs line 721 at r112 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

erhm, why?

It allows get() to work with the default V = MapData, returning an untyped handle for introspection (info(), pin(), fd()). If this not worth keeping, I can remove it.


aya/src/maps/mod.rs line 733 at r112 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

why?

InnerMap for MapFd is used in integration tests for the lightweight set(&fd) pattern. InnerMap for MapData allows passing a raw MapData to set()/insert() without extracting the fd first. Same story as FromMapData, same as the upper comment.


aya/src/maps/mod.rs line 777 at r112 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

this feels like significant coupling at a distance. in other words, the types and constants encoded here need to match these same ones encoded in the implementation of each map. can we avoid this fragile duplication?

The map type and key size are duplicated between create() and each map's new() validation. I could move create() into each map module next to new(), or use a trait with associated constants to centralize the logic. What do you prefer?


aya/src/maps/of_maps/array.rs line 154 at r111 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

still here isn't it?

Removed.


aya-ebpf-macros/src/map.rs line 29 at r112 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

can we just take the same approach as libbpf and NOT support legacy maps-in-maps?

The maps-in-maps BTF need Kernel 5.7+ to work. Do we keep it and support for the olders versions (since 4.12) or remove so the user will require this minimum version?


ebpf/aya-ebpf/src/maps/array_of_maps.rs line 66 at r112 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

here and in the hash map - why return a mut pointer rather than a mut ref?

This follows the existing pattern, Array::get_ptr_mut, HashMap::get_ptr_mut, and PerCpuArray::get_ptr_mut all return *mut T rather than &mut T. Returning &mut from &self would be an aliasing violation since two calls with the same key would produce two &mut to the same memory. The raw pointer leaves safety responsibility to the caller via unsafe.


test/integration-ebpf/src/btf_map_of_maps.rs line 21 at r112 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

probably no longer makes sense to call this OUTER?

Renamed.


test/integration-ebpf/src/btf_map_of_maps.rs line 25 at r112 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

why 4? why not an array of 1 element containing a structure?

Done.


test/integration-ebpf/src/btf_map_of_maps.rs line 31 at r112 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

uh, i think this is is not true. it can be const

Yup, it can be, done.

Copy link
Member

@tamird tamird left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tamird reviewed 13 files and all commit messages, made 6 comments, resolved 11 discussions, and dismissed @alessandrod from a discussion.
Reviewable status: 51 of 54 files reviewed, 9 unresolved discussions (waiting on alessandrod, Brskt, and vadorovsky).


aya/src/maps/mod.rs line 684 at r112 (raw file):

Previously, Brskt wrote…

Not all map types have map_data(), only PerfEventArray and RingBuf (different field layout: Arc<T> / T vs inner: T). Unifying would mean adding map_data() to all map types. Want me to do that here or in a follow-up?

I think doing it here would make sense (i believe I suggested that via macro in an earlier comment thread)


aya/src/maps/mod.rs line 733 at r112 (raw file):

Previously, Brskt wrote…

InnerMap for MapFd is used in integration tests for the lightweight set(&fd) pattern. InnerMap for MapData allows passing a raw MapData to set()/insert() without extracting the fd first. Same story as FromMapData, same as the upper comment.

do we actually want people doing this? if not and it's just a shortcut for the tests, i think we'd better remove it


aya/src/maps/mod.rs line 777 at r112 (raw file):

Previously, Brskt wrote…

The map type and key size are duplicated between create() and each map's new() validation. I could move create() into each map module next to new(), or use a trait with associated constants to centralize the logic. What do you prefer?

centralized logic is good and a trait is better than a macro


aya/src/maps/of_maps/array.rs line 154 at r111 (raw file):

Previously, Brskt wrote…

Removed.

ArrayOfMaps::new, no need for turbofish when it's all underscores


aya-ebpf-macros/src/map.rs line 29 at r112 (raw file):

Previously, Brskt wrote…

The maps-in-maps BTF need Kernel 5.7+ to work. Do we keep it and support for the olders versions (since 4.12) or remove so the user will require this minimum version?

We don't even have CI coverage before 5.10, so I think it's OK to have the same version requirements as libbpf.


ebpf/aya-ebpf/src/maps/array_of_maps.rs line 66 at r112 (raw file):

Previously, Brskt wrote…

This follows the existing pattern, Array::get_ptr_mut, HashMap::get_ptr_mut, and PerCpuArray::get_ptr_mut all return *mut T rather than &mut T. Returning &mut from &self would be an aliasing violation since two calls with the same key would produce two &mut to the same memory. The raw pointer leaves safety responsibility to the caller via unsafe.

OK. This ofc stinks because we should just take &mut self but we can solve this separately

Brskt added 11 commits March 10, 2026 00:09
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.
@Brskt Brskt force-pushed the hashmapofmaps-new branch from 6ba0d85 to 978b651 Compare March 9, 2026 23:12
Copy link
Contributor Author

@Brskt Brskt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Brskt made 7 comments.
Reviewable status: 51 of 54 files reviewed, 9 unresolved discussions (waiting on alessandrod, tamird, and vadorovsky).


aya/src/maps/mod.rs line 684 at r112 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

I think doing it here would make sense (i believe I suggested that via macro in an earlier comment thread)

do we actually want people doing this? if not and it's just a shortcut for the tests, i think we'd better remove it


aya/src/maps/mod.rs line 733 at r112 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

do we actually want people doing this? if not and it's just a shortcut for the tests, i think we'd better remove it

To clarify my previous answer: it's not just a test shortcut, the tests demonstrate a real use case. When inner maps come from Ebpf::map(), you can't borrow the inner and outer maps at the same time. The only way around it is cloning the fd:

let inner_fd = ebpf.map("INNER").unwrap().fd().try_clone().unwrap();
let mut outer: ArrayOfMaps<&mut MapData, MapFd> = ebpf.map_mut("OUTER").unwrap().try_into().unwrap();
outer.set(0, &inner_fd, 0).unwrap();

Without InnerMap for MapFd, users have no way to insert loaded maps into a map-of-maps.


aya/src/maps/mod.rs line 777 at r112 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

centralized logic is good and a trait is better than a macro

Done. Replaced the macro body with a pub(super) trait CreatableMap in mod sealed that holds the associated constants and a provided create() method. The macro is now a thin wrapper that generates inherent create() methods delegating to the trait.


aya/src/maps/of_maps/array.rs line 154 at r111 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

ArrayOfMaps::new, no need for turbofish when it's all underscores

Done. Extracted the call into a typed let binding to avoid the turbofish.


aya-ebpf-macros/src/map.rs line 29 at r112 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

We don't even have CI coverage before 5.10, so I think it's OK to have the same version requirements as libbpf.

Done. Removed legacy map-of-maps support entirely.


aya-obj/src/maps.rs line 287 at r112 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

reminder to remove if we agree not to support legacy maps of maps

Removed.


aya-obj/src/obj.rs line 902 at r112 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

same comment here as elsewhere (and reminder to remove associated error types if we agree)

Removed.

- 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
@Brskt Brskt force-pushed the hashmapofmaps-new branch from 978b651 to 87f9634 Compare March 9, 2026 23:23
Copy link
Member

@tamird tamird left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tamird reviewed 20 files and all commit messages, made 5 comments, and resolved 4 discussions.
Reviewable status: 52 of 55 files reviewed, 7 unresolved discussions (waiting on alessandrod, Brskt, and vadorovsky).


aya/src/maps/mod.rs line 684 at r112 (raw file):

Previously, Brskt wrote…

do we actually want people doing this? if not and it's just a shortcut for the tests, i think we'd better remove it

?


aya/src/maps/of_maps/array.rs line 154 at r111 (raw file):

Previously, Brskt wrote…

Done. Extracted the call into a typed let binding to avoid the turbofish.

I think I don't understand why any of this ascription is needed, can't you just write ArrayOfMaps::new(&map)?


aya-obj/src/maps.rs line 287 at r112 (raw file):

Previously, Brskt wrote…

Removed.

not?


aya/src/maps/mod.rs line 133 at r114 (raw file):

    }

    pub(super) trait CreatableMap: Sized {

not pub, so needn't be in the sealed module. right?


aya/src/maps/mod.rs line 805 at r114 (raw file):

        impl<$($param: Pod),*> $ty<MapData, $($param),*> {
            /// Creates a standalone map with the given `max_entries` capacity and `flags`.
            pub fn create(max_entries: u32, flags: u32) -> Result<Self, MapError> {

why do we need both the trait method and the inherent method? maybe the trait should be sealed after all

Copy link
Contributor Author

@Brskt Brskt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Brskt made 5 comments.
Reviewable status: 48 of 55 files reviewed, 7 unresolved discussions (waiting on alessandrod, tamird, and vadorovsky).


aya/src/maps/mod.rs line 684 at r112 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

?

How did this sentence end up there?

Should be:
Done, added a via $accessor arm to impl_from_map_data so PerfEventArray and RingBuf use the macro instead of manual impls.


aya/src/maps/mod.rs line 133 at r114 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

not pub, so needn't be in the sealed module. right?

Right, moved CreatableMap out of sealed, it's pub(super) so it doesn't need the sealed pattern.


aya/src/maps/mod.rs line 805 at r114 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

why do we need both the trait method and the inherent method? maybe the trait should be sealed after all

Done, moved CreatableMap into sealed with a public wrapper, same pattern as FromMapData and InnerMap. No more duplicate inherent method.


aya/src/maps/of_maps/array.rs line 154 at r111 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

I think I don't understand why any of this ascription is needed, can't you just write ArrayOfMaps::new(&map)?

The annotations are required, the default V = MapData isn't used when the compiler needs to resolve trait bounds (InnerMap, FromMapData). Without them, rustc can't infer V.


aya-obj/src/maps.rs line 287 at r112 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

not?

Actually, we need them. renamed new_legacy to new_from_params since the name was misleading since it's not about legacy map definitions. It's used by CreatableMap::create() to build inner maps programmatically from raw parameters (type, key_size, value_size, max_entries). Without it, userspace has no way to create inner maps to insert into map-of-maps slots.

- 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
@Brskt Brskt force-pushed the hashmapofmaps-new branch from 574b52a to e4d474a Compare March 10, 2026 13:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants