Linux eBPF

Last update: 2026-06-09 02:54:06 +0800 +0800
tags: Bpf | Kernel | Linux
Available translations: 日本語 / 中文(简体) / 中文(繁體)

eBPF is a verified execution environment in the Linux kernel. Applications load programs and maps, attach programs to kernel-defined hooks, and exchange data through file descriptors. Networking remains an important use, but tracing, security policy, and performance analysis no longer depend on packet capture.

From Packet Filters to Linux Socket Filters #

The design lineage begins with DEC’s packet filter for systems including ULTRIX and DIGITAL UNIX. A process opened a packet-filter device, selected an interface, installed an enfilter instruction sequence with ioctl(), and read accepted link-layer packets. The filter used a stack machine, and multiple consumers were ordered by priority.

Steven McCanne and Van Jacobson designed the Berkeley Packet Filter (BPF) to reduce packet-copy and interpretation costs. Their 1993 paper describes two changes that remain recognizable:

  • a register-based virtual machine decides whether a packet should be accepted and how many bytes should be copied;
  • each open /dev/bpf instance has its own filter and packet buffer, so packet capture is expressed as a device instance rather than DEC’s priority chain.

Classic BPF, now called cBPF, has a 32-bit accumulator A, an index register X, 16 scratch words, packet-load instructions, conditional jumps, arithmetic, and a return value. Linux adopted the instruction format for Linux Socket Filtering (LSF), but attaches a filter directly to a socket rather than requiring /dev/bpf.

c
struct sock_filter code[] = {
    BPF_STMT(BPF_LD  | BPF_B | BPF_ABS, 9), /* IPv4 protocol byte */
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, IPPROTO_TCP, 0, 1),
    BPF_STMT(BPF_RET | BPF_K, 0xffff),
    BPF_STMT(BPF_RET | BPF_K, 0),
};
struct sock_fprog program = {
    .len = sizeof(code) / sizeof(code[0]),
    .filter = code,
};

setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER,
           &program, sizeof(program));

The example expects the socket’s data to begin with an IPv4 header, so byte 9 is its protocol field. Packet layout depends on the socket family and hook. For a socket filter, the return value is the number of packet bytes to keep; zero rejects the packet. The kernel checks the instruction sequence before installing it. SO_LOCK_FILTER can prevent replacement or removal, which lets a process attach a filter and then drop privilege. libpcap and tcpdump compile high-level capture expressions into this instruction model.

The eBPF Execution Model #

Linux’s modern BPF system call interface arrived in Linux 3.18. eBPF uses fixed-width 64-bit instructions and eleven 64-bit registers:

  • R0 carries a function or program return value.
  • R1 through R5 pass arguments and are caller-saved.
  • R6 through R9 are callee-saved.
  • R10 is the read-only frame pointer for a per-invocation stack.

On entry, R1 points to a program-type-specific context such as an xdp_md, __sk_buff, or tracing context. The program returns a hook-specific action: an XDP program can pass, drop, redirect, or transmit a packet, while a tracing program commonly returns zero.

An interpreter can execute accepted bytecode. When the architecture enables a BPF JIT, the kernel translates the program into native instructions and can harden generated images according to sysctl policy. JIT compilation changes execution cost, not verifier requirements.

Verification Defines the Safety Boundary #

Before a program can attach, the verifier symbolically executes its possible paths. It tracks register and stack-slot types, scalar ranges, pointer provenance, alignment, initialization, reference ownership, and bounds. Therefore code such as a packet read is accepted only after control flow proves that the requested bytes precede data_end.

Verification enforces properties such as:

  • no use of uninitialized registers or stack data;
  • no arbitrary conversion of scalars into kernel pointers;
  • only program-type-approved context fields and helper functions;
  • bounded memory access and valid pointer arithmetic;
  • balanced acquisition and release of tracked references;
  • bounded execution, including loops whose termination can be proven.

