Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Layer 0 Spec — ebpf Family

Technical specification — Version 1.0

Family overview

The air-sys-syscall::ebpf module exposes the programming primitives of the Linux kernel’s eBPF subsystem: the bpf() syscall (creating and manipulating maps, loading and attaching programs, introspection, pinning, BTF, iterators, statistics) and the perf_event_open() syscall (performance counters, sampling, trace points), which is the canonical attachment point for tracing-type eBPF programs.

eBPF is, along with seccomp-BPF (security family) and Landlock, one of the deliberately embraced technical pillars of Air’s Linux-only choice (ADR-004). It serves observability (profiling, tracing), security (network/LSM filtering), and fine-grained system instrumentation — capabilities leveraged by the layer 5 system services.

Layer 0 ↔ layer 1 boundary: we load, we do not fabricate

Architecture decision (aligned with the security family, Q4). As with seccomp, layer 0 does not assemble and does not compile an eBPF program. Producing eBPF bytecode (encoding struct bpf_insn, register allocation, CO-RE relocations, BTF generation, helper resolution) is generative logic, not a syscall wrapper. Layer 0 exposes the primitive: loading an already assembled program into the kernel (a slice of BpfInstruction provided by the caller), creating/querying maps, attaching, pinning, introspecting.

Therefore outside layer 0 (specified in layer 1, future air-bpf crate) are: the instruction assembler, the libbpf-style loader (ELF parsing, sections, relocations), the generation and rich manipulation of BTF, the decoding of the perf/ring-buffer sampling records. Layer 0 provides the building blocks on which this logic relies.

Family scope

Exhaustive coverage of the bpf() syscall: the 37 sub-commands of enum bpf_cmd (kernel target 6.12 LTS, like io_uring) are each exposed by a dedicated, typed function, in line with ADR-021 convention 3 — no generic bpf(cmd, attr, size) wrapper is offered. Plus the perf_event_open() syscall and its control ioctls, also as dedicated functions.

Cross-cutting characteristics of the family.

  1. Universal CLOEXEC. All eBPF FDs (maps, programs, links, BTF) and the perf_event FDs are created CLOEXEC (the BPF_F_*CLOEXEC / PERF_FLAG_FD_CLOEXEC flag is set by the wrapper).

  2. No generic multiplexed syscall (ADR-021 conv. 3). bpf() multiplexes 37 operations, ioctl on perf multiplexes a dozen: each becomes a typed function. The complexity is concentrated on the Air side, not the caller side.

  3. Kernel sentinels → Option<T> / enums (ADR-021 conv. 1). The -1 “all CPUs / all processes” of perf_event_open, the id 0 “start of iteration”, the optional fds become Option<T> or enums.

  4. RAII for all resources. BpfMap, BpfProgram, BpfLink, Btf, PerfEvent own an OwnedFd and close it on Drop. No FD leaks.

  5. EINTR surfaced as-is (ADR-021 conv. 2). No automatic retry.

  6. Privileges. Most operations require CAP_BPF (+ CAP_PERFMON for tracing, CAP_NET_ADMIN for networking) since Linux 5.8, or CAP_SYS_ADMIN before that. Unprivileged BPF is often disabled (kernel.unprivileged_bpf_disabled). Documented per function; not hidden.

  7. Typed slices rather than raw pointers. Map keys/values, program instructions, verifier log buffers are passed as &[u8] / &[BpfInstruction] whose length is validated against the geometry of the resource (key_size, value_size…) before the call (Principle 4, “validate upstream”). Mismatch ⇒ EINVAL on the Air side, without touching the kernel.


Subsection 1: Fundamental types

These types are pure (no syscalls) and live in air-sys-types::ebpf, except the RAII ones that call syscalls (placed in air-sys-syscall::ebpf).

Instruction and program

#![allow(unused)]
fn main() {
/// `#[repr(C)]` mirror of `struct bpf_insn` (8 bytes). "Mirror" type:
/// explicit type name (ADR-029), fields keep their kernel names.
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BpfInstruction {
    pub code: u8,
    /// 4 bits `dst_reg` + 4 bits `src_reg` (kernel layout).
    pub registers: u8,
    pub offset: i16,
    pub immediate: i32,
}
}

The program is a slice &[BpfInstruction] owned by the caller. Layer 0 neither inspects nor transforms it: the kernel verifier handles it at load time. The BPF_MAXINSNS (4096) limit only concerns classic BPF (seccomp); an eBPF program is bounded by the complexity that the verifier accepts (up to ~1M instructions analyzed, privileged).

Typed enumerations

