Skip to content

feat(sys): expose test-utils feature for mocking BPF syscalls#1565

Open
OliverGavin wants to merge 1 commit into
aya-rs:mainfrom
OliverGavin:feat/test-utils
Open

feat(sys): expose test-utils feature for mocking BPF syscalls#1565
OliverGavin wants to merge 1 commit into
aya-rs:mainfrom
OliverGavin:feat/test-utils

Conversation

@OliverGavin
Copy link
Copy Markdown

@OliverGavin OliverGavin commented May 13, 2026

Summary

Add a test-utils feature that exposes override_syscall and the Syscall enum publicly via sys::test_utils module. This allows downstream crates to intercept BPF syscalls in their tests without needing to reimplement aya's internal types.

Motivation

As discussed in #999, testing userspace code that interacts with BPF maps currently requires reimplementing aya's type signatures in mock crates. Aya already has an internal override_syscall mechanism used in its own tests — this PR exposes it behind an opt-in feature.

Changes

  • Added test-utils feature to aya/Cargo.toml
  • sys/fake.rs now compiles under cfg(any(test, feature = "test-utils"))
  • New sys::test_utils module (feature-gated) re-exports override_syscall, Syscall, SysResult, bpf_attr, bpf_cmd, and mock_fd()
  • Syscall, PerfEventIoctlRequest, and SysResult made pub (previously pub(crate))
  • MockableFd internals gated on any(test, feature = "test-utils")
  • Version bumped to 0.13.3

Usage

use aya::sys::test_utils::{override_syscall, Syscall, bpf_cmd, mock_fd};

override_syscall(|call| match call {
    Syscall::Ebpf { cmd: bpf_cmd::BPF_MAP_CREATE, .. } => Ok(mock_fd()),
    Syscall::Ebpf { cmd: bpf_cmd::BPF_MAP_LOOKUP_ELEM, attr } => {
        // Fill attr with test data
        Ok(0)
    }
    _ => Ok(0),
});

Closes #999
Related: #36


This change is Reviewable

@OliverGavin OliverGavin requested a review from a team as a code owner May 13, 2026 10:12
@netlify
Copy link
Copy Markdown

netlify Bot commented May 13, 2026

Deploy Preview for aya-rs-docs ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit f12521d
🔍 Latest deploy log https://app.netlify.com/projects/aya-rs-docs/deploys/6a08ac2a9dd38d0008b02bd2
😎 Deploy Preview https://deploy-preview-1565--aya-rs-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link
Copy Markdown
Member

@vadorovsky vadorovsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Overall I agree with the direction and this looks like a good first step towards making different pieces of Aya mockable.

Comment thread aya/src/sys/fake.rs Outdated
Comment thread aya/src/sys/mod.rs Outdated
Comment thread aya/Cargo.toml Outdated
Comment thread aya/Cargo.toml Outdated
@OliverGavin OliverGavin force-pushed the feat/test-utils branch 3 times, most recently from ed8c3e9 to 0dd36f2 Compare May 16, 2026 12:54
Copy link
Copy Markdown
Member

@tamird tamird left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tamird reviewed 5 files and all commit messages, and made 6 comments.
Reviewable status: all files reviewed, 10 unresolved discussions (waiting on Brskt and OliverGavin).


-- commits line 9 at r2:
nit: can just be "Closes #999."


aya/src/sys/fake.rs line 7 at r2 (raw file):

