From 27c28a60ed8985f05dd7c051f34e36ef0bb9ef0b Mon Sep 17 00:00:00 2001 From: Tyrone Wu Date: Mon, 21 Oct 2024 01:38:09 +0000 Subject: [PATCH] aya: add feature probing for map type Add API that probes whether kernel supports a map type. Assertion for `InodeStorage` are disabled because they require CONFIG_BPF_LSM to be enabled, which is not be default in VM tests. --- aya/src/sys/bpf.rs | 2 +- aya/src/sys/feature_probe.rs | 155 ++++++++++++- .../src/tests/feature_probe.rs | 207 +++++++++++++++++- test/integration-test/src/tests/load.rs | 4 +- xtask/public-api/aya.txt | 1 + 5 files changed, 358 insertions(+), 11 deletions(-) diff --git a/aya/src/sys/bpf.rs b/aya/src/sys/bpf.rs index 26d000d8..c503d14d 100644 --- a/aya/src/sys/bpf.rs +++ b/aya/src/sys/bpf.rs @@ -677,7 +677,7 @@ pub(crate) fn bpf_load_btf( } // SAFETY: only use for bpf_cmd that return a new file descriptor on success. -unsafe fn fd_sys_bpf(cmd: bpf_cmd, attr: &mut bpf_attr) -> SysResult { +pub(super) unsafe fn fd_sys_bpf(cmd: bpf_cmd, attr: &mut bpf_attr) -> SysResult { let fd = sys_bpf(cmd, attr)?; let fd = fd.try_into().map_err(|_| { ( diff --git a/aya/src/sys/feature_probe.rs b/aya/src/sys/feature_probe.rs index 43501ebe..8b6cd1d5 100644 --- a/aya/src/sys/feature_probe.rs +++ b/aya/src/sys/feature_probe.rs @@ -1,12 +1,19 @@ //! Probes and identifies available eBPF features supported by the host kernel. -use std::mem; +use std::{mem, os::fd::AsRawFd}; -use aya_obj::generated::{bpf_attach_type, bpf_attr, bpf_insn, BPF_F_SLEEPABLE}; +use aya_obj::generated::{ + bpf_attach_type, bpf_attr, bpf_cmd, bpf_insn, BPF_F_MMAPABLE, BPF_F_NO_PREALLOC, + BPF_F_SLEEPABLE, +}; use libc::{E2BIG, EINVAL}; -use super::{bpf_prog_load, SyscallError}; -use crate::{programs::ProgramType, util::KernelVersion}; +use super::{bpf_prog_load, fd_sys_bpf, SyscallError}; +use crate::{ + maps::MapType, + programs::ProgramType, + util::{page_size, KernelVersion}, +}; const RETURN_ZERO_INSNS: &[bpf_insn] = &[ bpf_insn::new(0xb7, 0, 0, 0, 0), // mov64 r0 = 0 @@ -107,3 +114,143 @@ pub fn is_program_supported(program_type: ProgramType) -> Result println!("hash_of_maps supported :)"), +/// Ok(false) => println!("hash_of_maps not supported :("), +/// Err(err) => println!("Uh oh! Unexpected error: {:?}", err), +/// } +/// ``` +/// +/// # Errors +/// +/// Returns [`SyscallError`] if kernel probing fails with an unexpected error. +/// +/// Note that certain errors are expected and handled internally; only +/// unanticipated failures during probing will result in this error. +pub fn is_map_supported(map_type: MapType) -> Result { + if map_type == MapType::Unspecified { + return Ok(false); + } + + // SAFETY: all-zero byte-pattern valid for `bpf_attr` + let mut attr = unsafe { mem::zeroed::() }; + // SAFETY: union access + let u = unsafe { &mut attr.__bindgen_anon_1 }; + + // To pass `map_alloc_check`/`map_alloc` + let key_size = match map_type { + MapType::LpmTrie | MapType::CgroupStorage | MapType::PerCpuCgroupStorage => 16, + MapType::Queue + | MapType::Stack + | MapType::RingBuf + | MapType::BloomFilter + | MapType::UserRingBuf + | MapType::Arena => 0, + _ => 4, + }; + let value_size = match map_type { + MapType::StackTrace | MapType::LpmTrie => 8, + MapType::SkStorage + | MapType::InodeStorage + | MapType::TaskStorage + | MapType::CgrpStorage => { + // Intentionally trigger `E2BIG` from + // `bpf_local_storage_map_alloc_check()`. + u32::MAX + } + MapType::RingBuf | MapType::UserRingBuf | MapType::Arena => 0, + _ => 4, + }; + let max_entries = match map_type { + MapType::CgroupStorage + | MapType::PerCpuCgroupStorage + | MapType::SkStorage + | MapType::InodeStorage + | MapType::TaskStorage + | MapType::CgrpStorage => 0, + MapType::RingBuf | MapType::UserRingBuf => page_size() as u32, + _ => 1, + }; + + let inner_map_fd; + match map_type { + MapType::LpmTrie => u.map_flags = BPF_F_NO_PREALLOC, + MapType::SkStorage + | MapType::InodeStorage + | MapType::TaskStorage + | MapType::CgrpStorage => { + u.map_flags = BPF_F_NO_PREALLOC; + u.btf_key_type_id = 1; + u.btf_value_type_id = 1; + } + MapType::ArrayOfMaps | MapType::HashOfMaps => { + inner_map_fd = dummy_map()?; + u.inner_map_fd = inner_map_fd.as_raw_fd() as u32; + } + MapType::StructOps => u.btf_vmlinux_value_type_id = 1, + MapType::Arena => u.map_flags = BPF_F_MMAPABLE, + _ => {} + } + + u.map_type = map_type as u32; + u.key_size = key_size; + u.value_size = value_size; + u.max_entries = max_entries; + + // SAFETY: BPF_MAP_CREATE returns a new file descriptor. + let io_error = match unsafe { fd_sys_bpf(bpf_cmd::BPF_MAP_CREATE, &mut attr) } { + Ok(_) => return Ok(true), + Err((_, io_error)) => io_error, + }; + match io_error.raw_os_error() { + Some(EINVAL) => Ok(false), + Some(E2BIG) + if matches!( + map_type, + MapType::SkStorage + | MapType::InodeStorage + | MapType::TaskStorage + | MapType::CgrpStorage + ) => + { + Ok(true) + } + // `ENOTSUPP` from `bpf_struct_ops_map_alloc()` for struct_ops + Some(524) if map_type == MapType::StructOps => Ok(true), + _ => Err(SyscallError { + call: "bpf_map_create", + io_error, + }), + } +} + +/// Create a map and return its fd. +fn dummy_map() -> Result { + // SAFETY: all-zero byte-pattern valid for `bpf_attr` + let mut attr = unsafe { mem::zeroed::() }; + // SAFETY: union access + let u = unsafe { &mut attr.__bindgen_anon_1 }; + u.map_type = 1; + u.key_size = 1; + u.value_size = 1; + u.max_entries = 1; + + // SAFETY: BPF_MAP_CREATE returns a new file descriptor. + unsafe { fd_sys_bpf(bpf_cmd::BPF_MAP_CREATE, &mut attr) }.map_err(|(_, io_error)| { + SyscallError { + call: "bpf_map_create", + io_error, + } + }) +} diff --git a/test/integration-test/src/tests/feature_probe.rs b/test/integration-test/src/tests/feature_probe.rs index e5be55f1..42258fce 100644 --- a/test/integration-test/src/tests/feature_probe.rs +++ b/test/integration-test/src/tests/feature_probe.rs @@ -1,7 +1,9 @@ //! Test feature probing against kernel version. use assert_matches::assert_matches; -use aya::{programs::ProgramType, sys::feature_probe::*, util::KernelVersion}; +use aya::{maps::MapType, programs::ProgramType, sys::feature_probe::*, util::KernelVersion}; + +use super::load::{MAX_RETRIES, RETRY_DURATION}; // TODO: Enable certain CONFIG_* options when compiling the image for VM tests. #[test] @@ -193,14 +195,211 @@ fn probe_supported_programs() { } } +#[test] +fn probe_supported_maps() { + let current = KernelVersion::current().unwrap(); + + let hash = is_map_supported(MapType::Hash); + let array = is_map_supported(MapType::Array); + if current >= KernelVersion::new(3, 19, 0) { + assert_matches!(hash, Ok(true)); + assert_matches!(array, Ok(true)); + } else { + assert_matches!(hash, Ok(false)); + assert_matches!(array, Ok(false)); + } + + let prog_array = retry(3, || is_map_supported(MapType::ProgramArray)); + if current >= KernelVersion::new(4, 2, 0) { + assert_matches!(prog_array, Ok(true)); + } else { + assert_matches!(prog_array, Ok(false)); + } + + let perf_event_array = retry(3, || is_map_supported(MapType::PerfEventArray)); + if current >= KernelVersion::new(4, 3, 0) { + assert_matches!(perf_event_array, Ok(true)); + } else { + assert_matches!(perf_event_array, Ok(false)); + } + + let per_cpu_hash = retry(3, || is_map_supported(MapType::PerCpuHash)); + let per_cpu_array = retry(3, || is_map_supported(MapType::PerCpuArray)); + let stack_trace = retry(3, || is_map_supported(MapType::StackTrace)); + if current >= KernelVersion::new(4, 6, 0) { + assert_matches!(per_cpu_hash, Ok(true)); + assert_matches!(per_cpu_array, Ok(true)); + assert_matches!(stack_trace, Ok(true)); + } else { + assert_matches!(per_cpu_hash, Ok(false)); + assert_matches!(per_cpu_array, Ok(false)); + assert_matches!(stack_trace, Ok(false)); + } + + let cgroup_array = is_map_supported(MapType::CgroupArray); + if current >= KernelVersion::new(4, 8, 0) { + assert_matches!(cgroup_array, Ok(true)); + } else { + assert_matches!(cgroup_array, Ok(false)); + } + + let lru_hash = retry(3, || is_map_supported(MapType::LruHash)); + let lru_per_cpu_hash = retry(3, || is_map_supported(MapType::LruPerCpuHash)); + if current >= KernelVersion::new(4, 10, 0) { + assert_matches!(lru_hash, Ok(true)); + assert_matches!(lru_per_cpu_hash, Ok(true)); + } else { + assert_matches!(lru_hash, Ok(false)); + assert_matches!(lru_per_cpu_hash, Ok(false)); + } + + let lpm_trie = retry(3, || is_map_supported(MapType::LpmTrie)); + if current >= KernelVersion::new(4, 11, 0) { + assert_matches!(lpm_trie, Ok(true)); + } else { + assert_matches!(lpm_trie, Ok(false)); + } + + let array_of_maps = retry(3, || is_map_supported(MapType::ArrayOfMaps)); + let hash_of_maps = retry(3, || is_map_supported(MapType::HashOfMaps)); + if current >= KernelVersion::new(4, 12, 0) { + assert_matches!(array_of_maps, Ok(true)); + assert_matches!(hash_of_maps, Ok(true)); + } else { + assert_matches!(array_of_maps, Ok(false)); + assert_matches!(hash_of_maps, Ok(false)); + } + + let dev_map = retry(MAX_RETRIES, || is_map_supported(MapType::DevMap)); + let sock_map = retry(3, || is_map_supported(MapType::SockMap)); + if current >= KernelVersion::new(4, 14, 0) { + assert_matches!(dev_map, Ok(true)); + assert_matches!(sock_map, Ok(true)); + } else { + assert_matches!(dev_map, Ok(false)); + assert_matches!(sock_map, Ok(false)); + } + + let cpu_map = retry(3, || is_map_supported(MapType::CpuMap)); + if current >= KernelVersion::new(4, 15, 0) { + assert_matches!(cpu_map, Ok(true)); + } else { + assert_matches!(cpu_map, Ok(false)); + } + + let xsk_map = retry(3, || is_map_supported(MapType::XskMap)); + let sock_hash = retry(3, || is_map_supported(MapType::SockHash)); + if current >= KernelVersion::new(4, 18, 0) { + assert_matches!(xsk_map, Ok(true)); + assert_matches!(sock_hash, Ok(true)); + } else { + assert_matches!(xsk_map, Ok(false)); + assert_matches!(sock_hash, Ok(false)); + } + + let cgroup_storage = retry(3, || is_map_supported(MapType::CgroupStorage)); + let reuseport_sock_array = retry(3, || is_map_supported(MapType::ReuseportSockArray)); + if current >= KernelVersion::new(4, 19, 0) { + assert_matches!(cgroup_storage, Ok(true)); + assert_matches!(reuseport_sock_array, Ok(true)); + } else { + assert_matches!(cgroup_storage, Ok(false)); + assert_matches!(reuseport_sock_array, Ok(false)); + } + + let per_cpu_cgroup_storage = retry(3, || is_map_supported(MapType::PerCpuCgroupStorage)); + let queue = retry(3, || is_map_supported(MapType::Queue)); + let stack = retry(3, || is_map_supported(MapType::Stack)); + if current >= KernelVersion::new(4, 20, 0) { + assert_matches!(per_cpu_cgroup_storage, Ok(true)); + assert_matches!(queue, Ok(true)); + assert_matches!(stack, Ok(true)); + } else { + assert_matches!(per_cpu_cgroup_storage, Ok(false)); + assert_matches!(queue, Ok(false)); + assert_matches!(stack, Ok(false)); + } + + let sk_storage = retry(3, || is_map_supported(MapType::SkStorage)); + if current >= KernelVersion::new(5, 2, 0) { + assert_matches!(sk_storage, Ok(true)); + } else { + assert_matches!(sk_storage, Ok(false)); + } + + let devmap_hash = retry(MAX_RETRIES, || is_map_supported(MapType::DevMapHash)); + if current >= KernelVersion::new(5, 4, 0) { + assert_matches!(devmap_hash, Ok(true)); + } else { + assert_matches!(devmap_hash, Ok(false)); + } + + let struct_ops = retry(3, || is_map_supported(MapType::StructOps)); + if current >= KernelVersion::new(5, 6, 0) { + assert_matches!(struct_ops, Ok(true)); + } else { + assert_matches!(struct_ops, Ok(false)); + } + + let ring_buf = retry(MAX_RETRIES, || is_map_supported(MapType::RingBuf)); + if current >= KernelVersion::new(5, 8, 0) { + assert_matches!(ring_buf, Ok(true)); + } else { + assert_matches!(ring_buf, Ok(false)); + } + + // Requires `CONFIG_BPF_LSM=y` + // let inode_storage = retry(3, || is_map_supported(MapType::InodeStorage)); + // if current >= KernelVersion::new(5, 10, 0) { + // assert_matches!(inode_storage, Ok(true)); + // } else { + // assert_matches!(inode_storage, Ok(false)); + // } + + let task_storage = retry(3, || is_map_supported(MapType::TaskStorage)); + if current >= KernelVersion::new(5, 11, 0) { + assert_matches!(task_storage, Ok(true)); + } else { + assert_matches!(task_storage, Ok(false)); + } + + let bloom_filter = retry(3, || is_map_supported(MapType::BloomFilter)); + if current >= KernelVersion::new(5, 16, 0) { + assert_matches!(bloom_filter, Ok(true)); + } else { + assert_matches!(bloom_filter, Ok(false)); + } + + let user_ring_buf = retry(3, || is_map_supported(MapType::UserRingBuf)); + if current >= KernelVersion::new(6, 1, 0) { + assert_matches!(user_ring_buf, Ok(true)); + } else { + assert_matches!(user_ring_buf, Ok(false)); + } + + let cgrp_storage = retry(3, || is_map_supported(MapType::CgrpStorage)); + if current >= KernelVersion::new(6, 2, 0) { + assert_matches!(cgrp_storage, Ok(true)); + } else { + assert_matches!(cgrp_storage, Ok(false)); + } + + let arena = retry(3, || is_map_supported(MapType::Arena)); + if current >= KernelVersion::new(6, 9, 0) { + assert_matches!(arena, Ok(true)); + } else { + assert_matches!(arena, Ok(false)); + } +} + // Back-to-back calls can be flaky and return `EPERM`. -fn retry(max_retries: u64, try_func: impl Fn() -> Result) -> Result { +fn retry(max_retries: usize, try_func: impl Fn() -> Result) -> Result { let mut res = try_func(); - for i in 1..max_retries { + for i in 1..(max_retries as u32) { if res.is_ok() { return res; } - std::thread::sleep(std::time::Duration::from_millis(i * 10)); + std::thread::sleep(RETRY_DURATION * i); res = try_func(); } res diff --git a/test/integration-test/src/tests/load.rs b/test/integration-test/src/tests/load.rs index f9ba4660..cb185651 100644 --- a/test/integration-test/src/tests/load.rs +++ b/test/integration-test/src/tests/load.rs @@ -12,8 +12,8 @@ use aya::{ use aya_obj::programs::XdpAttachType; use test_log::test; -const MAX_RETRIES: usize = 100; -const RETRY_DURATION: Duration = Duration::from_millis(10); +pub(crate) const MAX_RETRIES: usize = 100; +pub(crate) const RETRY_DURATION: Duration = Duration::from_millis(10); #[test] fn long_name() { diff --git a/xtask/public-api/aya.txt b/xtask/public-api/aya.txt index 3d90f6c9..fcddb6b6 100644 --- a/xtask/public-api/aya.txt +++ b/xtask/public-api/aya.txt @@ -9006,6 +9006,7 @@ pub fn aya::programs::tc::SchedClassifier::fd(&self) -> core::result::Result impl core::iter::traits::iterator::Iterator> pub mod aya::sys pub mod aya::sys::feature_probe +pub fn aya::sys::feature_probe::is_map_supported(map_type: aya::maps::MapType) -> core::result::Result pub fn aya::sys::feature_probe::is_program_supported(program_type: aya::programs::ProgramType) -> core::result::Result #[non_exhaustive] pub enum aya::sys::Stats pub aya::sys::Stats::RunTime