Skip to content

Commit

Permalink
feat(aya-ebpf): BTF maps
Browse files Browse the repository at this point in the history
Before this change, Aya supported only legacy BPF map definitions, which
are instances of the `bpf_map_def` struct and end up in the `maps` ELF
section. This change introduces BTF maps, with custom structs indicating
the metadata of the map, which end up in the `.maps` section.

Legacy maps are not supported by libbpf anymore and not even by the
kernel for newer types of maps like inode/task storage.

Add support of BTF maps in aya-ebpf under the `btf-maps` feature flag.

Usage of this feature requires emitting debug info for the eBPF crate
and passing the `--btf` flag to bpf-linker.
  • Loading branch information
vadorovsky committed Dec 16, 2024
1 parent 5a43bed commit f327ceb
Show file tree
Hide file tree
Showing 42 changed files with 2,269 additions and 60 deletions.
39 changes: 21 additions & 18 deletions aya-build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pub fn build_ebpf(packages: impl IntoIterator<Item = Package>) -> Result<()> {

let arch =
env::var_os("CARGO_CFG_TARGET_ARCH").ok_or(anyhow!("CARGO_CFG_TARGET_ARCH not set"))?;
let path = env::var_os("PATH").ok_or(anyhow!("PATH not set"))?;

let target = format!("{target}-unknown-none");

Expand All @@ -58,26 +59,28 @@ pub fn build_ebpf(packages: impl IntoIterator<Item = Package>) -> Result<()> {
println!("cargo:rerun-if-changed={dir}");

let mut cmd = Command::new("cargo");
cmd.args([
"+nightly",
"build",
"--package",
&name,
"-Z",
"build-std=core",
"--bins",
"--message-format=json",
"--release",
"--target",
&target,
]);

cmd.env("CARGO_CFG_BPF_TARGET_ARCH", &arch);
cmd.current_dir(dir)
.args([
"+nightly",
"build",
"-Z",
"build-std=core",
"--bins",
"--message-format=json",
"--release",
"--target",
&target,
])
.env_clear()
.env("CARGO_CFG_BPF_TARGET_ARCH", &arch)
// FIXME: Try to find which exact environment variable triggers the
// strip of debug info.
.env("PATH", &path);

// Workaround to make sure that the correct toolchain is used.
for key in ["RUSTC", "RUSTC_WORKSPACE_WRAPPER"] {
cmd.env_remove(key);
}
// for key in ["RUSTC", "RUSTC_WORKSPACE_WRAPPER"] {
// cmd.env_remove(key);
// }

// Workaround for https://github.com/rust-lang/cargo/issues/6412 where cargo flocks itself.
let target_dir = out_dir.join(name);
Expand Down
4 changes: 4 additions & 0 deletions aya-ebpf-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ syn = { workspace = true, default-features = true, features = ["full"] }

[dev-dependencies]
aya-ebpf = { path = "../ebpf/aya-ebpf", default-features = false }

[features]
default = ["btf-maps"]
btf-maps = []
73 changes: 73 additions & 0 deletions aya-ebpf-macros/src/btf_map.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::{ItemStatic, Result};

use crate::args::name_arg;

pub(crate) struct BtfMap {
item: ItemStatic,
name: String,
}

impl BtfMap {
pub(crate) fn parse(attrs: TokenStream, item: TokenStream) -> Result<BtfMap> {
let item: ItemStatic = syn::parse2(item)?;
let mut args = syn::parse2(attrs)?;
let name = name_arg(&mut args).unwrap_or_else(|| item.ident.to_string());
Ok(BtfMap { item, name })
}

pub(crate) fn expand(&self) -> TokenStream {
let section_name = ".maps";
let name = &self.name;
let item = &self.item;
quote! {
#[link_section = #section_name]
#[export_name = #name]
#item
}
}
}

#[cfg(test)]
mod tests {
use syn::parse_quote;

use super::*;

#[test]
fn test_map_with_name() {
let map = BtfMap::parse(
parse_quote!(name = "foo"),
parse_quote!(
static BAR: HashMap<&'static str, u32> = HashMap::new();
),
)
.unwrap();
let expanded = map.expand();
let expected = quote!(
#[link_section = ".maps"]
#[export_name = "foo"]
static BAR: HashMap<&'static str, u32> = HashMap::new();
);
assert_eq!(expected.to_string(), expanded.to_string());
}

#[test]
fn test_map_no_name() {
let map = BtfMap::parse(
parse_quote!(),
parse_quote!(
static BAR: HashMap<&'static str, u32> = HashMap::new();
),
)
.unwrap();
let expanded = map.expand();
let expected = quote!(
#[link_section = ".maps"]
#[export_name = "BAR"]
static BAR: HashMap<&'static str, u32> = HashMap::new();
);
assert_eq!(expected.to_string(), expanded.to_string());
}
}
14 changes: 14 additions & 0 deletions aya-ebpf-macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
pub(crate) mod args;
#[cfg(feature = "btf-maps")]
mod btf_map;
mod btf_tracepoint;
mod cgroup_device;
mod cgroup_skb;
Expand All @@ -23,6 +25,8 @@ mod tracepoint;
mod uprobe;
mod xdp;

#[cfg(feature = "btf-maps")]
use btf_map::BtfMap;
use btf_tracepoint::BtfTracePoint;
use cgroup_device::CgroupDevice;
use cgroup_skb::CgroupSkb;
Expand Down Expand Up @@ -56,6 +60,16 @@ pub fn map(attrs: TokenStream, item: TokenStream) -> TokenStream {
}
.into()
}

#[proc_macro_attribute]
pub fn btf_map(attrs: TokenStream, item: TokenStream) -> TokenStream {
match BtfMap::parse(attrs.into(), item.into()) {
Ok(prog) => prog.expand(),
Err(err) => err.into_compile_error(),
}
.into()
}

#[proc_macro_attribute]
pub fn kprobe(attrs: TokenStream, item: TokenStream) -> TokenStream {
match KProbe::parse(KProbeKind::KProbe, attrs.into(), item.into()) {
Expand Down
63 changes: 57 additions & 6 deletions aya-obj/src/obj.rs
Original file line number Diff line number Diff line change
Expand Up @@ -773,7 +773,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) = parse_btf_map(btf, info)?;
let symbol_index =
maps.get(&map_name)
.ok_or_else(|| ParseError::SymbolNotFound {
Expand Down Expand Up @@ -1257,7 +1257,7 @@ fn parse_map_def(name: &str, data: &[u8]) -> Result<bpf_map_def, ParseError> {
}
}

fn parse_btf_map_def(btf: &Btf, info: &DataSecEntry) -> Result<(String, BtfMapDef), BtfError> {
fn parse_btf_map(btf: &Btf, info: &DataSecEntry) -> Result<(String, BtfMapDef), BtfError> {
let ty = match btf.type_by_id(info.btf_type)? {
BtfType::Var(var) => var,
other => {
Expand All @@ -1267,11 +1267,17 @@ 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();

// Safety: union
let root_type = btf.resolve_type(ty.btf_type)?;
let s = match btf.type_by_id(root_type)? {
let root_type_id = btf.resolve_type(ty.btf_type)?;
parse_btf_map_def(btf, &map_name, root_type_id)
}

fn parse_btf_map_def(
btf: &Btf,
map_name: &str,
btf_type_id: u32,
) -> Result<(String, BtfMapDef), BtfError> {
let s = match btf.type_by_id(btf_type_id)? {
BtfType::Struct(s) => s,
other => {
return Err(BtfError::UnexpectedBtfType {
Expand All @@ -1280,8 +1286,26 @@ fn parse_btf_map_def(btf: &Btf, info: &DataSecEntry) -> Result<(String, BtfMapDe
}
};

let mut map_def = BtfMapDef::default();

for m in &s.members {
// In aya-ebpf, the BTF map definition types are not used directly.
// Instead, they are wrapped in structs provided by aya-ebpf (e.g.
// `HashMap`, `Array`), which then wrap the definition type in
// `UnsafeCell`.
// To retrieve the definition type, we need to walk through all the
// wrapper types:
//
// - aya-ebpf wrappers like `HashMap`, `Array` etc. They wrap an
// `UnsafeCell` in a tuple struct with one member, hence the field
// name is `__0`.
// - `UnsafeCell`, which wraps the BTF map definition inside a `value`
// field.
match btf.string_at(m.name_offset)?.as_ref() {
"__0" => {
let unsafe_cell_id = btf.resolve_type(m.btf_type)?;
return parse_btf_map_def(btf, map_name, unsafe_cell_id);
}
"type" => {
map_def.map_type = get_map_field(btf, m.btf_type)?;
}
Expand All @@ -1301,6 +1325,32 @@ fn parse_btf_map_def(btf: &Btf, info: &DataSecEntry) -> Result<(String, BtfMapDe
map_def.key_size = get_map_field(btf, m.btf_type)?;
}
"value" => {
// There are two cases to handle:
//
// 1. We are parsing an actual BTF map type with fields like
// `type`, `value`, `key`. In this case, `value` is a
// pointer.
// 2. We are parsing an `UnsafeCell`, which wraps an actual BTF
// map type, and the `value` field of `UnsafeCell` is a
// struct which we want to parse.
match btf.type_by_id(m.btf_type)? {
// BTF map with `value` as a pointer field.
BtfType::Ptr(pty) => {
let t = pty.btf_type;
map_def.value_size = btf.type_size(t)? as u32;
map_def.btf_value_type_id = t;
}
// `UnsafeCell` wrapping a BTF map in a `value field`.
BtfType::Struct(_) => {
let map_type_id = btf.resolve_type(m.btf_type)?;
return parse_btf_map_def(btf, map_name, map_type_id);
}
_ => {
return Err(BtfError::UnexpectedBtfType {
type_id: m.btf_type,
});
}
}
if let BtfType::Ptr(pty) = btf.type_by_id(m.btf_type)? {
let t = pty.btf_type;
map_def.value_size = btf.type_size(t)? as u32;
Expand Down Expand Up @@ -1333,6 +1383,7 @@ fn parse_btf_map_def(btf: &Btf, info: &DataSecEntry) -> Result<(String, BtfMapDe
}
}
}

Ok((map_name.to_string(), map_def))
}

Expand Down
4 changes: 4 additions & 0 deletions ebpf/aya-ebpf/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ aya-ebpf-bindings = { version = "^0.1.1", path = "../aya-ebpf-bindings" }

[build-dependencies]
rustversion = { workspace = true }

[features]
default = ["btf-maps"]
btf-maps = ["aya-ebpf-macros/btf-maps"]
41 changes: 41 additions & 0 deletions ebpf/aya-ebpf/src/btf_maps/array.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use core::{cell::UnsafeCell, ptr::NonNull};

use crate::{
bindings::bpf_map_type::BPF_MAP_TYPE_ARRAY, btf_map_def, cty::c_void,
helpers::bpf_map_lookup_elem,
};

btf_map_def!(ArrayDef, BPF_MAP_TYPE_ARRAY);

#[repr(transparent)]
pub struct Array<T, const M: usize, const F: usize = 0>(UnsafeCell<ArrayDef<u32, T, M, F>>);

unsafe impl<T: Sync, const M: usize, const F: usize> Sync for Array<T, M, F> {}

impl<T, const M: usize, const F: usize> Array<T, M, F> {
pub const fn new() -> Self {
Array(UnsafeCell::new(ArrayDef::new()))
}

#[inline(always)]
pub fn get(&self, index: u32) -> Option<&T> {
// FIXME: alignment
unsafe { self.lookup(index).map(|p| p.as_ref()) }
}

#[inline(always)]
pub fn get_ptr(&self, index: u32) -> Option<*const T> {
unsafe { self.lookup(index).map(|p| p.as_ptr() as *const T) }
}

#[inline(always)]
pub fn get_ptr_mut(&self, index: u32) -> Option<*mut T> {
unsafe { self.lookup(index).map(|p| p.as_ptr()) }
}

#[inline(always)]
unsafe fn lookup(&self, index: u32) -> Option<NonNull<T>> {
let ptr = bpf_map_lookup_elem(self.0.get() as *mut _, &index as *const _ as *const c_void);
NonNull::new(ptr as *mut T)
}
}
60 changes: 60 additions & 0 deletions ebpf/aya-ebpf/src/btf_maps/bloom_filter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use core::{cell::UnsafeCell, ptr};

use aya_ebpf_bindings::helpers::{bpf_map_peek_elem, bpf_map_push_elem};

use crate::{
bindings::bpf_map_type::BPF_MAP_TYPE_BLOOM_FILTER, btf_maps::AyaBtfMapMarker, cty::c_void,
};

#[allow(dead_code)]
pub struct BloomFilterDef<T, const M: usize, const H: usize = 5, const F: usize = 0> {
r#type: *const [i32; BPF_MAP_TYPE_BLOOM_FILTER as usize],
value: *const T,
max_entries: *const [i32; M],
map_extra: *const [i32; H],
map_flags: *const [i32; F],

// Anonymize the struct.
_anon: AyaBtfMapMarker,
}

#[repr(transparent)]
pub struct BloomFilter<T, const M: usize, const H: usize = 5, const F: usize = 0>(
UnsafeCell<BloomFilterDef<T, M, H, F>>,
);

impl<T, const M: usize, const H: usize, const F: usize> BloomFilter<T, M, H, F> {
pub const fn new() -> Self {
BloomFilter(UnsafeCell::new(BloomFilterDef {
r#type: &[0i32; BPF_MAP_TYPE_BLOOM_FILTER as usize] as *const _,
value: ptr::null(),
max_entries: &[0i32; M] as *const _,
map_extra: &[0i32; H] as *const _,
map_flags: &[0i32; F] as *const _,
_anon: AyaBtfMapMarker::new(),
}))
}

#[inline]
pub fn contains(&mut self, value: &T) -> Result<(), i64> {
let ret = unsafe {
bpf_map_peek_elem(
&mut self.0.get() as *mut _ as *mut _,
value as *const _ as *mut c_void,
)
};
(ret == 0).then_some(()).ok_or(ret)
}

#[inline]
pub fn insert(&mut self, value: &T, flags: u64) -> Result<(), i64> {
let ret = unsafe {
bpf_map_push_elem(
&mut self.0.get() as *mut _ as *mut _,
value as *const _ as *const _,
flags,
)
};
(ret == 0).then_some(()).ok_or(ret)
}
}
Loading

0 comments on commit f327ceb

Please sign in to comment.