Acceptance proves conformance to the verifier’s model, not functional correctness. A valid program can drop every packet, aggregate the wrong key, or add excessive overhead. Complexity limits also mean that logically safe source can be rejected when path exploration cannot establish the required invariants.

Maps and Helpers #

Maps are kernel objects with type-specific key/value storage and concurrency semantics. Hashes, arrays, per-CPU maps, ring buffers, LRU maps, longest-prefix tries, program arrays, and storage maps serve different communication patterns. User space creates and operates on maps through bpf() commands, commonly wrapped by libbpf:

c
int map_fd = bpf_map_create(BPF_MAP_TYPE_HASH, "counts",
                            sizeof(__u32), sizeof(__u64),
                            1024, NULL);
bpf_map_update_elem(map_fd, &key, &value, BPF_ANY);

A program uses approved helpers or BPF kfuncs to look up map elements, read time and process identity, redirect packets, emit ring-buffer events, or access hook-specific data. Helper availability and argument types depend on the program type. A map lookup can return NULL; the verifier requires a null check before dereference.

Maps are shared state, so races remain an application concern. Per-CPU maps avoid cross-CPU updates for counters. Atomic operations or struct bpf_spin_lock are available only where their documented constraints permit them, and user space must aggregate per-CPU values explicitly.

Program Types and Attachment Points #

A program type selects the context, return convention, verifier operations, and allowed helpers. Attachment type further identifies a hook. Common combinations include:

AreaProgram and Attachment Examples
Early packet pathXDP on a network device
Traffic controlSCHED_CLS or SCHED_ACT at TC ingress/egress
Socketssocket filters, reuseport selection, socket lookup, sockops, cgroup socket hooks
Tracingtracepoints, raw tracepoints, kprobes/kretprobes, fentry/fexit, perf events
SecurityLSM hooks and cgroup policy hooks

Hook choice determines both semantics and cost. XDP runs before allocation of the normal networking stack’s socket buffer on supported drivers, whereas TC programs see an skb. fentry/fexit use BTF-described function boundaries, while kprobes target instruction addresses and are more sensitive to implementation changes.

Privilege, Lifecycle, and Ownership #

Loading is privileged unless the kernel and system policy allow the requested operation. Modern kernels can separate powers with CAP_BPF, CAP_PERFMON, and CAP_NET_ADMIN; older paths and some operations still use CAP_SYS_ADMIN. Distribution kernels can disable unprivileged loading through kernel.unprivileged_bpf_disabled, and lockdown, LSM, namespace, token, or delegation policy can impose additional restrictions. Check the running kernel rather than treating one capability recipe as universal.

Programs, maps, BTF objects, and links are reference-counted kernel objects exposed through file descriptors. Closing the last reference destroys an object. Pinning an object in BPF filesystem (bpffs) gives it a persistent path and reference:

bash
mount -t bpf bpf /sys/fs/bpf
bpftool prog show
bpftool map show
bpftool link show

bpf_link represents an attachment independently from a raw program FD and provides a safer lifecycle for supported hooks: closing the link detaches the program unless another reference or pin keeps the link alive. Pinning survives the creating process during the current boot. After a reboot, startup code must recreate the kernel objects; it cannot reopen objects from the previous kernel instance.

Observability without Unlimited Access #

Tracing programs can measure latency, count events, sample stacks, correlate scheduler activity, and export structured records through ring buffers. CO-RE relocations use BTF to adapt field accesses across compatible kernel type layouts. Operational tooling should still account for:

  • event loss when consumers cannot drain buffers;
  • per-event overhead at high-frequency hooks;
  • symbol and type changes on kernels without suitable BTF;
  • privacy and security implications of exposing kernel or process data.

Use bpftool prog show, bpftool prog dump xlated, verifier logs, and program run-time statistics to inspect what is loaded. Observability code runs in production kernel paths, so bounded overhead and an explicit detach plan belong in its design.

References #

Comments

Read or comment on GitHub (sign-in required to comment)