From cb8e47880082ccfcd75b02209b686e15426e9b6a Mon Sep 17 00:00:00 2001 From: tyrone-wu Date: Tue, 30 Jul 2024 21:30:00 +0000 Subject: [PATCH] aya,integration-test: improve integration tests for info API Improves the existing integraiton tests for `loaded_programs()` and `loaded_maps()` in consideration for older kernels: - Opt for `SocketFilter` program in tests since XDP requires v4.8 and fragments requires v5.18. - For assertion tests, first perform the assertion, if the assertion fails, then it checks the host kernel version to see if it is above the minimum version requirement. If not, then continue with test, otherwise fail. For assertions that are skipped, they're logged in stderr which can be observed with `-- --nocapture`. This also fixes the `bpf_prog_get_info_by_fd()` call for kernels below v4.15. If calling syscall on kernels below v4.15, it can produce an `E2BIG` error because `check_uarg_tail_zero()` expects the entire struct to all-zero bytes (which is caused from the map info). Instead, we first attempt the syscall with the map info filled, if it returns `E2BIG`, then perform syscall again with empty closure. Also adds doc for which version a kernel feature was introduced for better awareness. The tests have been verified kernel versions: - 4.13.0 - 4.15.0 - 6.1.0 --- aya/src/maps/mod.rs | 2 + aya/src/programs/mod.rs | 6 +- aya/src/sys/bpf.rs | 25 +++- aya/src/util.rs | 7 + test/integration-ebpf/Cargo.toml | 4 + test/integration-ebpf/src/map_test.rs | 33 +++-- test/integration-ebpf/src/simple_prog.rs | 19 +++ test/integration-test/src/lib.rs | 1 + test/integration-test/src/tests.rs | 1 + test/integration-test/src/tests/info.rs | 181 +++++++++++++++++++++++ test/integration-test/src/tests/smoke.rs | 59 +------- test/integration-test/src/utils.rs | 59 ++++++++ xtask/public-api/aya.txt | 4 + 13 files changed, 325 insertions(+), 76 deletions(-) create mode 100644 test/integration-ebpf/src/simple_prog.rs create mode 100644 test/integration-test/src/tests/info.rs diff --git a/aya/src/maps/mod.rs b/aya/src/maps/mod.rs index 431064d8..00585a13 100644 --- a/aya/src/maps/mod.rs +++ b/aya/src/maps/mod.rs @@ -1039,6 +1039,8 @@ impl MapInfo { /// This differs from [`crate::Ebpf::maps`] since it will return all maps /// listed on the host system and not only maps for a specific [`crate::Ebpf`] instance. /// +/// Uses kernel v4.13 features. +/// /// # Example /// ``` /// # use aya::maps::loaded_maps; diff --git a/aya/src/programs/mod.rs b/aya/src/programs/mod.rs index 898070e5..81b94f78 100644 --- a/aya/src/programs/mod.rs +++ b/aya/src/programs/mod.rs @@ -961,7 +961,9 @@ macro_rules! impl_info { ($($struct_name:ident),+ $(,)?) => { $( impl $struct_name { - /// Returns the file descriptor of this Program. + /// Returns metadata information of this program. + /// + /// Uses kernel v4.13 features. pub fn info(&self) -> Result { let ProgramFd(fd) = self.fd()?; @@ -1142,6 +1144,8 @@ impl ProgramInfo { /// This differs from [`crate::Ebpf::programs`] since it will return all programs /// listed on the host system and not only programs a specific [`crate::Ebpf`] instance. /// +/// Uses kernel v4.13 features. +/// /// # Example /// ``` /// # use aya::programs::loaded_programs; diff --git a/aya/src/sys/bpf.rs b/aya/src/sys/bpf.rs index 01bad1a3..058de40e 100644 --- a/aya/src/sys/bpf.rs +++ b/aya/src/sys/bpf.rs @@ -8,7 +8,7 @@ use std::{ }; use assert_matches::assert_matches; -use libc::{ENOENT, ENOSPC}; +use libc::{E2BIG, ENOENT, ENOSPC}; use obj::{ btf::{BtfEnum64, Enum64}, generated::bpf_stats_type, @@ -499,6 +499,7 @@ pub(crate) fn bpf_prog_query( ret } +/// Introduced in kernel v4.13. pub(crate) fn bpf_prog_get_fd_by_id(prog_id: u32) -> Result { let mut attr = unsafe { mem::zeroed::() }; @@ -513,6 +514,7 @@ pub(crate) fn bpf_prog_get_fd_by_id(prog_id: u32) -> Result( fd: BorrowedFd<'_>, init: F, @@ -541,16 +543,31 @@ fn bpf_obj_get_info_by_fd( } } +/// Introduced in kernel v4.13. pub(crate) fn bpf_prog_get_info_by_fd( fd: BorrowedFd<'_>, map_ids: &mut [u32], ) -> Result { - bpf_obj_get_info_by_fd(fd, |info: &mut bpf_prog_info| { + // Attempt syscall with the map info filled. + let mut info = bpf_obj_get_info_by_fd(fd, |info: &mut bpf_prog_info| { info.nr_map_ids = map_ids.len() as _; info.map_ids = map_ids.as_mut_ptr() as _; - }) + }); + + // An `E2BIG` error can occur on kernels below v4.15 when handing over a large struct where the + // extra space is not all-zero bytes. + if let Err(err) = &info { + if let Some(errno) = &err.io_error.raw_os_error() { + if errno == &E2BIG { + info = bpf_obj_get_info_by_fd(fd, |_| {}); + } + } + } + + info } +/// Introduced in kernel v4.13. pub(crate) fn bpf_map_get_fd_by_id(map_id: u32) -> Result { let mut attr = unsafe { mem::zeroed::() }; @@ -1093,6 +1110,7 @@ fn iter_obj_ids( }) } +/// Introduced in kernel v4.13. pub(crate) fn iter_prog_ids() -> impl Iterator> { iter_obj_ids(bpf_cmd::BPF_PROG_GET_NEXT_ID, "bpf_prog_get_next_id") } @@ -1101,6 +1119,7 @@ pub(crate) fn iter_link_ids() -> impl Iterator> iter_obj_ids(bpf_cmd::BPF_LINK_GET_NEXT_ID, "bpf_link_get_next_id") } +/// Introduced in kernel v4.13. pub(crate) fn iter_map_ids() -> impl Iterator> { iter_obj_ids(bpf_cmd::BPF_MAP_GET_NEXT_ID, "bpf_map_get_next_id") } diff --git a/aya/src/util.rs b/aya/src/util.rs index 17371460..18de6002 100644 --- a/aya/src/util.rs +++ b/aya/src/util.rs @@ -3,6 +3,7 @@ use std::{ collections::BTreeMap, error::Error, ffi::{CStr, CString}, + fmt::Display, fs::{self, File}, io::{self, BufRead, BufReader}, mem, @@ -177,6 +178,12 @@ impl KernelVersion { } } +impl Display for KernelVersion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}.{}.{}", self.major, self.minor, self.patch) + } +} + const ONLINE_CPUS: &str = "/sys/devices/system/cpu/online"; pub(crate) const POSSIBLE_CPUS: &str = "/sys/devices/system/cpu/possible"; diff --git a/test/integration-ebpf/Cargo.toml b/test/integration-ebpf/Cargo.toml index 2c5e7b2e..a69595da 100644 --- a/test/integration-ebpf/Cargo.toml +++ b/test/integration-ebpf/Cargo.toml @@ -64,3 +64,7 @@ path = "src/ring_buf.rs" [[bin]] name = "memmove_test" path = "src/memmove_test.rs" + +[[bin]] +name = "simple_prog" +path = "src/simple_prog.rs" diff --git a/test/integration-ebpf/src/map_test.rs b/test/integration-ebpf/src/map_test.rs index 242207b8..e3fc28bc 100644 --- a/test/integration-ebpf/src/map_test.rs +++ b/test/integration-ebpf/src/map_test.rs @@ -1,29 +1,34 @@ +// Socket Filter program for testing with an arbitrary program with maps. +// This is mainly used in tests with consideration for old kernels. + #![no_std] #![no_main] use aya_ebpf::{ - bindings::xdp_action, - macros::{map, xdp}, - maps::Array, - programs::XdpContext, + macros::{map, socket_filter}, + maps::{Array, HashMap}, + programs::SkBuffContext, }; +// Introduced in kernel v3.19. #[map] static FOO: Array = Array::::with_max_entries(10, 0); +// Introduced in kernel v3.19. #[map(name = "BAR")] -static BAZ: Array = Array::::with_max_entries(10, 0); +static BAZ: HashMap = HashMap::::with_max_entries(8, 0); -#[xdp(frags)] -pub fn pass(ctx: XdpContext) -> u32 { - match unsafe { try_pass(ctx) } { - Ok(ret) => ret, - Err(_) => xdp_action::XDP_ABORTED, - } -} +// Introduced in kernel v3.19. +#[socket_filter] +pub fn simple_prog(_ctx: SkBuffContext) -> i64 { + // So that these maps show up under the `map_ids` field. + FOO.get(0); + // If we use the literal value `0` instead of the local variable `i`, then an additional + // `.rodata` map will be associated with the program. + let i = 0; + BAZ.get_ptr(&i); -unsafe fn try_pass(_ctx: XdpContext) -> Result { - Ok(xdp_action::XDP_PASS) + 0 } #[cfg(not(test))] diff --git a/test/integration-ebpf/src/simple_prog.rs b/test/integration-ebpf/src/simple_prog.rs new file mode 100644 index 00000000..98725dce --- /dev/null +++ b/test/integration-ebpf/src/simple_prog.rs @@ -0,0 +1,19 @@ +// Socket Filter program for testing with an arbitrary program. +// This is mainly used in tests with consideration for old kernels. + +#![no_std] +#![no_main] + +use aya_ebpf::{macros::socket_filter, programs::SkBuffContext}; + +// Introduced in kernel v3.19. +#[socket_filter] +pub fn simple_prog(_ctx: SkBuffContext) -> i64 { + 0 +} + +#[cfg(not(test))] +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/test/integration-test/src/lib.rs b/test/integration-test/src/lib.rs index 4b55f3a1..fc31e6c2 100644 --- a/test/integration-test/src/lib.rs +++ b/test/integration-test/src/lib.rs @@ -23,6 +23,7 @@ pub const REDIRECT: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/re pub const XDP_SEC: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/xdp_sec")); pub const RING_BUF: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/ring_buf")); pub const MEMMOVE_TEST: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/memmove_test")); +pub const SIMPLE_PROG: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/simple_prog")); #[cfg(test)] mod tests; diff --git a/test/integration-test/src/tests.rs b/test/integration-test/src/tests.rs index f37d54bb..db99e095 100644 --- a/test/integration-test/src/tests.rs +++ b/test/integration-test/src/tests.rs @@ -1,6 +1,7 @@ mod bpf_probe_read; mod btf_relocations; mod elf; +mod info; mod load; mod log; mod rbpf; diff --git a/test/integration-test/src/tests/info.rs b/test/integration-test/src/tests/info.rs new file mode 100644 index 00000000..e44d720d --- /dev/null +++ b/test/integration-test/src/tests/info.rs @@ -0,0 +1,181 @@ +//! Tests the Info API. + +use std::{fs, time::SystemTime}; + +use aya::{ + maps::{loaded_maps, MapError}, + programs::{loaded_programs, ProgramError, SocketFilter}, + util::KernelVersion, + Ebpf, +}; +use aya_obj::generated::{bpf_map_type, bpf_prog_type}; +use libc::EINVAL; + +use crate::utils::{kernel_assert, kernel_assert_eq}; + +const BPF_JIT_ENABLE: &str = "/proc/sys/net/core/bpf_jit_enable"; + +#[test] +fn list_loaded_programs() { + // Kernels below v4.15 have been observed to have `bpf_jit_enable` disabled by default. + let jit_enabled = enable_jit(); + + // Load a program. + // Since we are only testing the programs for their metadata, there is no need to "attach" them. + let mut bpf = Ebpf::load(crate::SIMPLE_PROG).unwrap(); + let prog: &mut SocketFilter = bpf.program_mut("simple_prog").unwrap().try_into().unwrap(); + prog.load().unwrap(); + + // Ensure the `loaded_programs()` api does not panic and grab the last loaded program in the + // iter, which should be our test program. + let prog = match loaded_programs().last().unwrap() { + Ok(prog) => prog, + Err(err) => { + if let ProgramError::SyscallError(err) = &err { + // Skip entire test since feature not available + if err + .io_error + .raw_os_error() + .is_some_and(|errno| errno == EINVAL) + { + eprintln!("ignoring test completely as `loaded_programs()` is not available on the host"); + return; + } + } + panic!("{err}"); + } + }; + + // Test `bpf_prog_info` fields. + kernel_assert_eq!( + bpf_prog_type::BPF_PROG_TYPE_SOCKET_FILTER as u32, + prog.program_type(), + KernelVersion::new(4, 13, 0), + ); + kernel_assert!(prog.id() > 0, KernelVersion::new(4, 13, 0)); + kernel_assert!(prog.tag() > 0, KernelVersion::new(4, 13, 0)); + if jit_enabled { + kernel_assert!(prog.size_jitted() > 0, KernelVersion::new(4, 13, 0)); + } + kernel_assert!(prog.size_translated() > 0, KernelVersion::new(4, 13, 0)); + let uptime = SystemTime::now().duration_since(prog.loaded_at()).unwrap(); + kernel_assert!(uptime.as_nanos() > 0, KernelVersion::new(4, 15, 0)); + let maps = prog.map_ids().unwrap(); + kernel_assert!(maps.is_empty(), KernelVersion::new(4, 15, 0)); + let name = prog.name_as_str().unwrap(); + kernel_assert_eq!("simple_prog", name, KernelVersion::new(4, 15, 0)); + kernel_assert!(prog.gpl_compatible(), KernelVersion::new(4, 18, 0)); + kernel_assert!( + prog.verified_instruction_count() > 0, + KernelVersion::new(5, 16, 0) + ); + + // We can't reliably test these fields since `0` can be interpreted as the actual value or + // unavailable. + prog.btf_id(); + + // Ensure rest of the fields do not panic. + prog.memory_locked().unwrap(); + prog.fd().unwrap(); +} + +#[test] +fn list_loaded_maps() { + // Load a program with maps. + let mut bpf: Ebpf = Ebpf::load(crate::MAP_TEST).unwrap(); + let prog: &mut SocketFilter = bpf.program_mut("simple_prog").unwrap().try_into().unwrap(); + prog.load().unwrap(); + + // Ensure the loaded_maps() api doesn't panic and retrieve loaded maps. + let mut maps = loaded_maps().peekable(); + if let Err(err) = maps.peek().unwrap() { + if let MapError::SyscallError(err) = &err { + if err + .io_error + .raw_os_error() + .is_some_and(|errno| errno == EINVAL) + { + eprintln!( + "ignoring test completely as `loaded_maps()` is not available on the host" + ); + return; + } + } + panic!("{err}"); + } + let mut maps: Vec<_> = maps.filter_map(|m| m.ok()).collect(); + + // There's not a good way to extract our maps of interest with load order being + // non-deterministic. Since we are trying to be more considerate of older kernels, we should + // only rely on v4.13 feats. + // Expected sort order should be: `BAR`, `aya_global` (if ran local), `FOO` + maps.sort_unstable_by_key(|m| (m.map_type(), m.id())); + + // Ensure program has the 2 maps. + if let Ok(info) = prog.info() { + let map_ids = info.map_ids().unwrap(); + kernel_assert_eq!(2, map_ids.len(), KernelVersion::new(4, 15, 0)); + + for id in map_ids.iter() { + assert!( + maps.iter().any(|m| m.id() == *id), + "expected `loaded_maps()` to have `map_ids` from program" + ); + } + } + + // Test `bpf_map_info` fields. + let hash = maps.first().unwrap(); + kernel_assert_eq!( + bpf_map_type::BPF_MAP_TYPE_HASH as u32, + hash.map_type(), + KernelVersion::new(4, 13, 0) + ); + kernel_assert!(hash.id() > 0, KernelVersion::new(4, 13, 0)); + kernel_assert_eq!(4, hash.key_size(), KernelVersion::new(4, 13, 0)); + kernel_assert_eq!(1, hash.value_size(), KernelVersion::new(4, 13, 0)); + kernel_assert_eq!(8, hash.max_entries(), KernelVersion::new(4, 13, 0)); + kernel_assert_eq!( + "BAR", + hash.name_as_str().unwrap(), + KernelVersion::new(4, 15, 0) + ); + + hash.map_flags(); + hash.fd().unwrap(); + + let array = maps.last().unwrap(); + kernel_assert_eq!( + bpf_map_type::BPF_MAP_TYPE_ARRAY as u32, + array.map_type(), + KernelVersion::new(4, 13, 0) + ); + kernel_assert!(array.id() > 0, KernelVersion::new(4, 13, 0)); + kernel_assert_eq!(4, array.key_size(), KernelVersion::new(4, 13, 0)); + kernel_assert_eq!(4, array.value_size(), KernelVersion::new(4, 13, 0)); + kernel_assert_eq!(10, array.max_entries(), KernelVersion::new(4, 13, 0)); + kernel_assert_eq!( + "FOO", + array.name_as_str().unwrap(), + KernelVersion::new(4, 15, 0) + ); + + array.map_flags(); + array.fd().unwrap(); +} + +/// Enable program to be JIT-compiled if not already enabled. +fn enable_jit() -> bool { + match fs::read_to_string(BPF_JIT_ENABLE) { + Ok(contents) => { + if contents.chars().next().is_some_and(|c| c == '0') { + let failed = fs::write(BPF_JIT_ENABLE, b"1").is_err(); + if failed { + return false; + } + } + true + } + Err(_) => false, + } +} diff --git a/test/integration-test/src/tests/smoke.rs b/test/integration-test/src/tests/smoke.rs index b5bb30a2..0d57aea9 100644 --- a/test/integration-test/src/tests/smoke.rs +++ b/test/integration-test/src/tests/smoke.rs @@ -1,6 +1,5 @@ use aya::{ - maps::loaded_maps, - programs::{loaded_programs, Extension, TracePoint, Xdp, XdpFlags}, + programs::{Extension, TracePoint, Xdp, XdpFlags}, util::KernelVersion, Ebpf, EbpfLoader, }; @@ -70,59 +69,3 @@ fn extension() { .load(pass.fd().unwrap().try_clone().unwrap(), "xdp_pass") .unwrap(); } - -#[test] -fn list_loaded_programs() { - // Load a program. - let mut bpf = Ebpf::load(crate::PASS).unwrap(); - let dispatcher: &mut Xdp = bpf.program_mut("pass").unwrap().try_into().unwrap(); - dispatcher.load().unwrap(); - dispatcher.attach("lo", XdpFlags::default()).unwrap(); - - // Ensure the loaded_programs() api doesn't panic. - let prog = loaded_programs() - .map(|p| p.unwrap()) - .find(|p| p.name_as_str().unwrap() == "pass") - .unwrap(); - - // Ensure all relevant helper functions don't panic. - prog.name(); - prog.id(); - prog.tag(); - prog.program_type(); - prog.gpl_compatible(); - prog.map_ids().unwrap(); - prog.btf_id(); - prog.size_translated(); - prog.memory_locked().unwrap(); - prog.verified_instruction_count(); - prog.loaded_at(); - prog.fd().unwrap(); - prog.run_time(); - prog.run_count(); -} - -#[test] -fn list_loaded_maps() { - // Load a program with maps. - let mut bpf = Ebpf::load(crate::MAP_TEST).unwrap(); - let dispatcher: &mut Xdp = bpf.program_mut("pass").unwrap().try_into().unwrap(); - dispatcher.load().unwrap(); - dispatcher.attach("lo", XdpFlags::default()).unwrap(); - - // Ensure the loaded_maps() api doesn't panic and retrieve a map. - let map = loaded_maps() - .map(|m| m.unwrap()) - .find(|m| m.name_as_str().unwrap() == "FOO") - .unwrap(); - - // Ensure all relevant helper functions don't panic. - map.name(); - map.id(); - map.map_type(); - map.key_size(); - map.value_size(); - map.max_entries(); - map.map_flags(); - map.fd().unwrap(); -} diff --git a/test/integration-test/src/utils.rs b/test/integration-test/src/utils.rs index 28b68d4a..27c41ad8 100644 --- a/test/integration-test/src/utils.rs +++ b/test/integration-test/src/utils.rs @@ -70,3 +70,62 @@ impl Drop for NetNsGuard { println!("Exited network namespace {}", self.name); } } + +/// Performs `assert!` macro. If the assertion fails and host kernel version +/// is above feature version, then fail test. +macro_rules! kernel_assert { + ($cond:expr, $version:expr $(,)?) => { + let pass: bool = $cond; + if !pass { + let feat_version: aya::util::KernelVersion = $version; + let current = aya::util::KernelVersion::current().unwrap(); + let cond_literal = stringify!($cond); + if current >= feat_version { + // Host kernel is expected to have the feat but does not + panic!( + r#" assertion `{cond_literal}` failed: expected host kernel v{current} to have v{feat_version} feature"#, + ); + } else { + // Continue with tests since host is not expected to have feat + eprintln!( + r#"ignoring assertion at {}:{} + assertion `{cond_literal}` failed: continuing since host kernel v{current} is not expected to have v{feat_version} feature"#, + file!(), line!(), + ); + } + } + }; +} + +pub(crate) use kernel_assert; + +/// Performs `assert_eq!` macro. If the assertion fails and host kernel version +/// is above feature version, then fail test. +macro_rules! kernel_assert_eq { + ($left:expr, $right:expr, $version:expr $(,)?) => { + if $left != $right { + let feat_version: aya::util::KernelVersion = $version; + let current = aya::util::KernelVersion::current().unwrap(); + if current >= feat_version { + // Host kernel is expected to have the feat but does not + panic!( + r#" assertion `left == right` failed: expected host kernel v{current} to have v{feat_version} feature + left: {:?} + right: {:?}"#, + $left, $right, + ); + } else { + // Continue with tests since host is not expected to have feat + eprintln!( + r#"ignoring assertion at {}:{} + assertion `left == right` failed: continuing since host kernel v{current} is not expected to have v{feat_version} feature + left: {:?} + right: {:?}"#, + file!(), line!(), $left, $right, + ); + } + } + }; +} + +pub(crate) use kernel_assert_eq; diff --git a/xtask/public-api/aya.txt b/xtask/public-api/aya.txt index b47fc579..0024ca71 100644 --- a/xtask/public-api/aya.txt +++ b/xtask/public-api/aya.txt @@ -8765,6 +8765,8 @@ impl core::cmp::PartialOrd for aya::util::KernelVersion pub fn aya::util::KernelVersion::partial_cmp(&self, other: &aya::util::KernelVersion) -> core::option::Option impl core::fmt::Debug for aya::util::KernelVersion pub fn aya::util::KernelVersion::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::fmt::Display for aya::util::KernelVersion +pub fn aya::util::KernelVersion::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result impl core::marker::Copy for aya::util::KernelVersion impl core::marker::StructuralPartialEq for aya::util::KernelVersion impl core::marker::Freeze for aya::util::KernelVersion @@ -8789,6 +8791,8 @@ impl alloc::borrow::ToOwned for aya::util::KernelVersion where T: core::clone pub type aya::util::KernelVersion::Owned = T pub fn aya::util::KernelVersion::clone_into(&self, target: &mut T) pub fn aya::util::KernelVersion::to_owned(&self) -> T +impl alloc::string::ToString for aya::util::KernelVersion where T: core::fmt::Display + core::marker::Sized +pub fn aya::util::KernelVersion::to_string(&self) -> alloc::string::String impl core::any::Any for aya::util::KernelVersion where T: 'static + core::marker::Sized pub fn aya::util::KernelVersion::type_id(&self) -> core::any::TypeId impl core::borrow::Borrow for aya::util::KernelVersion where T: core::marker::Sized