type SyscallFn = unsafe fn(Syscall<'_>) -> SysResult;

#[cfg(test)]

in other places this changed to

#[cfg(any(test, feature = "test-utils"))]

why not in this file?


aya/src/sys/mod.rs line 9 at r2 (raw file):

#[cfg(any(test, feature = "test-utils"))]
pub(crate) mod fake;

why pub(crate) here?


aya/src/sys/mod.rs line 19 at r2 (raw file):

use aya_obj::generated::{bpf_attr, bpf_cmd, bpf_stats_type, perf_event_attr};
pub(crate) use bpf::*;
#[cfg(any(test, feature = "test-utils"))]

i don't think you need the pub(crate) use fake::* under test-utils? i think it doesn't change the crate's API, so I don't see why you would


aya/src/sys/mod.rs line 50 at r2 (raw file):

#[cfg(feature = "test-utils")]
#[cfg_attr(docsrs, doc(cfg(feature = "test-utils")))]
pub mod test_utils {

could we rename fake to test_utils so that the decision to make certain testing utils externally visible lives on each item rather than having this parallel export layer?


aya/src/sys/mod.rs line 59 at r2 (raw file):

    /// syscalls that create FDs (e.g. `BPF_MAP_CREATE`).
    pub const fn mock_fd() -> i64 {
        crate::MockableFd::mock_signed_fd() as i64

why i64 rather than the underlying i32?

Add a 'test-utils' feature that exposes override_syscall and the Syscall
enum publicly via sys::test_utils module. This allows downstream crates
to intercept BPF syscalls in their tests without needing to reimplement
Aya's internal types.

Closes aya-rs#999.
@OliverGavin
Copy link
Copy Markdown
Author

Thanks for the review @tamird! I've addressed all your feedback in the latest push:

commit message: nit: can just be "Closes #999."

Done.

fake.rs line 7: in other places this changed to #[cfg(any(test, feature = "test-utils"))] — why not in this file?

The entire file is now gated by the mod declaration in mod.rs, so individual #[cfg] annotations inside it were redundant. But this is now moot because...

mod.rs line 9: why pub(crate) here?
mod.rs line 50: could we rename fake to test_utils so that the decision to make certain testing utils externally visible lives on each item rather than having this parallel export layer?

Great suggestion — done. I've renamed fake.rs to test_utils.rs and made it the public module directly. The pub visibility now lives on each item (override_syscall, mock_fd) rather than going through a re-export layer. Internal items (TEST_SYSCALL, TEST_MMAP_RET) remain pub(crate).

mod.rs line 19: I don't think you need the pub(crate) use fake::* under test-utils?

Agreed. I've split it: TEST_MMAP_RET and TEST_SYSCALL are imported under cfg(any(test, feature = "test-utils")) (needed by syscall() and mmap()), while override_syscall is only imported under cfg(test) (for internal test convenience).

mod.rs line 59: why i64 rather than the underlying i32?

Good point — changed mock_fd() to return i32 since that's what an FD actually is. Users can do Ok(i64::from(mock_fd())) in their handlers which makes the intent clearer.

Copy link
Copy Markdown
Member

@tamird tamird left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codex review

@tamird reviewed 4 files and all commit messages, made 1 comment, and resolved 6 discussions.
Reviewable status: all files reviewed, 4 unresolved discussions (waiting on Brskt and OliverGavin).

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new test-utils feature intended to let downstream crates mock/intercept Aya’s BPF-related syscalls by exposing an opt-in aya::sys::test_utils module and making some internal syscall types public.

Changes:

  • Adds a test-utils feature to the aya crate and a new sys::test_utils module that re-exports syscall mocking helpers/types.
  • Moves syscall mocking infrastructure from the old test-only sys/fake.rs into sys/test_utils.rs and wires syscall/mmap/munmap to use it under cfg(any(test, feature = "test-utils")).
  • Promotes Syscall, PerfEventIoctlRequest, and SysResult visibility to public (affecting the public API surface).

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
aya/Cargo.toml Adds the test-utils feature definition.
aya/src/sys/mod.rs Exposes sys::test_utils and routes syscalls/mmap/munmap through test hooks under test-utils.
aya/src/sys/test_utils.rs New public module providing override_syscall, mock_fd, and re-exports for mocking.
aya/src/lib.rs Extends MockableFd test-only behavior to also apply when test-utils is enabled.
aya/src/sys/fake.rs Removes the old test-only fake syscall module.
xtask/public-api/aya.txt Updates the recorded public API surface to include the newly exposed items.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread aya/src/sys/test_utils.rs
Comment on lines +25 to +55
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 thread aya/src/sys/mod.rs
Comment on lines 122 to 129
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 thread aya/Cargo.toml
Comment on lines 1 to +23
[package]
description = "An eBPF library with a focus on developer experience and operability."
documentation = "https://docs.rs/aya"
keywords = ["bpf", "ebpf", "kernel", "linux"]
name = "aya"
readme = "README.md"
version = "0.13.2"

authors.workspace = true
edition.workspace = true
homepage.workspace = true
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 = []
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support Unit Testing of Userspace Programs

4 participants