Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions aya/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
20 changes: 10 additions & 10 deletions aya/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<OwnedFd>,
}

impl MockableFd {
#[cfg(test)]
#[cfg(any(test, feature = "test-utils"))]
const fn mock_signed_fd() -> i32 {
1337
}
Expand All @@ -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()
}
Expand Down Expand Up @@ -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 _};
Expand Down
21 changes: 0 additions & 21 deletions aya/src/sys/fake.rs

This file was deleted.

57 changes: 40 additions & 17 deletions aya/src/sys/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -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<i64, (i64, io::Error)>;
/// The result type for syscall operations.
///
/// `Ok(i64)` on success, `Err((i64, io::Error))` on failure.
pub type SysResult = Result<i64, (i64, io::Error)>;

#[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>,
},
}
Expand Down Expand Up @@ -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")))]
{
Comment on lines 122 to 129
let ret = unsafe {
match call {
Expand Down Expand Up @@ -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(
Expand All @@ -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 _;

Expand All @@ -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) }
}
Expand Down
62 changes: 62 additions & 0 deletions aya/src/sys/test_utils.rs
Original file line number Diff line number Diff line change
@@ -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<SyscallFn> = 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);
}
Comment on lines +25 to +55

/// 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()
}
Loading