#![allow(unused)]
fn main() {
/// `enum bpf_map_type`. `Other(u32)` variant for types not yet named.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BpfMapType {
    Hash, Array, ProgramArray, PerfEventArray, PercpuHash, PercpuArray,
    StackTrace, CgroupArray, LruHash, LruPercpuHash, LpmTrie, ArrayOfMaps,
    HashOfMaps, DevMap, SockMap, Cpumap, XskMap, SockHash, CgroupStorage,
    ReuseportSockArray, PercpuCgroupStorage, Queue, Stack, SkStorage, DevmapHash, StructOps,
    RingBuf, InodeStorage, TaskStorage, BloomFilter, UserRingBuf, CgrpStorage,
    Arena,
    Other(u32),
}

/// `enum bpf_prog_type`. Fallback `Other(u32)` variant.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BpfProgramType {
    SocketFilter, Kprobe, SchedCls, SchedAct, Tracepoint, Xdp, PerfEvent,
    CgroupSkb, CgroupSock, LwtIn, LwtOut, LwtXmit, SockOps, SkSkb, CgroupDevice,
    SkMsg, RawTracepoint, CgroupSockAddr, LwtSeg6local, LircMode2, SkReuseport,
    FlowDissector, CgroupSysctl, RawTracepointWritable, CgroupSockopt, Tracing,
    StructOps, Ext, Lsm, SkLookup, Syscall, Netfilter,
    Other(u32),
}

/// `enum bpf_attach_type` (attachment points for programs and links).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BpfAttachType {
    CgroupInetIngress, CgroupInetEgress, CgroupInetSockCreate, CgroupSockOps,
    SkSkbStreamParser, SkSkbStreamVerdict, CgroupDevice, SkMsgVerdict,
    CgroupInet4Bind, CgroupInet6Bind, CgroupInet4Connect, CgroupInet6Connect,
    TraceFentry, TraceFexit, ModifyReturn, LsmMac, TraceIter, XdpDevmap,
    XdpCpumap, SkLookup, Xdp, /* … */ Other(u32),
}
}

Naming (ADR-029). Type names are explicit (BpfMapType, not BpfMt). The variants mirror the kernel constants BPF_MAP_TYPE_* / BPF_PROG_TYPE_* (kernel authority, which the developer finds again in uapi/linux/bpf.h), in CamelCase, with universally accepted abbreviations tolerated (Lru, Xdp, Lsm). The Other(u32) variant avoids blocking a type added by a kernel more recent than 6.12.

RAII handles (in air-sys-syscall::ebpf)

#![allow(unused)]
fn main() {
pub struct BpfMap { /* OwnedFd */ }
pub struct BpfProgram { /* OwnedFd */ }
pub struct BpfLink { /* OwnedFd */ }
pub struct Btf { /* OwnedFd, loaded BTF object */ }
pub struct PerfEvent { /* OwnedFd */ }

// Each exposes: as_fd(&self) -> BorrowedFd<'_>, into_fd(self) -> OwnedFd,
// and a from_fd(OwnedFd) constructor to re-adopt an fd obtained from *_get_fd_by_id.
}

Subsection 2: Maps

Creation

#![allow(unused)]
fn main() {
pub fn bpf_map_create(request: &BpfMapCreateRequest<'_>) -> Result<BpfMap, Errno>;

pub struct BpfMapCreateRequest<'a> {
    pub map_type: BpfMapType,
    pub key_size: u32,
    pub value_size: u32,
    pub max_entries: u32,
    pub flags: BpfMapCreateFlags,
    /// Name (≤ 15 useful bytes + NUL), for introspection. `None` = anonymous.
    pub name: Option<&'a CStr>,
    /// For map-of-maps: the fd of the inner map template.
    pub inner_map: Option<BorrowedFd<'a>>,
    /// Preferred NUMA node (`None` = no preference).
    pub numa_node: Option<u32>,
    /// Optional BTF info (typed key/value).
    pub btf: Option<BpfMapBtfInfo<'a>>,
}

bitflags! {
    pub struct BpfMapCreateFlags: u32 {
        const NO_PREALLOC      = 1 << 0;
        const NO_COMMON_LRU    = 1 << 1;
        const NUMA_NODE        = 1 << 2;
        const READ_ONLY        = 1 << 3;
        const WRITE_ONLY       = 1 << 4;
        const STACK_BUILD_ID   = 1 << 5;
        const ZERO_SEED        = 1 << 6;
        const READ_ONLY_PROG   = 1 << 7;
        const WRITE_ONLY_PROG  = 1 << 8;
        const CLONE            = 1 << 9;
        const MMAPABLE         = 1 << 10;
        const PRESERVE_ELEMS   = 1 << 11;
        const INNER_LOCK       = 1 << 12;
    }
}
}

