diff --git a/aya/Cargo.toml b/aya/Cargo.toml index d4134686a..8dbe20aee 100644 --- a/aya/Cargo.toml +++ b/aya/Cargo.toml @@ -13,6 +13,15 @@ license.workspace = true repository.workspace = true rust-version.workspace = true +[features] +default = [] +# Expose test utilities for mocking syscalls in user tests. +# +# When enabled, the `sys::test_utils` module becomes available, providing +# `override_syscall` and the `Syscall` enum so that downstream crates can +# intercept BPF syscalls in their own `#[cfg(test)]` code. +test-utils = [] + [lints] workspace = true diff --git a/aya/src/lib.rs b/aya/src/lib.rs index 9ca777a79..d0b68b05a 100644 --- a/aya/src/lib.rs +++ b/aya/src/lib.rs @@ -63,14 +63,14 @@ pub use sys::netlink_set_link_up; // process when we try to close a fake file descriptor. #[derive(Debug)] struct MockableFd { - #[cfg(not(test))] + #[cfg(not(any(test, feature = "test-utils")))] fd: OwnedFd, - #[cfg(test)] + #[cfg(any(test, feature = "test-utils"))] fd: Option, } impl MockableFd { - #[cfg(test)] + #[cfg(any(test, feature = "test-utils"))] const fn mock_signed_fd() -> i32 { 1337 } @@ -80,35 +80,35 @@ impl MockableFd { 1337 } - #[cfg(not(test))] + #[cfg(not(any(test, feature = "test-utils")))] const fn from_fd(fd: OwnedFd) -> Self { Self { fd } } - #[cfg(test)] + #[cfg(any(test, feature = "test-utils"))] const fn from_fd(fd: OwnedFd) -> Self { let fd = Some(fd); Self { fd } } - #[cfg(not(test))] + #[cfg(not(any(test, feature = "test-utils")))] const fn inner(&self) -> &OwnedFd { let Self { fd } = self; fd } - #[cfg(test)] + #[cfg(any(test, feature = "test-utils"))] const fn inner(&self) -> &OwnedFd { let Self { fd } = self; fd.as_ref().unwrap() } - #[cfg(not(test))] + #[cfg(not(any(test, feature = "test-utils")))] fn into_inner(self) -> OwnedFd { self.fd } - #[cfg(test)] + #[cfg(any(test, feature = "test-utils"))] fn into_inner(mut self) -> OwnedFd { self.fd.take().unwrap() } @@ -149,7 +149,7 @@ impl FromRawFd for MockableFd { } } -#[cfg(test)] +#[cfg(any(test, feature = "test-utils"))] impl Drop for MockableFd { fn drop(&mut self) { use std::os::fd::{AsRawFd as _, IntoRawFd as _}; diff --git a/aya/src/sys/fake.rs b/aya/src/sys/fake.rs deleted file mode 100644 index a994cbfaa..000000000 --- a/aya/src/sys/fake.rs +++ /dev/null @@ -1,21 +0,0 @@ -use std::{cell::RefCell, ffi::c_void, io, ptr}; - -use super::{SysResult, Syscall}; - -type SyscallFn = unsafe fn(Syscall<'_>) -> SysResult; - -#[cfg(test)] -thread_local! { - pub(crate) static TEST_SYSCALL: RefCell = RefCell::new(test_syscall); - pub(crate) static TEST_MMAP_RET: RefCell<*mut c_void> = const { RefCell::new(ptr::null_mut()) }; -} - -#[cfg(test)] -unsafe fn test_syscall(_call: Syscall<'_>) -> SysResult { - Err((-1, io::Error::from_raw_os_error(libc::EINVAL))) -} - -#[cfg(test)] -pub(crate) fn override_syscall(call: unsafe fn(Syscall<'_>) -> SysResult) { - TEST_SYSCALL.with(|test_impl| *test_impl.borrow_mut() = call); -} diff --git a/aya/src/sys/mod.rs b/aya/src/sys/mod.rs index d17abd4cd..fca553281 100644 --- a/aya/src/sys/mod.rs +++ b/aya/src/sys/mod.rs @@ -5,8 +5,9 @@ pub(crate) mod feature_probe; mod netlink; mod perf_event; -#[cfg(test)] -mod fake; +#[cfg(any(test, feature = "test-utils"))] +#[cfg_attr(docsrs, doc(cfg(feature = "test-utils")))] +pub mod test_utils; use std::{ ffi::{c_int, c_void}, @@ -16,40 +17,62 @@ use std::{ use aya_obj::generated::{bpf_attr, bpf_cmd, bpf_stats_type, perf_event_attr}; pub(crate) use bpf::*; -#[cfg(test)] -pub(crate) use fake::*; pub use feature_probe::{is_map_supported, is_program_supported}; #[doc(hidden)] pub use netlink::netlink_set_link_up; pub(crate) use netlink::*; pub(crate) use perf_event::*; +#[cfg(test)] +pub(crate) use test_utils::override_syscall; +#[cfg(any(test, feature = "test-utils"))] +pub(crate) use test_utils::{TEST_MMAP_RET, TEST_SYSCALL}; use thiserror::Error; -pub(crate) type SysResult = Result; +/// The result type for syscall operations. +/// +/// `Ok(i64)` on success, `Err((i64, io::Error))` on failure. +pub type SysResult = Result; -#[cfg_attr(test, expect(dead_code, reason = "test stubs cut above this"))] +/// A perf event ioctl request. #[derive(Debug)] -pub(crate) enum PerfEventIoctlRequest<'a> { +pub enum PerfEventIoctlRequest<'a> { + /// Enable the perf event. Enable, + /// Disable the perf event. Disable, + /// Set the BPF program for the perf event. SetBpf(BorrowedFd<'a>), } -#[cfg_attr(test, expect(dead_code, reason = "test stubs cut above this"))] -pub(crate) enum Syscall<'a> { +/// Represents a system call that Aya may perform. +/// +/// Pattern-match on this in your [`test_utils::override_syscall`] handler. +pub enum Syscall<'a> { + /// A BPF syscall. Ebpf { + /// The BPF command. cmd: bpf_cmd, + /// The BPF attributes. attr: &'a mut bpf_attr, }, + /// A `perf_event_open` syscall. PerfEventOpen { + /// The perf event attributes. attr: perf_event_attr, + /// The PID. pid: libc::pid_t, + /// The CPU. cpu: i32, + /// The group. group: i32, + /// The flags. flags: u32, }, + /// A perf event ioctl syscall. PerfEventIoctl { + /// The file descriptor. fd: BorrowedFd<'a>, + /// The ioctl request. request: PerfEventIoctlRequest<'a>, }, } @@ -97,12 +120,12 @@ impl std::fmt::Debug for Syscall<'_> { } fn syscall(call: Syscall<'_>) -> SysResult { - #[cfg(test)] + #[cfg(any(test, feature = "test-utils"))] { TEST_SYSCALL.with(|test_impl| unsafe { test_impl.borrow()(call) }) } - #[cfg(not(test))] + #[cfg(not(any(test, feature = "test-utils")))] { let ret = unsafe { match call { @@ -154,7 +177,7 @@ fn syscall(call: Syscall<'_>) -> SysResult { } #[cfg_attr( - test, + any(test, feature = "test-utils"), expect(unused_variables, reason = "TODO: we should validate all arguments") )] pub(crate) unsafe fn mmap( @@ -165,12 +188,12 @@ pub(crate) unsafe fn mmap( fd: BorrowedFd<'_>, offset: libc::off_t, ) -> *mut c_void { - #[cfg(test)] + #[cfg(any(test, feature = "test-utils"))] { TEST_MMAP_RET.with(|ret| *ret.borrow()) } - #[cfg(not(test))] + #[cfg(not(any(test, feature = "test-utils")))] { use std::os::fd::AsRawFd as _; @@ -179,17 +202,17 @@ pub(crate) unsafe fn mmap( } #[cfg_attr( - test, + any(test, feature = "test-utils"), expect(clippy::missing_const_for_fn, reason = "only const in cfg(test)"), expect(unused_variables, reason = "TODO: we should validate all arguments") )] pub(crate) unsafe fn munmap(addr: *mut c_void, len: usize) -> c_int { - #[cfg(test)] + #[cfg(any(test, feature = "test-utils"))] { 0 } - #[cfg(not(test))] + #[cfg(not(any(test, feature = "test-utils")))] { unsafe { libc::munmap(addr, len) } } diff --git a/aya/src/sys/test_utils.rs b/aya/src/sys/test_utils.rs new file mode 100644 index 000000000..21adcf5d4 --- /dev/null +++ b/aya/src/sys/test_utils.rs @@ -0,0 +1,62 @@ +//! Test utilities for mocking BPF syscalls. +//! +//! This module is only available when the `test-utils` feature is enabled. +//! It provides [`override_syscall`] to intercept BPF syscalls in tests, +//! allowing userspace code to be tested without a real kernel. +//! +//! # Example +//! +//! ```ignore +//! use aya::sys::test_utils::{override_syscall, Syscall, bpf_cmd}; +//! +//! override_syscall(|call| match call { +//! Syscall::Ebpf { cmd, attr } => { +//! // Handle BPF syscalls +//! Ok(0) +//! } +//! _ => Ok(0), +//! }); +//! ``` + +use std::{cell::RefCell, ffi::c_void, io, ptr}; + +pub use aya_obj::generated::{bpf_attr, bpf_cmd}; + +pub use super::{PerfEventIoctlRequest, SysResult, Syscall}; + +type SyscallFn = unsafe fn(Syscall<'_>) -> SysResult; + +thread_local! { + pub(crate) static TEST_SYSCALL: RefCell = RefCell::new(test_syscall); + pub(crate) static TEST_MMAP_RET: RefCell<*mut c_void> = const { RefCell::new(ptr::null_mut()) }; +} + +unsafe fn test_syscall(_call: Syscall<'_>) -> SysResult { + Err((-1, io::Error::from_raw_os_error(libc::EINVAL))) +} + +/// Overrides the syscall implementation for testing purposes. +/// +/// This function replaces the BPF syscall with a user-provided function, +/// allowing tests to intercept and mock kernel interactions. +/// +/// # Example +/// +/// ```ignore +/// use aya::sys::test_utils::{override_syscall, Syscall}; +/// +/// override_syscall(|call| match call { +/// Syscall::Ebpf { cmd, attr } => Ok(0), +/// _ => Ok(0), +/// }); +/// ``` +pub fn override_syscall(call: unsafe fn(Syscall<'_>) -> SysResult) { + TEST_SYSCALL.with(|test_impl| *test_impl.borrow_mut() = call); +} + +/// Returns the fake file descriptor value used internally by Aya's test +/// infrastructure. Return this from your [`override_syscall`] handler for +/// syscalls that create FDs (e.g. `BPF_MAP_CREATE`). +pub const fn mock_fd() -> i32 { + crate::MockableFd::mock_signed_fd() +} diff --git a/xtask/public-api/aya.txt b/xtask/public-api/aya.txt index 6f5347d5a..3bab5ad3c 100644 --- a/xtask/public-api/aya.txt +++ b/xtask/public-api/aya.txt @@ -7180,6 +7180,60 @@ pub fn aya::programs::xdp::Xdp::test_run(&self, opts: Self::Opts) -> core::resul pub fn aya::programs::loaded_links() -> impl core::iter::traits::iterator::Iterator> pub fn aya::programs::loaded_programs() -> impl core::iter::traits::iterator::Iterator> pub mod aya::sys +pub mod aya::sys::test_utils +pub use aya::sys::test_utils::bpf_attr +pub use aya::sys::test_utils::bpf_cmd +pub enum aya::sys::test_utils::PerfEventIoctlRequest<'a> +pub aya::sys::test_utils::PerfEventIoctlRequest::Disable +pub aya::sys::test_utils::PerfEventIoctlRequest::Enable +pub aya::sys::test_utils::PerfEventIoctlRequest::SetBpf(std::os::fd::owned::BorrowedFd<'a>) +impl<'a> core::fmt::Debug for aya::sys::PerfEventIoctlRequest<'a> +pub fn aya::sys::PerfEventIoctlRequest<'a>::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl<'a> core::marker::Freeze for aya::sys::PerfEventIoctlRequest<'a> +impl<'a> core::marker::Send for aya::sys::PerfEventIoctlRequest<'a> +impl<'a> core::marker::Sync for aya::sys::PerfEventIoctlRequest<'a> +impl<'a> core::marker::Unpin for aya::sys::PerfEventIoctlRequest<'a> +impl<'a> core::marker::UnsafeUnpin for aya::sys::PerfEventIoctlRequest<'a> +impl<'a> core::panic::unwind_safe::RefUnwindSafe for aya::sys::PerfEventIoctlRequest<'a> +impl<'a> core::panic::unwind_safe::UnwindSafe for aya::sys::PerfEventIoctlRequest<'a> +pub enum aya::sys::test_utils::Syscall<'a> +pub aya::sys::test_utils::Syscall::Ebpf +pub aya::sys::test_utils::Syscall::Ebpf::attr: &'a mut aya_obj::generated::linux_bindings_x86_64::bpf_attr +pub aya::sys::test_utils::Syscall::Ebpf::cmd: aya_obj::generated::linux_bindings_x86_64::bpf_cmd +pub aya::sys::test_utils::Syscall::PerfEventIoctl +pub aya::sys::test_utils::Syscall::PerfEventIoctl::fd: std::os::fd::owned::BorrowedFd<'a> +pub aya::sys::test_utils::Syscall::PerfEventIoctl::request: aya::sys::PerfEventIoctlRequest<'a> +pub aya::sys::test_utils::Syscall::PerfEventOpen +pub aya::sys::test_utils::Syscall::PerfEventOpen::attr: aya_obj::generated::linux_bindings_x86_64::perf_event_attr +pub aya::sys::test_utils::Syscall::PerfEventOpen::cpu: i32 +pub aya::sys::test_utils::Syscall::PerfEventOpen::flags: u32 +pub aya::sys::test_utils::Syscall::PerfEventOpen::group: i32 +pub aya::sys::test_utils::Syscall::PerfEventOpen::pid: libc::unix::pid_t +impl core::fmt::Debug for aya::sys::Syscall<'_> +pub fn aya::sys::Syscall<'_>::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl<'a> core::marker::Freeze for aya::sys::Syscall<'a> +impl<'a> core::marker::Send for aya::sys::Syscall<'a> +impl<'a> core::marker::Sync for aya::sys::Syscall<'a> +impl<'a> core::marker::Unpin for aya::sys::Syscall<'a> +impl<'a> core::marker::UnsafeUnpin for aya::sys::Syscall<'a> +impl<'a> core::panic::unwind_safe::RefUnwindSafe for aya::sys::Syscall<'a> +impl<'a> !core::panic::unwind_safe::UnwindSafe for aya::sys::Syscall<'a> +pub const fn aya::sys::test_utils::mock_fd() -> i32 +pub fn aya::sys::test_utils::override_syscall(call: fn(aya::sys::Syscall<'_>) -> aya::sys::SysResult) +pub type aya::sys::test_utils::SysResult = core::result::Result +pub enum aya::sys::PerfEventIoctlRequest<'a> +pub aya::sys::PerfEventIoctlRequest::Disable +pub aya::sys::PerfEventIoctlRequest::Enable +pub aya::sys::PerfEventIoctlRequest::SetBpf(std::os::fd::owned::BorrowedFd<'a>) +impl<'a> core::fmt::Debug for aya::sys::PerfEventIoctlRequest<'a> +pub fn aya::sys::PerfEventIoctlRequest<'a>::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl<'a> core::marker::Freeze for aya::sys::PerfEventIoctlRequest<'a> +impl<'a> core::marker::Send for aya::sys::PerfEventIoctlRequest<'a> +impl<'a> core::marker::Sync for aya::sys::PerfEventIoctlRequest<'a> +impl<'a> core::marker::Unpin for aya::sys::PerfEventIoctlRequest<'a> +impl<'a> core::marker::UnsafeUnpin for aya::sys::PerfEventIoctlRequest<'a> +impl<'a> core::panic::unwind_safe::RefUnwindSafe for aya::sys::PerfEventIoctlRequest<'a> +impl<'a> core::panic::unwind_safe::UnwindSafe for aya::sys::PerfEventIoctlRequest<'a> #[non_exhaustive] pub enum aya::sys::Stats pub aya::sys::Stats::RunTime impl core::clone::Clone for aya::sys::Stats @@ -7196,6 +7250,28 @@ impl core::marker::Unpin for aya::sys::Stats impl core::marker::UnsafeUnpin for aya::sys::Stats impl core::panic::unwind_safe::RefUnwindSafe for aya::sys::Stats impl core::panic::unwind_safe::UnwindSafe for aya::sys::Stats +pub enum aya::sys::Syscall<'a> +pub aya::sys::Syscall::Ebpf +pub aya::sys::Syscall::Ebpf::attr: &'a mut aya_obj::generated::linux_bindings_x86_64::bpf_attr +pub aya::sys::Syscall::Ebpf::cmd: aya_obj::generated::linux_bindings_x86_64::bpf_cmd +pub aya::sys::Syscall::PerfEventIoctl +pub aya::sys::Syscall::PerfEventIoctl::fd: std::os::fd::owned::BorrowedFd<'a> +pub aya::sys::Syscall::PerfEventIoctl::request: aya::sys::PerfEventIoctlRequest<'a> +pub aya::sys::Syscall::PerfEventOpen +pub aya::sys::Syscall::PerfEventOpen::attr: aya_obj::generated::linux_bindings_x86_64::perf_event_attr +pub aya::sys::Syscall::PerfEventOpen::cpu: i32 +pub aya::sys::Syscall::PerfEventOpen::flags: u32 +pub aya::sys::Syscall::PerfEventOpen::group: i32 +pub aya::sys::Syscall::PerfEventOpen::pid: libc::unix::pid_t +impl core::fmt::Debug for aya::sys::Syscall<'_> +pub fn aya::sys::Syscall<'_>::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl<'a> core::marker::Freeze for aya::sys::Syscall<'a> +impl<'a> core::marker::Send for aya::sys::Syscall<'a> +impl<'a> core::marker::Sync for aya::sys::Syscall<'a> +impl<'a> core::marker::Unpin for aya::sys::Syscall<'a> +impl<'a> core::marker::UnsafeUnpin for aya::sys::Syscall<'a> +impl<'a> core::panic::unwind_safe::RefUnwindSafe for aya::sys::Syscall<'a> +impl<'a> !core::panic::unwind_safe::UnwindSafe for aya::sys::Syscall<'a> pub struct aya::sys::SyscallError pub aya::sys::SyscallError::call: &'static str pub aya::sys::SyscallError::io_error: std::io::error::Error @@ -7223,6 +7299,7 @@ impl !core::panic::unwind_safe::UnwindSafe for aya::sys::SyscallError pub fn aya::sys::enable_stats(stats_type: aya::sys::Stats) -> core::result::Result pub fn aya::sys::is_map_supported(map_type: aya::maps::MapType) -> core::result::Result pub fn aya::sys::is_program_supported(program_type: aya::programs::ProgramType) -> core::result::Result +pub type aya::sys::SysResult = core::result::Result pub mod aya::util pub struct aya::util::KernelVersion impl aya::util::KernelVersion