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
pull/1007/head
tyrone-wu 2 months ago
parent ab000ad7c3
commit cb8e478800
No known key found for this signature in database
GPG Key ID: 978B1A1B79210AD6

@ -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;

@ -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<ProgramInfo, ProgramError> {
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;

@ -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<crate::MockableFd, SyscallError> {
let mut attr = unsafe { mem::zeroed::<bpf_attr>() };
@ -513,6 +514,7 @@ pub(crate) fn bpf_prog_get_fd_by_id(prog_id: u32) -> Result<crate::MockableFd, S
})
}
/// Introduced in kernel v4.13.
fn bpf_obj_get_info_by_fd<T, F: FnOnce(&mut T)>(
fd: BorrowedFd<'_>,
init: F,
@ -541,16 +543,31 @@ fn bpf_obj_get_info_by_fd<T, F: FnOnce(&mut T)>(
}
}
/// Introduced in kernel v4.13.
pub(crate) fn bpf_prog_get_info_by_fd(
fd: BorrowedFd<'_>,
map_ids: &mut [u32],
) -> Result<bpf_prog_info, SyscallError> {
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<crate::MockableFd, SyscallError> {
let mut attr = unsafe { mem::zeroed::<bpf_attr>() };
@ -1093,6 +1110,7 @@ fn iter_obj_ids(
})
}
/// Introduced in kernel v4.13.
pub(crate) fn iter_prog_ids() -> impl Iterator<Item = Result<u32, SyscallError>> {
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<Item = Result<u32, SyscallError>>
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<Item = Result<u32, SyscallError>> {
iter_obj_ids(bpf_cmd::BPF_MAP_GET_NEXT_ID, "bpf_map_get_next_id")
}

@ -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";

@ -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"

@ -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<u32> = Array::<u32>::with_max_entries(10, 0);
// Introduced in kernel v3.19.
#[map(name = "BAR")]
static BAZ: Array<u32> = Array::<u32>::with_max_entries(10, 0);
static BAZ: HashMap<u32, u8> = HashMap::<u32, u8>::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<u32, u32> {
Ok(xdp_action::XDP_PASS)
0
}
#[cfg(not(test))]

@ -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 {}
}

@ -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;

@ -1,6 +1,7 @@
mod bpf_probe_read;
mod btf_relocations;
mod elf;
mod info;
mod load;
mod log;
mod rbpf;

@ -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,
}
}

@ -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();
}

@ -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;

@ -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<core::cmp::Ordering>
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<T> 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<T> 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<T> 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<T> core::borrow::Borrow<T> for aya::util::KernelVersion where T: core::marker::Sized

Loading…
Cancel
Save