Underlying syscall. bpf(BPF_MAP_CREATE, &attr, size). bpf: x86_64 #321, ARM64 #280. Available since Linux 3.18; types and flags depending on version (6.12 for the frozen scope).

Preconditions (validated upstream, Principle 4). key_size/value_size consistent with the map_type (some types impose a fixed key/value size; e.g. Arraykey_size == 4). max_entries > 0 except for entry-less types. READ_ONLY/WRITE_ONLY mutually exclusive.

Errors. EPERM/EACCES (privilege, unprivileged BPF disabled), EINVAL (invalid geometry), E2BIG (map too large), ENOMEM.

Element-by-element access

#![allow(unused)]
fn main() {
pub fn bpf_map_lookup_element(
    map: BorrowedFd<'_>, key: &[u8], value_out: &mut [u8], flags: BpfMapLookupFlags,
) -> Result<(), Errno>;

pub fn bpf_map_update_element(
    map: BorrowedFd<'_>, key: &[u8], value: &[u8], flags: BpfMapUpdateFlags,
) -> Result<(), Errno>;

pub fn bpf_map_delete_element(map: BorrowedFd<'_>, key: &[u8]) -> Result<(), Errno>;

/// `None` for `key` = "first key" (kernel `NULL` sentinel, ADR-021 c.1).
pub fn bpf_map_get_next_key(
    map: BorrowedFd<'_>, key: Option<&[u8]>, next_key_out: &mut [u8],
) -> Result<bool, Errno>; // false if end of iteration (ENOENT)

/// `BPF_MAP_LOOKUP_AND_DELETE_ELEM` (Queue/Stack maps in particular).
pub fn bpf_map_lookup_and_delete_element(
    map: BorrowedFd<'_>, key: Option<&[u8]>, value_out: &mut [u8], flags: BpfMapLookupFlags,
) -> Result<(), Errno>;

bitflags! { pub struct BpfMapUpdateFlags: u64 {
    const ANY = 0; const NO_EXIST = 1; const EXIST = 2; const F_LOCK = 4;
}}
bitflags! { pub struct BpfMapLookupFlags: u64 { const F_LOCK = 4; }}
}

Length validation. key.len() must equal key_size, value.len()/value_out.len() must equal value_size (or value_size * num_possible_cpus for per-CPU maps). The wrapper checks via a cached BPF_OBJ_GET_INFO_BY_FD, or requires the caller to provide correct slices and returns EINVAL if the kernel rejects them. Decision taken: do not introspect implicitly (hidden cost) — the caller guarantees the size; mismatch ⇒ EINVAL surfaced from the kernel. Documented.

Batch operations

#![allow(unused)]
fn main() {
pub fn bpf_map_lookup_batch(map: BorrowedFd<'_>, request: &mut BpfMapBatchRequest<'_>)
    -> Result<u32, Errno>; // number of elements processed
pub fn bpf_map_lookup_and_delete_batch(map: BorrowedFd<'_>, request: &mut BpfMapBatchRequest<'_>)
    -> Result<u32, Errno>;
pub fn bpf_map_update_batch(map: BorrowedFd<'_>, request: &BpfMapBatchInput<'_>)
    -> Result<u32, Errno>;
pub fn bpf_map_delete_batch(map: BorrowedFd<'_>, request: &BpfMapBatchInput<'_>)
    -> Result<u32, Errno>;
}

BpfMapBatchRequest carries the key/value buffers, the in_batch/out_batch cursor (opaque, managed by the kernel), the requested count and the flags. Batch operations amortize the syscall cost over large maps.

Freezing

#![allow(unused)]
fn main() {
/// `BPF_MAP_FREEZE` — makes the map non-modifiable from user space
/// (the eBPF program can still write to it according to its rights). Irreversible.
pub fn bpf_map_freeze(map: BorrowedFd<'_>) -> Result<(), Errno>;
}

Performance (maps). unit lookup/update/delete: ~1-3 µs (dominated by the syscall). The batch variants divide this cost by the batch factor.


Subsection 3: Programs

Loading (already-assembled program)

#![allow(unused)]
fn main() {
pub fn bpf_program_load(
    request: &BpfProgramLoadRequest<'_>,
    verifier_log: Option<&mut [u8]>,
) -> Result<BpfProgram, Errno>;

pub struct BpfProgramLoadRequest<'a> {
    pub program_type: BpfProgramType,
    /// **Already-assembled** program (layer 0 does not fabricate it).
    pub instructions: &'a [BpfInstruction],
    /// Program license (`GPL`, `Dual BSD/GPL`…). Conditions access to
    /// helpers marked GPL-only by the kernel.
    pub license: &'a CStr,
    pub name: Option<&'a CStr>,
    pub expected_attach_type: Option<BpfAttachType>,
    /// For fentry/fexit/LSM/tracing: target BTF + id of the attachment point.
    pub attach_btf: Option<BorrowedFd<'a>>,
    pub attach_btf_id: Option<u32>,
    /// Program to extend (`Ext` / freplace).
    pub attach_program: Option<BorrowedFd<'a>>,
    pub flags: BpfProgramLoadFlags,
    pub log_level: BpfVerifierLogLevel,
}

bitflags! { pub struct BpfProgramLoadFlags: u32 {
    const STRICT_ALIGNMENT = 1 << 0;
    const ANY_ALIGNMENT    = 1 << 1;
    const TEST_RND_HI32    = 1 << 2;
    const TEST_STATE_FREQ  = 1 << 3;
    const SLEEPABLE        = 1 << 4;
    const XDP_HAS_FRAGS    = 1 << 5;
}}

#[derive(Debug, Clone, Copy)]
pub enum BpfVerifierLogLevel { Disabled, Basic, Verbose, Stats }
}

Underlying syscall. bpf(BPF_PROG_LOAD, …). The kernel verifier analyzes the program: memory safety, termination (no unbounded loop), types. On rejection, the wrapper returns the kernel Errno (often EACCES/EINVAL) and fills verifier_log with the verifier’s textual diagnostic — an element indispensable to debugging. On a log too short: ENOSPC (and the log truncated).

Safety. bpf_program_load is not unsafe in the Rust sense: the kernel verifier guarantees the memory safety of the loaded program. The wrapper exposes no raw pointer. (The effect of an attached program on the system is another question, addressed at attachment.)

Test and binding

#![allow(unused)]
fn main() {
/// `BPF_PROG_TEST_RUN` / `BPF_PROG_RUN` — runs the program over a provided
/// context and data, without attaching it. Returns return value + duration.
pub fn bpf_program_test_run(
    program: BorrowedFd<'_>, request: &mut BpfProgramTestRunRequest<'_>,
) -> Result<BpfProgramTestRunResult, Errno>;

/// `BPF_PROG_BIND_MAP` — explicitly binds a map to a program (lifetime).
pub fn bpf_program_bind_map(
    program: BorrowedFd<'_>, map: BorrowedFd<'_>, flags: u32,
) -> Result<(), Errno>;
}

Two models coexist: the historical attachment (PROG_ATTACH/DETACH, mostly cgroup and sockmap, “anonymous” attachment without an object) and the modern bpf_link model (an FD object representing the attachment, detached on Drop).

#![allow(unused)]
fn main() {
/// `BPF_PROG_ATTACH` — historical attachment (cgroup, sk_skb, flow_dissector…).
pub fn bpf_program_attach(
    program: BorrowedFd<'_>, target: BorrowedFd<'_>,
    attach_type: BpfAttachType, flags: BpfAttachFlags,
) -> Result<(), Errno>;

/// `BPF_PROG_DETACH`.
pub fn bpf_program_detach(
    target: BorrowedFd<'_>, attach_type: BpfAttachType,
    program: Option<BorrowedFd<'_>>,
) -> Result<(), Errno>;

/// `BPF_LINK_CREATE` — creates a modern link (returns a RAII FD).
pub fn bpf_link_create(request: &BpfLinkCreateRequest<'_>) -> Result<BpfLink, Errno>;

/// `BPF_LINK_UPDATE` — hot-swaps the program of a link.
pub fn bpf_link_update(
    link: BorrowedFd<'_>, new_program: BorrowedFd<'_>,
    old_program: Option<BorrowedFd<'_>>, flags: u32,
) -> Result<(), Errno>;

/// `BPF_LINK_DETACH` — detaches the program while keeping the link (auto-detaches).
pub fn bpf_link_detach(link: BorrowedFd<'_>) -> Result<(), Errno>;

/// `BPF_RAW_TRACEPOINT_OPEN` — attaches a program to a raw tracepoint.
pub fn bpf_raw_tracepoint_open(
    name: &CStr, program: BorrowedFd<'_>,
) -> Result<BpfLink, Errno>;

/// `BPF_ITER_CREATE` — creates a BPF iterator from an iteration `BpfLink`.
/// Returns a readable FD that produces the iterator's output.
pub fn bpf_iterator_create(link: BorrowedFd<'_>, flags: u32) -> Result<OwnedFd, Errno>;

bitflags! { pub struct BpfAttachFlags: u32 {
    const ALLOW_OVERRIDE = 1 << 0;
    const ALLOW_MULTI    = 1 << 1;
    const REPLACE        = 1 << 2;
}}
}

System effect — safety note (not unsafe, but documented). Attaching a program modifies the behavior of the system (network filtering, LSM hooks, tracing). These functions are not unsafe in the memory sense, but their # Effects documentation reminds that an attached program runs on critical paths. Layer 5 (system services) orchestrates these attachments under the control of entitlements (ADR-010).

Performance. Creating a link / attachment: ~10-50 µs. The Drop of a BpfLink detaches automatically.


Subsection 5: Pinning (bpf filesystem)

#![allow(unused)]
fn main() {
/// `BPF_OBJ_PIN` — pins an object (map/program/link) under a path of the
/// `bpffs` mount (`/sys/fs/bpf/...`), to make it outlive the process.
pub fn bpf_object_pin(object: BorrowedFd<'_>, path: &CStr) -> Result<(), Errno>;

/// `BPF_OBJ_GET` — retrieves an FD to a previously pinned object.
pub fn bpf_object_get(path: &CStr, flags: BpfObjectGetFlags) -> Result<OwnedFd, Errno>;

bitflags! { pub struct BpfObjectGetFlags: u32 {
    const RDONLY = 1 << 3; const WRONLY = 1 << 4;
}}
}

The FD returned by bpf_object_get is re-adoptable into BpfMap::from_fd / BpfProgram::from_fd / BpfLink::from_fd depending on the type (verifiable via bpf_object_get_info_by_fd).


Subsection 6: Introspection by identifier

The kernel assigns a stable id to each live object. These commands iterate over the ids and convert id → FD (with privilege). The “start at the beginning” sentinel (start_id = 0) becomes Option<u32> (ADR-021 conv. 1).

#![allow(unused)]
fn main() {
pub fn bpf_program_get_next_id(after: Option<u32>) -> Result<Option<u32>, Errno>;
pub fn bpf_map_get_next_id(after: Option<u32>) -> Result<Option<u32>, Errno>;
pub fn bpf_btf_get_next_id(after: Option<u32>) -> Result<Option<u32>, Errno>;
pub fn bpf_link_get_next_id(after: Option<u32>) -> Result<Option<u32>, Errno>;
// `Ok(None)` = end of iteration (the kernel returns ENOENT).

pub fn bpf_program_get_fd_by_id(id: u32) -> Result<BpfProgram, Errno>;
pub fn bpf_map_get_fd_by_id(id: u32) -> Result<BpfMap, Errno>;
pub fn bpf_btf_get_fd_by_id(id: u32) -> Result<Btf, Errno>;
pub fn bpf_link_get_fd_by_id(id: u32) -> Result<BpfLink, Errno>;

/// `BPF_OBJ_GET_INFO_BY_FD` — fills a kernel info structure for an object.
/// `info_out` is a buffer whose size depends on the type of object queried.
pub fn bpf_object_get_info_by_fd(
    object: BorrowedFd<'_>, info_out: &mut [u8],
) -> Result<u32, Errno>; // bytes actually written

/// `BPF_PROG_QUERY` — lists the programs attached to a target (cgroup…).
pub fn bpf_program_query(request: &mut BpfProgramQueryRequest<'_>) -> Result<u32, Errno>;

/// `BPF_TASK_FD_QUERY` — queries the program behind a perf/tracepoint fd
/// of a given task (introspection of kprobe/uprobe).
pub fn bpf_task_fd_query(request: &mut BpfTaskFdQueryRequest<'_>) -> Result<(), Errno>;
}

Privilege. *_get_fd_by_id and PROG_QUERY typically require CAP_SYS_ADMIN/CAP_BPF. Documented.


Subsection 7: BTF (BPF Type Format)

#![allow(unused)]
fn main() {
/// `BPF_BTF_LOAD` — loads an **already-formed** BTF blob into the kernel.
/// (BTF *generation* is logic → layer 1.)
pub fn bpf_btf_load(
    btf_blob: &[u8],
    verifier_log: Option<&mut [u8]>,
    flags: BpfBtfLoadFlags,
) -> Result<Btf, Errno>;

bitflags! { pub struct BpfBtfLoadFlags: u32 { const TOKEN_FD = 1 << 0; }}
}

bpf_btf_get_fd_by_id / bpf_btf_get_next_id are in subsection 6. Layer 0 loads and references a BTF blob; the decoding of the BTF format (types, names, CO-RE relocations) is layer 1 logic.


Subsection 8: Statistics and tokens

#![allow(unused)]
fn main() {
/// `BPF_ENABLE_STATS` — globally enables a type of statistics (CPU time
/// spent in programs…). Returns an FD: the stats stay active as long
/// as it is open (RAII).
pub fn bpf_enable_statistics(kind: BpfStatsType) -> Result<OwnedFd, Errno>;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BpfStatsType { RunTime }

/// `BPF_TOKEN_CREATE` (Linux 6.9+) — creates a BPF privilege-delegation token
/// from a `bpffs` mount, to authorize BPF operations within a
/// container/namespace without global `CAP_SYS_ADMIN`. Consistent with Air's
/// confinement model.
pub fn bpf_token_create(bpffs: BorrowedFd<'_>, flags: u32) -> Result<OwnedFd, Errno>;
}

BpfStatsType is extensible (#[non_exhaustive] or a fallback variant) for the stat types added after 6.12.


Subsection 9: perf_event_open and event control

perf_event_open opens a performance counter/sampler. It is the canonical attachment point for tracing eBPF programs: you open a perf_event on a kprobe/uprobe/tracepoint, then attach a program to it via perf_event_set_bpf_program.

Opening

#![allow(unused)]
fn main() {
pub fn perf_event_open(
    attr: &PerfEventAttr,
    scope: PerfEventScope<'_>,
    group_leader: Option<BorrowedFd<'_>>,
    flags: PerfEventOpenFlags,
) -> Result<PerfEvent, Errno>;

/// Kernel `pid`/`cpu` encoded without the `-1` sentinel (ADR-021 conv. 1).
pub enum PerfEventScope<'a> {
    /// Current process, on any CPU (`pid=0, cpu=-1`).
    CallingProcessAnyCpu,
    /// Current process, on a specific CPU (`pid=0, cpu=N`).
    CallingProcessOnCpu(u32),
    /// A specific process, any CPU (`pid=P, cpu=-1`).
    ProcessAnyCpu(Pid),
    /// A specific process on a specific CPU (`pid=P, cpu=N`).
    ProcessOnCpu { process: Pid, cpu: u32 },
    /// All processes on a CPU (system-wide sampling, `pid=-1, cpu=N`).
    AllProcessesOnCpu(u32),
    /// Per-cgroup monitoring (`flags |= PID_CGROUP`, `pid = cgroup_fd`).
    Cgroup { cgroup: BorrowedFd<'a>, cpu: u32 },
}

bitflags! { pub struct PerfEventOpenFlags: u64 {
    const FD_NO_GROUP = 1 << 0;
    const FD_OUTPUT   = 1 << 1;
    // PID_CGROUP (1<<2) is handled via PerfEventScope::Cgroup, not exposed here.
    const FD_CLOEXEC  = 1 << 3; // always set by the wrapper
}}
}

Underlying syscall. perf_event_open: x86_64 #298, ARM64 #241. Available since Linux 2.6.31.

PerfEventAttr is a #[repr(C)] mirror of struct perf_event_attr (~120 bytes): explicit type name, fields with kernel names (type, config, sample_period, sample_type, read_format, bitfields disabled, inherit, exclude_kernel…). It implements Default (all zero = valid). Helper enums name the common values of the type field (PerfTypeId::{Hardware, Software, Tracepoint, HwCache, Raw, Breakpoint}) and hardware config values (PerfHardwareCounter::{CpuCycles, Instructions, …}), without hiding the raw structure. An ergonomic builder (high-level assembly) belongs to layer 1.

Preconditions / errors. EACCES/EPERM (privilege, perf_event_paranoid), EINVAL (inconsistent attr), ENOENT (unknown type/config), EMFILE, ENODEV, EOPNOTSUPP.

Reading the counters

#![allow(unused)]
fn main() {
impl PerfEvent {
    pub fn as_fd(&self) -> BorrowedFd<'_>;
    pub fn into_fd(self) -> OwnedFd;
    /// Reads the scalar value of the counter (simple `read_format`).
    pub fn read_count(&self) -> Result<u64, Errno>;
}
}

Reading in group mode (PERF_FORMAT_GROUP, several counters at once) and the decoding of the sampling ring (mmap of the ring buffer, walking the struct perf_event_header / PERF_RECORD_* records) are logic → layer 1. Layer 0 provides the FD: the ring is mmaped via mem::mmap (mem family), its interpretation is done above.

Control (dedicated ioctls — no generic ioctl, ADR-021 c.3)

#![allow(unused)]
fn main() {
pub fn perf_event_enable(event: BorrowedFd<'_>) -> Result<(), Errno>;   // IOC_ENABLE
pub fn perf_event_disable(event: BorrowedFd<'_>) -> Result<(), Errno>;  // IOC_DISABLE
pub fn perf_event_reset(event: BorrowedFd<'_>) -> Result<(), Errno>;    // IOC_RESET
pub fn perf_event_refresh(event: BorrowedFd<'_>, count: u32) -> Result<(), Errno>; // IOC_REFRESH
pub fn perf_event_set_period(event: BorrowedFd<'_>, period: u64) -> Result<(), Errno>; // IOC_PERIOD
pub fn perf_event_set_filter(event: BorrowedFd<'_>, filter: &CStr) -> Result<(), Errno>; // IOC_SET_FILTER
pub fn perf_event_id(event: BorrowedFd<'_>) -> Result<u64, Errno>;      // IOC_ID

/// `PERF_EVENT_IOC_SET_BPF` — attaches an eBPF program to this perf_event
/// (kprobe/uprobe/tracepoint). THE eBPF ↔ perf BRIDGE.
pub fn perf_event_set_bpf_program(
    event: BorrowedFd<'_>, program: BorrowedFd<'_>,
) -> Result<(), Errno>;

/// `PERF_EVENT_IOC_SET_OUTPUT` — redirects the output to another event's
/// ring; `None` detaches the redirection (`-1` sentinel, ADR-021 c.1).
pub fn perf_event_set_output(
    event: BorrowedFd<'_>, output: Option<BorrowedFd<'_>>,
) -> Result<(), Errno>;

pub fn perf_event_pause_output(event: BorrowedFd<'_>, pause: bool) -> Result<(), Errno>; // IOC_PAUSE_OUTPUT
pub fn perf_event_query_bpf(event: BorrowedFd<'_>, ids_out: &mut [u32]) -> Result<u32, Errno>; // IOC_QUERY_BPF
pub fn perf_event_modify_attributes(event: BorrowedFd<'_>, attr: &PerfEventAttr) -> Result<(), Errno>; // IOC_MODIFY_ATTRIBUTES
}

Underlying syscall (control). ioctl on the perf FD, one PERF_EVENT_IOC_* constant per function. No generic ioctl exposed.

Performance. Opening: ~10-30 µs. enable/disable/reset: ~1-2 µs. read_count: ~1-2 µs.


ebpf family summary

Exposed functions, by category:

CategoryMain functions
Maps — lifetimebpf_map_create, bpf_map_freeze
Maps — elementsbpf_map_lookup_element, bpf_map_update_element, bpf_map_delete_element, bpf_map_get_next_key, bpf_map_lookup_and_delete_element
Maps — batchbpf_map_lookup_batch, bpf_map_lookup_and_delete_batch, bpf_map_update_batch, bpf_map_delete_batch
Programsbpf_program_load, bpf_program_test_run, bpf_program_bind_map
Attachment / linksbpf_program_attach, bpf_program_detach, bpf_link_create, bpf_link_update, bpf_link_detach, bpf_raw_tracepoint_open, bpf_iterator_create
Pinningbpf_object_pin, bpf_object_get
Introspectionbpf_program_get_next_id, bpf_map_get_next_id, bpf_btf_get_next_id, bpf_link_get_next_id, bpf_program_get_fd_by_id, bpf_map_get_fd_by_id, bpf_btf_get_fd_by_id, bpf_link_get_fd_by_id, bpf_object_get_info_by_fd, bpf_program_query, bpf_task_fd_query
BTFbpf_btf_load
Stats / tokensbpf_enable_statistics, bpf_token_create
perf — lifetimeperf_event_open, PerfEvent::read_count
perf — controlperf_event_enable, perf_event_disable, perf_event_reset, perf_event_refresh, perf_event_set_period, perf_event_set_filter, perf_event_id, perf_event_set_bpf_program, perf_event_set_output, perf_event_pause_output, perf_event_query_bpf, perf_event_modify_attributes

Total: 37 bpf_* functions (exhaustive coverage of the 37 sub-commands of enum bpf_cmd, target 6.12) + ~14 perf_event_* functions, i.e. ~51 main public functions.

Deferred to layer 1 (out of scope): eBPF instruction assembler, ELF/libbpf loader, rich BTF generation and decoding, CO-RE relocations, decoding of the perf sampling ring / ring-buffer, ergonomic builders of PerfEventAttr and of programs.

Non-wrapped syscalls / operations (listed in UNSUPPORTED.md):

  • bpf(): no sub-command omitted — coverage is exhaustive for 6.12. Sub-commands added by a kernel later than 6.12 will be added by RFC, without breaking the API (Other(u32) variants already provided for).
  • perf_event_open: the decoding of the sampling ring-buffer is in layer 1 (logic), not a missing syscall.

Distribution of types between the two crates

In air-sys-types (pure, no syscall)

  • BpfInstruction — mirror of struct bpf_insn.
  • BpfMapType, BpfProgramType, BpfAttachType, BpfStatsType, BpfVerifierLogLevel, PerfTypeId, PerfHardwareCounter, PerfEventScope, EventClock (reused from device if relevant) — typed enums.
  • BpfMapCreateFlags, BpfMapUpdateFlags, BpfMapLookupFlags, BpfProgramLoadFlags, BpfAttachFlags, BpfObjectGetFlags, BpfBtfLoadFlags, PerfEventOpenFlags — bitflags.
  • PerfEventAttr — mirror of struct perf_event_attr (fields with kernel names).
  • The request structs (BpfMapCreateRequest, BpfProgramLoadRequest, BpfLinkCreateRequest, BpfMapBatchRequest, …) — pure views/aggregates passed to the wrappers.

In air-sys-syscall::ebpf (RAII calling syscalls)

  • BpfMap, BpfProgram, BpfLink, Btf, PerfEvent — own an OwnedFd, close on Drop. Same rule as SignalFd/LandlockRuleset/UEventSocket: a type that calls a syscall does not live in air-sys-types.

That is ~25 types added to air-sys-types (including 2 heavy mirrors) + 5 RAII.


Tests (strategy)

  • Maps — pure kernel round-trip: create a Hash/Array, update then lookup a key, get_next_key to iterate, delete, verify ENOENT. Deterministic without a program, but requires CAP_BPF → harness that skips cleanly if privilege is absent, and marks it in COVERAGE-EXCEPTIONS.md (“privilege” category).
  • Minimal program: load a trivial SocketFilter program (“return 0”) pre-assembled as a test constant (a few hand-written BpfInstruction), verify success; load an invalid program (uninitialized register) and verify that verifier_log contains the diagnostic. This tests the log path without an assembler.
  • Attachment/links: attach a Tracepoint/Kprobe program via perf_event_open + perf_event_set_bpf_program, trigger the event, verify a map counter; Drop detaches.
  • Introspection: *_get_next_id + *_get_fd_by_id on an object created in the test.
  • perf: open a Software/PERF_COUNT_SW_TASK_CLOCK counter on the current process, enable, do some work, read_count > 0, disable.
  • Property-based (proptest): encoding/decoding of the mirrors (BpfInstruction, PerfEventAttr, input-like) round-trip; any &[u8] passed as a key/value never panics (upstream length validation).
  • Fuzzing (cargo-fuzz): the decoding of the info structures returned by BPF_OBJ_GET_INFO_BY_FD accepts external data (kernel) → fuzz harness on the info decoders.
  • Coverage: the privileged branches not reachable in non-root CI are recorded in COVERAGE-EXCEPTIONS.md (“privilege”, “feature/kernel” categories). Cross-arch validation x86_64 + aarch64.

Foundational decisions that emerged in the ebpf family

1. Exhaustive coverage of bpf(), one function per sub-command.

The 37 sub-commands of enum bpf_cmd (6.12) are all exposed as dedicated typed functions. No generic bpf(cmd, attr, size) (ADR-021 conv. 3). The family is large, that is embraced: the complexity goes on the Air side.

2. We load already-formed programs/BTF (layer 1 boundary).

Like seccomp (Q4 security family), layer 0 loads a program or a BTF blob already assembled; it does not assemble, does not compile, does not relocate. The assembler, the libbpf loader, BTF generation, the decoding of the perf ring-buffer are in layer 1.

3. bpf_program_load is not unsafe; it fills the verifier log.

The kernel verifier guarantees memory safety ⇒ no unsafe. The log buffer is a first-class parameter: without it, debugging a rejection is impossible.

4. Kernel sentinels → Option/enums everywhere.

start_id = 0, pid/cpu = -1, key = NULL (first key), null output fd: all replaced by Option<T> or enums (PerfEventScope), in line with ADR-021 conv. 1.

5. RAII for maps, programs, links, BTF, perf_event.

No FD leaks; Drop detaches links. *_get_fd_by_id / obj_get return FDs re-adoptable via from_fd.

6. perf_event in the same family as eBPF.

Because perf_event_open + PERF_EVENT_IOC_SET_BPF is the bridge for attaching tracing programs. Grouping them avoids an orphaned perf family and reflects real usage.

7. Other(u32) / #[non_exhaustive] variants for longevity.

The type enums (map/prog/attach/stats) provide for a fallback so as not to break when a kernel later than 6.12 adds a value, without giving up typing.


Conclusion: layer 0 is complete

With the device and ebpf families, all eleven families of layer 0 are specified: process, fs, mem, signal, time, net, ipc, security, system, device, ebpf, plus the cross-cutting io_uring module and the air-sys-types types crate. To fully deliver layer 0, all that remains is the implementation of device, ebpf and the remaining io_uring phases (2a→4, 3a–3f), following the documented-skeleton-first method.


Document license: MPL 2.0 Status: Technical specification of the air-sys-syscall::ebpf module (layer 0).