diff --git a/aya-ebpf-macros/src/map.rs b/aya-ebpf-macros/src/map.rs index a5a2f22e3..7402d9b0f 100644 --- a/aya-ebpf-macros/src/map.rs +++ b/aya-ebpf-macros/src/map.rs @@ -1,13 +1,14 @@ use std::borrow::Cow; use proc_macro2::TokenStream; -use quote::quote; +use quote::{format_ident, quote}; use syn::{ItemStatic, Result}; use crate::args::Args; pub(crate) struct Map { item: ItemStatic, name: String, + inner: Option, } impl Map { @@ -15,18 +16,48 @@ impl Map { let item: ItemStatic = syn::parse2(item)?; let mut args: Args = syn::parse2(attrs)?; let name = args.pop_name().unwrap_or_else(|| item.ident.to_string()); + let inner = args.pop_string("inner"); args.into_error()?; - Ok(Self { item, name }) + Ok(Self { item, name, inner }) } pub(crate) fn expand(&self) -> TokenStream { let section_name: Cow<'_, _> = "maps".into(); let name = &self.name; let item = &self.item; + + // Aya-specific mechanism for inner map bindings (legacy, NOT libbpf-compatible). + // + // Unlike libbpf which uses BTF relocations within the `.maps` section + // (see https://patchwork.ozlabs.org/comment/2418417/), this legacy `#[map]` macro + // uses a separate `.maps.inner` section containing null-terminated string pairs. + // + // This approach was chosen because the legacy map system doesn't require BTF. + // However, this mechanism is NOT compatible with libbpf loaders. + // + // For libbpf compatibility, use `#[btf_map]` with `aya_ebpf::btf_maps::{ArrayOfMaps, HashOfMaps}` + // which use BTF relocations that both aya and libbpf can process. + // + // Format: "outer_name\0inner_name\0" pairs, parsed by aya-obj. + let inner_binding = self.inner.as_ref().map(|inner| { + let binding_ident = format_ident!("__inner_map_binding_{}", name); + let binding_value = format!("{name}\0{inner}\0"); + let binding_len = binding_value.len(); + let binding_bytes = binding_value.as_bytes(); + quote! { + #[unsafe(link_section = ".maps.inner")] + #[used] + #[allow(non_upper_case_globals)] + static #binding_ident: [u8; #binding_len] = [#(#binding_bytes),*]; + } + }); + quote! { #[unsafe(link_section = #section_name)] #[unsafe(export_name = #name)] #item + + #inner_binding } } } @@ -72,4 +103,66 @@ mod tests { ); assert_eq!(expected.to_string(), expanded.to_string()); } + + #[test] + fn test_map_with_inner() { + let map = Map::parse( + parse_quote!(inner = "INNER_TEMPLATE"), + parse_quote!( + static OUTER: Array = Array::new(); + ), + ) + .unwrap(); + let expanded = map.expand(); + let binding_bytes: &[u8] = b"OUTER\0INNER_TEMPLATE\0"; + let expected = quote!( + #[unsafe(link_section = "maps")] + #[unsafe(export_name = "OUTER")] + static OUTER: Array = Array::new(); + + #[unsafe(link_section = ".maps.inner")] + #[used] + #[allow(non_upper_case_globals)] + static __inner_map_binding_OUTER: [u8; 21usize] = [#(#binding_bytes),*]; + ); + assert_eq!(expected.to_string(), expanded.to_string()); + } + + #[test] + fn test_map_with_name_and_inner() { + let map = Map::parse( + parse_quote!(name = "my_map", inner = "my_template"), + parse_quote!( + static OUTER: Array = Array::new(); + ), + ) + .unwrap(); + let expanded = map.expand(); + let binding_bytes: &[u8] = b"my_map\0my_template\0"; + let expected = quote!( + #[unsafe(link_section = "maps")] + #[unsafe(export_name = "my_map")] + static OUTER: Array = Array::new(); + + #[unsafe(link_section = ".maps.inner")] + #[used] + #[allow(non_upper_case_globals)] + static __inner_map_binding_my_map: [u8; 19usize] = [#(#binding_bytes),*]; + ); + assert_eq!(expected.to_string(), expanded.to_string()); + } + + #[test] + fn test_map_unknown_arg() { + let result = Map::parse( + parse_quote!(unknown = "foo"), + parse_quote!( + static BAR: HashMap<&'static str, u32> = HashMap::new(); + ), + ); + let Err(err) = result else { + panic!("expected parse error for unknown argument") + }; + assert_eq!(err.to_string(), "invalid argument"); + } } diff --git a/aya-obj/src/btf/btf.rs b/aya-obj/src/btf/btf.rs index e08de4e9e..713f5f854 100644 --- a/aya-obj/src/btf/btf.rs +++ b/aya-obj/src/btf/btf.rs @@ -160,6 +160,27 @@ pub enum BtfError { /// unable to get symbol name #[error("Unable to get symbol name")] InvalidSymbolName, + + /// Inner map definition cannot be pinned. + #[error("BTF map `{name}`: inner map definition cannot be pinned")] + InnerMapCannotBePinned { + /// The name of the map with the invalid definition. + name: String, + }, + + /// Multi-level map-in-map is not supported. + #[error("BTF map `{name}`: multi-level map-in-map is not supported")] + MultiLevelMapInMapNotSupported { + /// The name of the map with the invalid definition. + name: String, + }, + + /// The `values` spec must be a zero-sized array. + #[error("BTF map `{name}`: `values` spec is not a zero-sized array")] + InvalidValuesSpec { + /// The name of the map with the invalid definition. + name: String, + }, } /// Available BTF features diff --git a/aya-obj/src/maps.rs b/aya-obj/src/maps.rs index e9dbc4561..85c6eaab0 100644 --- a/aya-obj/src/maps.rs +++ b/aya-obj/src/maps.rs @@ -251,6 +251,63 @@ impl Map { Self::Btf(m) => Some(m.symbol_index), } } + + /// Returns the inner map definition, in case of a map of maps. + pub fn inner(&self) -> Option { + match self { + Self::Legacy(m) => m.inner_def.as_ref().map(|inner_def| { + Self::Legacy(LegacyMap { + def: *inner_def, + inner_def: None, + // The inner map is a synthetic object with no ELF presence + // of its own, so use neutral metadata values. + section_index: 0, + section_kind: EbpfSectionKind::Undefined, + symbol_index: None, + data: Vec::new(), + }) + }), + Self::Btf(m) => m.inner_def.as_ref().map(|inner_def| { + Self::Btf(BtfMap { + def: *inner_def, + inner_def: None, + // The inner map is a synthetic object with no ELF presence + // of its own, so use neutral metadata values. + section_index: 0, + symbol_index: 0, + data: Vec::new(), + }) + }), + } + } + + /// Creates a new legacy map definition programmatically. + /// + /// This is useful for creating inner maps dynamically for map-of-maps types. + pub const fn new_legacy( + map_type: u32, + key_size: u32, + value_size: u32, + max_entries: u32, + flags: u32, + ) -> Self { + Self::Legacy(LegacyMap { + def: bpf_map_def { + map_type, + key_size, + value_size, + max_entries, + map_flags: flags, + id: 0, + pinning: PinningType::None, + }, + inner_def: None, + section_index: 0, + section_kind: EbpfSectionKind::Undefined, + symbol_index: None, + data: Vec::new(), + }) + } } /// A map declared with legacy BPF map declaration style, most likely from a `maps` section. @@ -261,6 +318,8 @@ impl Map { pub struct LegacyMap { /// The definition of the map pub def: bpf_map_def, + /// The definition of the inner map, in case of a map of maps. + pub inner_def: Option, /// The section index pub section_index: usize, /// The section kind @@ -280,6 +339,8 @@ pub struct LegacyMap { pub struct BtfMap { /// The definition of the map pub def: BtfMapDef, + /// The definition of the inner map, in case of a map of maps. + pub(crate) inner_def: Option, pub(crate) section_index: usize, pub(crate) symbol_index: usize, pub(crate) data: Vec, diff --git a/aya-obj/src/obj.rs b/aya-obj/src/obj.rs index ccb37527e..a7856b642 100644 --- a/aya-obj/src/obj.rs +++ b/aya-obj/src/obj.rs @@ -25,6 +25,7 @@ use object::{ use crate::{ btf::{ Array, Btf, BtfError, BtfExt, BtfFeatures, BtfType, DataSecEntry, FuncSecInfo, LineSecInfo, + Struct, }, generated::{ BPF_CALL, BPF_F_RDONLY_PROG, BPF_JMP, BPF_K, bpf_func_id, bpf_insn, bpf_map_info, @@ -146,6 +147,8 @@ pub struct Object { pub programs: HashMap, /// Functions pub functions: BTreeMap<(usize, u64), Function>, + /// Inner map bindings: maps outer map name to inner (template) map name. + pub(crate) inner_map_bindings: HashMap, pub(crate) relocations: HashMap>, pub(crate) symbol_table: HashMap, pub(crate) symbols_by_section: HashMap>, @@ -530,6 +533,7 @@ impl Object { maps: HashMap::new(), programs: HashMap::new(), functions: BTreeMap::new(), + inner_map_bindings: HashMap::new(), relocations: HashMap::new(), symbol_table: HashMap::new(), symbols_by_section: HashMap::new(), @@ -770,7 +774,7 @@ impl Object { if type_name == section.name { // each btf_var_secinfo contains a map for info in &datasec.entries { - let (map_name, def) = parse_btf_map_def(btf, info)?; + let (map_name, def, inner_def) = parse_btf_map_def(btf, info)?; let symbol_index = maps.get(&map_name) .ok_or_else(|| ParseError::SymbolNotFound { @@ -780,6 +784,7 @@ impl Object { map_name, Map::Btf(BtfMap { def, + inner_def, section_index: section.index.0, symbol_index: *symbol_index, data: Vec::new(), @@ -825,6 +830,7 @@ impl Object { section_kind: section.kind, symbol_index: Some(sym.index), def, + inner_def: None, data: Vec::new(), }), ); @@ -886,12 +892,86 @@ impl Object { ); } } + EbpfSectionKind::MapsInner => self.parse_maps_inner(§ion)?, EbpfSectionKind::Undefined | EbpfSectionKind::License | EbpfSectionKind::Version => {} } Ok(()) } + /// Parses the `.maps.inner` section which contains outer->inner map bindings. + /// + /// This is an aya-specific mechanism for declaring inner map bindings at compile time, + /// used by the legacy `#[map(inner = "...")]` macro. This mechanism is NOT compatible + /// with libbpf loaders. + /// + /// Unlike libbpf which uses BTF relocations within the `.maps` section + /// (see ), this legacy system uses a + /// separate section containing null-terminated string pairs. + /// + /// For libbpf compatibility, use `#[btf_map]` with `aya_ebpf::btf_maps::{ArrayOfMaps, HashOfMaps}` + /// which use BTF relocations that both aya and libbpf can process. + /// + /// Format: `"outer_name\0inner_name\0"` pairs, emitted by the `#[map(inner = "...")]` macro. + fn parse_maps_inner(&mut self, section: &Section<'_>) -> Result<(), ParseError> { + let data = section.data; + let mut offset = 0; + + while offset < data.len() { + // Read outer map name (null-terminated). + let Some(outer_len) = data[offset..].iter().position(|&b| b == 0) else { + return Err(ParseError::InvalidMapsInnerSection { + offset, + msg: "unterminated outer map name", + }); + }; + + let outer_name = + core::str::from_utf8(&data[offset..offset + outer_len]).map_err(|_utf8_err| { + ParseError::InvalidMapsInnerSection { + offset, + msg: "invalid UTF-8 in outer map name", + } + })?; + offset += outer_len + 1; // skip null terminator + + // Read inner map name (null-terminated). + let Some(inner_len) = data[offset..].iter().position(|&b| b == 0) else { + return Err(ParseError::InvalidMapsInnerSection { + offset, + msg: "unterminated inner map name", + }); + }; + + let inner_name = + core::str::from_utf8(&data[offset..offset + inner_len]).map_err(|_utf8_err| { + ParseError::InvalidMapsInnerSection { + offset, + msg: "invalid UTF-8 in inner map name", + } + })?; + offset += inner_len + 1; // skip null terminator + + if outer_name.is_empty() || inner_name.is_empty() { + return Err(ParseError::InvalidMapsInnerSection { + offset, + msg: "empty outer or inner map name", + }); + } + + self.inner_map_bindings + .insert(outer_name.to_owned(), inner_name.to_owned()); + } + + Ok(()) + } + + /// Returns the inner (template) map name bound to the given outer map, if + /// any. These bindings are parsed from the `.maps.inner` ELF section. + pub fn inner_map_binding(&self, outer: &str) -> Option<&str> { + self.inner_map_bindings.get(outer).map(String::as_str) + } + /// Sanitize BPF functions. pub fn sanitize_functions(&mut self, features: &Features) { for function in self.functions.values_mut() { @@ -1009,6 +1089,15 @@ pub enum ParseError { /// No BTF parsed for object #[error("no BTF parsed for object")] NoBTF, + + /// Invalid `.maps.inner` section data. + #[error("invalid `.maps.inner` section at offset {offset}: {msg}")] + InvalidMapsInnerSection { + /// Byte offset within the section where the error occurred. + offset: usize, + /// Description of the error. + msg: &'static str, + }, } /// Invalid bindings to the bpf type from the parsed/received value. @@ -1026,6 +1115,8 @@ pub enum EbpfSectionKind { Maps, /// `.maps` BtfMaps, + /// `.maps.inner` + MapsInner, /// A program section Program, /// `.data` @@ -1054,6 +1145,10 @@ impl EbpfSectionKind { Self::Version } else if name.starts_with("maps") { Self::Maps + // NB: `.maps.inner` must be matched before `.maps` to avoid being + // swallowed by the `.starts_with(".maps")` arm below. + } else if name == ".maps.inner" { + Self::MapsInner } else if name.starts_with(".maps") { Self::BtfMaps } else if name.starts_with(".text") { @@ -1240,6 +1335,7 @@ fn parse_data_map_section(section: &Section<'_>) -> Map { // Data maps don't require symbols to be relocated symbol_index: None, def, + inner_def: None, data, }) } @@ -1263,7 +1359,10 @@ fn parse_map_def(name: &str, data: &[u8]) -> Result { } } -fn parse_btf_map_def(btf: &Btf, info: &DataSecEntry) -> Result<(String, BtfMapDef), BtfError> { +fn parse_btf_map_def( + btf: &Btf, + info: &DataSecEntry, +) -> Result<(String, BtfMapDef, Option), BtfError> { let ty = match btf.type_by_id(info.btf_type)? { BtfType::Var(var) => var, other => { @@ -1273,7 +1372,6 @@ fn parse_btf_map_def(btf: &Btf, info: &DataSecEntry) -> Result<(String, BtfMapDe } }; let map_name = btf.string_at(ty.name_offset)?; - let mut map_def = BtfMapDef::default(); let root_type = btf.resolve_type(ty.btf_type)?; let s = match btf.type_by_id(root_type)? { @@ -1285,6 +1383,23 @@ fn parse_btf_map_def(btf: &Btf, info: &DataSecEntry) -> Result<(String, BtfMapDe } }; + let (map_def, inner_def) = parse_btf_map_struct(btf, s, &map_name, false)?; + Ok((map_name.to_string(), map_def, inner_def)) +} + +/// Parses BTF struct members into a map definition. +/// +/// When `is_inner` is true, rejects `values` and `pinning` fields, matching +/// libbpf behavior for inner map definitions. +fn parse_btf_map_struct( + btf: &Btf, + s: &Struct, + map_name: &str, + is_inner: bool, +) -> Result<(BtfMapDef, Option), BtfError> { + let mut map_def = BtfMapDef::default(); + let mut inner_map_def = None; + for m in &s.members { match btf.string_at(m.name_offset)?.as_ref() { "type" => { @@ -1325,18 +1440,67 @@ fn parse_btf_map_def(btf: &Btf, info: &DataSecEntry) -> Result<(String, BtfMapDe map_def.map_flags = get_map_field(btf, m.btf_type)?; } "pinning" => { + if is_inner { + return Err(BtfError::InnerMapCannotBePinned { + name: map_name.to_owned(), + }); + } let pinning = get_map_field(btf, m.btf_type)?; map_def.pinning = PinningType::try_from(pinning).unwrap_or_else(|_| { debug!("{pinning} is not a valid pin type. using PIN_NONE"); PinningType::None }); } + "values" => { + if is_inner { + return Err(BtfError::MultiLevelMapInMapNotSupported { + name: map_name.to_owned(), + }); + } + // The inner map type is encoded as a zero-length array of pointers + // to the inner struct, matching libbpf's parse_btf_map_def(). + let arr = match btf.type_by_id(m.btf_type)? { + BtfType::Array(Array { array, .. }) => array, + other => { + return Err(BtfError::UnexpectedBtfType { + type_id: other.btf_type().unwrap_or(0), + }); + } + }; + if arr.len != 0 { + return Err(BtfError::InvalidValuesSpec { + name: map_name.to_owned(), + }); + } + let elem_type_id = btf.resolve_type(arr.element_type)?; + let ptr = match btf.type_by_id(elem_type_id)? { + BtfType::Ptr(ptr) => ptr, + other => { + return Err(BtfError::UnexpectedBtfType { + type_id: other.btf_type().unwrap_or(0), + }); + } + }; + let inner_struct_id = btf.resolve_type(ptr.btf_type)?; + let inner_s = match btf.type_by_id(inner_struct_id)? { + BtfType::Struct(s) => s, + other => { + return Err(BtfError::UnexpectedBtfType { + type_id: other.btf_type().unwrap_or(0), + }); + } + }; + let (inner_def, _) = parse_btf_map_struct(btf, inner_s, map_name, true)?; + inner_map_def = Some(inner_def); + // Map-of-maps value is always an fd (u32). + map_def.value_size = size_of::() as u32; + } other => { debug!("skipping unknown map section: {other}"); } } } - Ok((map_name.to_string(), map_def)) + Ok((map_def, inner_map_def)) } /// Parses a [`bpf_map_info`] into a [`Map`]. @@ -1353,6 +1517,7 @@ pub const fn parse_map_info(info: bpf_map_info, pinned: PinningType) -> Map { btf_key_type_id: info.btf_key_type_id, btf_value_type_id: info.btf_value_type_id, }, + inner_def: None, section_index: 0, symbol_index: 0, data: Vec::new(), @@ -1368,6 +1533,7 @@ pub const fn parse_map_info(info: bpf_map_info, pinned: PinningType) -> Map { pinning: pinned, id: info.id, }, + inner_def: None, section_index: 0, symbol_index: None, section_kind: EbpfSectionKind::Undefined, @@ -1632,6 +1798,7 @@ mod tests { pinning: PinningType::None, }, data, + .. }) if data == map_data && value_size == map_data.len() as u32 ) } @@ -2629,6 +2796,7 @@ mod tests { id: 1, pinning: PinningType::None, }, + inner_def: None, section_index: 1, section_kind: EbpfSectionKind::Rodata, symbol_index: Some(1), diff --git a/aya-obj/src/relocation.rs b/aya-obj/src/relocation.rs index 67a5d8307..f86d69d67 100644 --- a/aya-obj/src/relocation.rs +++ b/aya-obj/src/relocation.rs @@ -519,6 +519,7 @@ mod test { fn fake_legacy_map(symbol_index: usize) -> Map { Map::Legacy(LegacyMap { def: Default::default(), + inner_def: None, section_index: 0, section_kind: EbpfSectionKind::Undefined, symbol_index: Some(symbol_index), @@ -529,6 +530,7 @@ mod test { fn fake_btf_map(symbol_index: usize) -> Map { Map::Btf(BtfMap { def: Default::default(), + inner_def: None, section_index: 0, symbol_index, data: Vec::new(), diff --git a/aya/src/bpf.rs b/aya/src/bpf.rs index 1777399cc..1a410b0d6 100644 --- a/aya/src/bpf.rs +++ b/aya/src/bpf.rs @@ -1,7 +1,7 @@ use std::{ borrow::Cow, collections::{HashMap, HashSet}, - fs, io, + fs, io, iter, os::fd::{AsFd as _, AsRawFd as _}, path::{Path, PathBuf}, sync::{Arc, LazyLock}, @@ -497,38 +497,93 @@ impl<'a> EbpfLoader<'a> { if let Some(btf) = &btf { obj.relocate_btf(btf)?; } - let mut maps = HashMap::new(); - for (name, mut obj) in obj.maps.drain() { + + const fn is_map_of_maps(map_type: bpf_map_type) -> bool { + matches!( + map_type, + bpf_map_type::BPF_MAP_TYPE_ARRAY_OF_MAPS | bpf_map_type::BPF_MAP_TYPE_HASH_OF_MAPS + ) + } + + // The kernel requires inner_map_fd when creating map-of-maps, so inner + // maps must be created first. Partition into regular maps and map-of-maps. + let mut regular_maps: Vec<(String, aya_obj::Map)> = Vec::new(); + let mut maps_of_maps: Vec<(String, aya_obj::Map)> = Vec::new(); + + for (name, map_obj) in obj.maps.drain() { if let (false, EbpfSectionKind::Bss | EbpfSectionKind::Data | EbpfSectionKind::Rodata) = - (FEATURES.bpf_global_data(), obj.section_kind()) + (FEATURES.bpf_global_data(), map_obj.section_kind()) { continue; } + let map_type: bpf_map_type = map_obj.map_type().try_into().map_err(MapError::from)?; + if is_map_of_maps(map_type) { + maps_of_maps.push((name, map_obj)); + } else { + regular_maps.push((name, map_obj)); + } + } + + let mut maps: HashMap = HashMap::new(); + + // Regular maps first, so they're available as inner maps below. + for ((name, mut map_obj), is_map_of_maps) in regular_maps + .into_iter() + .zip(iter::repeat(false)) + .chain(maps_of_maps.into_iter().zip(iter::repeat(true))) + { let num_cpus = || { Ok(nr_cpus().map_err(|(path, error)| EbpfError::FileError { path: PathBuf::from(path), error, })? as u32) }; - let map_type: bpf_map_type = obj.map_type().try_into().map_err(MapError::from)?; - if let Some(max_entries) = max_entries_override( + let map_type: bpf_map_type = map_obj.map_type().try_into().map_err(MapError::from)?; + if let Some(max_entries_val) = max_entries_override( map_type, max_entries.get(name.as_str()).copied(), - || obj.max_entries(), + || map_obj.max_entries(), num_cpus, || page_size() as u32, )? { - obj.set_max_entries(max_entries) + map_obj.set_max_entries(max_entries_val) } if let Some(value_size) = value_size_override(map_type) { - obj.set_value_size(value_size) + map_obj.set_value_size(value_size) } + let btf_fd = btf_fd.as_deref().map(|fd| fd.as_fd()); + + // 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 { + if let Some(inner) = map_obj.inner() { + // Try using a BTF definition of the inner map. + btf_inner_map = MapData::create(inner, &format!("{name}.inner"), btf_fd)?; + Some(btf_inner_map.fd().as_fd()) + } 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(), + }) + })?; + Some(inner_map.fd().as_fd()) + } + } else { + None + }; let mut map = if let Some(pin_path) = map_pin_path_by_name.get(name.as_str()) { - MapData::create_pinned_by_name(pin_path, obj, &name, btf_fd)? + MapData::create_pinned_by_name(pin_path, map_obj, &name, btf_fd, inner_map_fd)? } else { - match obj.pinning() { - PinningType::None => MapData::create(obj, &name, btf_fd)?, + match map_obj.pinning() { + PinningType::None => { + MapData::create_with_inner_map_fd(map_obj, &name, btf_fd, inner_map_fd)? + } PinningType::ByName => { // pin maps in /sys/fs/bpf by default to align with libbpf // behavior https://github.com/libbpf/libbpf/blob/v1.2.2/src/libbpf.c#L2161. @@ -537,7 +592,7 @@ impl<'a> EbpfLoader<'a> { .unwrap_or_else(|| Path::new("/sys/fs/bpf")); let path = path.join(&name); - MapData::create_pinned_by_name(path, obj, &name, btf_fd)? + MapData::create_pinned_by_name(path, map_obj, &name, btf_fd, inner_map_fd)? } } }; @@ -778,6 +833,8 @@ fn parse_map( bpf_map_type::BPF_MAP_TYPE_DEVMAP_HASH => Map::DevMapHash(map), bpf_map_type::BPF_MAP_TYPE_XSKMAP => Map::XskMap(map), bpf_map_type::BPF_MAP_TYPE_SK_STORAGE => Map::SkStorage(map), + bpf_map_type::BPF_MAP_TYPE_ARRAY_OF_MAPS => Map::ArrayOfMaps(map), + bpf_map_type::BPF_MAP_TYPE_HASH_OF_MAPS => Map::HashOfMaps(map), m_type => { if allow_unsupported_maps { Map::Unsupported(map) diff --git a/aya/src/maps/mod.rs b/aya/src/maps/mod.rs index 3ded408cd..175937349 100644 --- a/aya/src/maps/mod.rs +++ b/aya/src/maps/mod.rs @@ -76,6 +76,7 @@ pub mod bloom_filter; pub mod hash_map; mod info; pub mod lpm_trie; +pub mod of_maps; pub mod perf; pub mod queue; pub mod ring_buf; @@ -90,6 +91,7 @@ pub use bloom_filter::BloomFilter; pub use hash_map::{HashMap, PerCpuHashMap}; pub use info::{MapInfo, MapType, loaded_maps}; pub use lpm_trie::LpmTrie; +pub use of_maps::{ArrayOfMaps, HashOfMaps}; pub use perf::PerfEventArray; pub use queue::Queue; pub use ring_buf::RingBuf; @@ -99,9 +101,78 @@ pub use stack::Stack; pub use stack_trace::StackTraceMap; pub use xdp::{CpuMap, DevMap, DevMapHash, XskMap}; +/// Trait for constructing a typed map from [`MapData`]. +/// +/// This is used by map-of-maps types ([`ArrayOfMaps`], [`HashOfMaps`]) to +/// let callers specify the expected inner map type when retrieving entries. +/// +/// This trait is sealed and cannot be implemented outside of this crate. +pub trait FromMapData: Sized + sealed::FromMapData { + /// Constructs a typed map from raw [`MapData`]. + fn from_map_data(map_data: MapData) -> Result; +} + +impl FromMapData for T { + fn from_map_data(map_data: MapData) -> Result { + ::from_map_data(map_data) + } +} + +/// Marker for map types that the kernel supports as inner maps. +/// +/// Types implementing this trait can be passed to +/// [`ArrayOfMaps::set`] and [`HashOfMaps::insert`]. +/// +/// This trait is sealed and cannot be implemented outside of this crate. +pub trait InnerMap: sealed::InnerMap { + /// Returns the map file descriptor. + fn fd(&self) -> &MapFd; +} + +impl InnerMap for T { + fn fd(&self) -> &MapFd { + sealed::InnerMap::inner_map_fd(self) + } +} + +mod sealed { + use super::{MapData, MapError, MapFd}; + + #[expect(unnameable_types, reason = "intentionally unnameable sealed trait")] + pub trait FromMapData: Sized { + /// Constructs a typed map from raw [`MapData`]. + fn from_map_data(map_data: MapData) -> Result; + } + + #[expect(unnameable_types, reason = "intentionally unnameable sealed trait")] + pub trait InnerMap { + /// Returns the map file descriptor. + fn inner_map_fd(&self) -> &MapFd; + } +} + #[derive(Error, Debug)] /// Errors occuring from working with Maps pub enum MapError { + /// Missing inner map binding for a map-of-maps. + #[error( + "map `{name}` is a map-of-maps but has no inner map binding; \ + use #[map(inner = \"