kernel-abi-tools
Test against the kernel you ship to.
Linux kernel ABI data and seccomp filters that make a modern host kernel answer like an older one. Run a Rust binary on the host under a local filter (no container, no root), or in a container of the target distribution under the equivalent profile, and:
- Syscalls the kernel lacks return
ENOSYS, as on the real machine. madviseadvice values it does not know returnEINVAL(for exampleMADV_POPULATE_READon 4.12, the kernel of SLES 12 SP5).
Part of the linux-abi-tools workspace, beside
libc-abi-tools (glibc ABI data) and linux-targets
(distribution presets), and used by cargo-libc.
The glibc side answers "will it link and load?"; this crate answers "will its
syscalls work on that kernel?". Initial scope: x86_64 Linux. Std-only
Rust; its only dependency is linux-targets. Works offline.
As a library, local::run and container::run run a program under the
simulation, Runner::choose picks between them, filter::program returns the
BPF program, and seccomp_profile returns the profile JSON, so a consumer such
as cargo-libc does not need the kernel-seccomp binary. See
WORKFLOWS.md for every workflow and how it maps onto
cargo-libc.
Status: prototype. Every profile is verified in CI (see Verification); vendor kernel backports and most flag-level differences are not modelled yet. See docs/ROADMAP.md.
Contents
| Path | Purpose |
|---|---|
data/linux/syscalls-x86_64.tsv |
Every x86_64 syscall and the first mainline release whose table lists it (v3.3 to v7.2, from 81 kernel tags) |
data/linux/madvise-x86_64.tsv |
Every madvise advice value and the first release defining it |
../linux-targets (dependency) |
28 distribution presets: kernel floor, glibc, container image |
profiles/kernel/linux-X.Y.json |
19 generic kernel profiles |
profiles/distro/ID.json |
One profile per distribution preset |
kernel-seccomp |
Generates profiles and runs programs under the local filter or a container |
syscall-probe |
Proves a profile simulates its kernel on your host and runtime |
scripts/self-test.sh |
Runs the probe against any set of profiles |
scripts/run-under-kernel.sh |
Thin wrapper over kernel-seccomp run |
scripts/install-crun.sh |
Pinned, checksum-verified static crun for hosts with an old libseccomp |
Usage
# On the host, as Linux 4.12 would answer it (local filter, no root)
# In SLES 12 SP5 userland, as its kernel would answer it (container)
Choosing a runner
--runner local |
--runner container |
|
|---|---|---|
| Userland | The host's | The image's |
| Needs | Linux x86_64 | podman or docker (crun with libseccomp 2.6+ for every profile) |
| Root | No | No with rootless podman |
| Default when | No image is known (--kernel alone) |
--image is given or --distro supplies one |
The local runner sets no_new_privs and installs a classic-BPF seccomp
filter built from the same data as the profiles, then executes the program.
The filter is inherited by every child and cannot be removed; setuid programs
inside it cannot gain privileges. It does not change the userland, so use the
container to run against the target's real glibc.
The container runner mounts the working directory, and the program file if it is outside it,
at their own absolute paths, and forwards CARGO*/RUST* variables. A bare
program name (sh) runs from the image. Engine and runtime options come from
CONTAINER_ENGINE (default podman), KERNEL_ABI_ENGINE_ARGS (e.g.
--runtime crun) and KERNEL_ABI_RUN_ARGS (e.g. --network none).
As a Cargo runner
Cargo runs every cargo run / cargo test binary through the runner, so the
whole test suite executes in the simulated environment. Build scripts and
procedural macros are not affected:
# .cargo/config.toml
[]
# Kernel only, host userland:
= ["kernel-seccomp", "run", "--kernel", "4.12", "--"]
# Or the real SLES 12 SP5 userland:
# runner = ["kernel-seccomp", "run", "--distro", "sles-12-sp5", "--"]
For the container, the binary must be built for a glibc no newer than the image's (use cargo-libc), and test data must live under the package directory, which is the working directory Cargo uses for tests.
How a profile works
A profile is an OCI seccomp profile (Podman, Docker, CRI-O). The local
filter (filter::program) makes the same decisions in BPF, and its tests
check it against the data for every syscall number and advice value of every
generic kernel and preset:
- Default action
ENOSYS; allowlist of the syscalls the target kernel has. It is an allowlist because runtimes resolve names with their own libseccomp and silently drop names it does not know. A blocklist would fail open for the newest syscalls, which is exactly what we observed during development (13 syscalls from 6.8 onward reached the kernel). With the allowlist an unknown name fails closed and the probe reports it. - One
madviserule per advice value (allowed orEINVAL), plus one for values above the highest known. No two rules overlap, so the result never depends on how a runtime orders conflicting rules.
Profiles are a testing aid, not a security boundary.
Runtime requirements
| Runtime | Works for | Why |
|---|---|---|
crun with libseccomp ≥ 2.6 (e.g. the static build from scripts/install-crun.sh) |
All profiles | Applies the filter immediately before execve, and names every syscall |
| crun or runc with libseccomp 2.5.x | Kernels 4.11 to 6.7 | libseccomp 2.5 cannot name 6.8+ syscalls; runc's init also calls statx (4.11) after installing the filter, so runc cannot start older profiles |
Select a runtime with KERNEL_ABI_ENGINE_ARGS="--runtime crun" or
--runtime /path/to/crun.
Verification
- CI on every push: every generic and preset profile tested with the
local filter on the host, then the generic kernels and three presets in
SLES 12 SP5 userland under static crun 1.28, both with a static musl probe
on GitHub's Ubuntu 24.04 runner. Each profile runs 68 syscall checks and 107
madvisechecks. - Local, 2026-10-10: host kernel 6.18 (WSL2). Container: Podman 5.8,
crun 1.28, all profiles pass. Local filter, as an unprivileged user: all 47
profiles pass. The only skip is
map_shadow_stack, which that host kernel also lacks. - End to end: a memmap2 reader built with a glibc 2.17 floor runs correctly
in SLES 12 SP5 userland under the 4.12 profile. The same program built
normally on Ubuntu 24.04 is rejected by the SLES loader (
GLIBC_2.28not found).
Releases
Published to crates.io only; data is compiled in, so nothing is downloaded at
run time. See RELEASING.md, which also covers
disconnected networks. The profiles/ directory is generated for reference
and is not part of the crate (kernel-seccomp generate DIR recreates it).
What this does not cover
- Other flags and behavior: only
madviseadvice is modelled.mmapsilently ignores unknown flags, which seccomp cannot reproduce, and flags onmemfd_create,openat,statxandcloneare on the roadmap. - Vendor backports: presets use the mainline base, so a profile can be stricter than the real vendor kernel, never looser.
- Kernels before 3.3: the data floor is v3.3. Rust std's documented minimum, 3.2, is treated as 3.3.
- Final qualification: run on a real VM of the target release before you ship.
Refreshing the data (online, occasional)
&&