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
securityfamily, Q4). As with seccomp, layer 0 does not assemble and does not compile an eBPF program. Producing eBPF bytecode (encodingstruct 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 ofBpfInstructionprovided by the caller), creating/querying maps, attaching, pinning, introspecting.Therefore outside layer 0 (specified in layer 1, future
air-bpfcrate) are: the instruction assembler, the libbpf-style loader (ELF parsing, sections, relocations), the generation and rich manipulation of BTF, the decoding of theperf/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.
-
Universal CLOEXEC. All eBPF FDs (maps, programs, links, BTF) and the
perf_eventFDs are createdCLOEXEC(theBPF_F_*CLOEXEC/PERF_FLAG_FD_CLOEXECflag is set by the wrapper). -
No generic multiplexed syscall (ADR-021 conv. 3).
bpf()multiplexes 37 operations,ioctlon perf multiplexes a dozen: each becomes a typed function. The complexity is concentrated on the Air side, not the caller side. -
Kernel sentinels →
Option<T>/ enums (ADR-021 conv. 1). The-1“all CPUs / all processes” ofperf_event_open, theid0 “start of iteration”, the optional fds becomeOption<T>or enums. -
RAII for all resources.
BpfMap,BpfProgram,BpfLink,Btf,PerfEventown anOwnedFdand close it onDrop. No FD leaks. -
EINTR surfaced as-is (ADR-021 conv. 2). No automatic retry.
-
Privileges. Most operations require
CAP_BPF(+CAP_PERFMONfor tracing,CAP_NET_ADMINfor networking) since Linux 5.8, orCAP_SYS_ADMINbefore that. Unprivileged BPF is often disabled (kernel.unprivileged_bpf_disabled). Documented per function; not hidden. -
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 ⇒EINVALon 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, notBpfMt). The variants mirror the kernel constantsBPF_MAP_TYPE_*/BPF_PROG_TYPE_*(kernel authority, which the developer finds again inuapi/linux/bpf.h), in CamelCase, with universally accepted abbreviations tolerated (Lru,Xdp,Lsm). TheOther(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. Array ⇒ key_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_loadis notunsafein 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>;
}
Subsection 4: Attachment and links
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 notunsafein the memory sense, but their# Effectsdocumentation 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:
| Category | Main functions |
|---|---|
| Maps — lifetime | bpf_map_create, bpf_map_freeze |
| Maps — elements | bpf_map_lookup_element, bpf_map_update_element, bpf_map_delete_element, bpf_map_get_next_key, bpf_map_lookup_and_delete_element |
| Maps — batch | bpf_map_lookup_batch, bpf_map_lookup_and_delete_batch, bpf_map_update_batch, bpf_map_delete_batch |
| Programs | bpf_program_load, bpf_program_test_run, bpf_program_bind_map |
| Attachment / links | bpf_program_attach, bpf_program_detach, bpf_link_create, bpf_link_update, bpf_link_detach, bpf_raw_tracepoint_open, bpf_iterator_create |
| Pinning | bpf_object_pin, bpf_object_get |
| Introspection | bpf_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 |
| BTF | bpf_btf_load |
| Stats / tokens | bpf_enable_statistics, bpf_token_create |
| perf — lifetime | perf_event_open, PerfEvent::read_count |
| perf — control | perf_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 ofstruct bpf_insn.BpfMapType,BpfProgramType,BpfAttachType,BpfStatsType,BpfVerifierLogLevel,PerfTypeId,PerfHardwareCounter,PerfEventScope,EventClock(reused fromdeviceif relevant) — typed enums.BpfMapCreateFlags,BpfMapUpdateFlags,BpfMapLookupFlags,BpfProgramLoadFlags,BpfAttachFlags,BpfObjectGetFlags,BpfBtfLoadFlags,PerfEventOpenFlags— bitflags.PerfEventAttr— mirror ofstruct 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 anOwnedFd, close onDrop. Same rule asSignalFd/LandlockRuleset/UEventSocket: a type that calls a syscall does not live inair-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,updatethenlookupa key,get_next_keyto iterate,delete, verifyENOENT. Deterministic without a program, but requiresCAP_BPF→ harness that skips cleanly if privilege is absent, and marks it inCOVERAGE-EXCEPTIONS.md(“privilege” category). - Minimal program: load a trivial
SocketFilterprogram (“return 0”) pre-assembled as a test constant (a few hand-writtenBpfInstruction), verify success; load an invalid program (uninitialized register) and verify thatverifier_logcontains the diagnostic. This tests the log path without an assembler. - Attachment/links: attach a
Tracepoint/Kprobeprogram viaperf_event_open+perf_event_set_bpf_program, trigger the event, verify a map counter;Dropdetaches. - Introspection:
*_get_next_id+*_get_fd_by_idon an object created in the test. - perf: open a
Software/PERF_COUNT_SW_TASK_CLOCKcounter 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 byBPF_OBJ_GET_INFO_BY_FDaccepts 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).