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