The idea: small programs that the kernel runs for you

Normally only kernel developers decide what the kernel does. BPF changes that a little: a process or a tool hands the kernel a tiny program, and the kernel runs it at a fixed point. The page shows the two best-known uses side by side. On the left, a seccomp filter decides, for every system call a process makes, whether it may run. On the right, an eBPF program, loaded by the tracing tool bpftrace, counts how often each process calls openat().

User space on top with app (pid 4242) calling openat on /etc/hosts and bpftrace (pid 6001) running @opens[comm] = count(). In kernel space, the call goes to the system call entry, then the seccomp filter, a classic BPF program that reads seccomp_data (nr, arch, args). The filter can answer ERRNO (-EPERM) or KILL (SIGSYS); on ALLOW the call reaches sys_openat and do_sys_openat2. A kprobe there runs the verified, JIT-compiled eBPF program, which adds 1 to the BPF map @opens, holding app 2 and cat 1. bpftrace loads the program and reads the map with the bpf() system call. RAX = 3 goes back to the app.
One openat() meets both programs: the seccomp filter decides if it may run, and the eBPF probe counts it.

Both run inside the app's own system call, on the app's CPU, before anything returns to user space. And both are safe to hand to the kernel for the same reason: the kernel checks the program before it accepts it, so it can't crash or hang the machine.

seccomp: a filter on every system call

A process installs a filter in two calls. First prctl(PR_SET_NO_NEW_PRIVS, 1): the process promises that execve will never give it more privileges (no setuid programs), so an ordinary user can't use a filter to trick a setuid program. Then seccomp(SECCOMP_SET_MODE_FILTER, 0, &prog) hands over the program. The kernel sets Seccomp: 2 in /proc/<pid>/status. From then on the filter runs on every system call, it can't be removed, and children inherit it across fork() and execve() (Demo: allowed syscall).

On each call, secure_computing() fills a struct seccomp_data and runs the filter over it. That is all the filter can see:

FieldExampleWhy the filter wants it
nr165 (mount)which system call
archAUDIT_ARCH_X86_64numbers differ between 32- and 64-bit calls, so check this first
instruction_pointer0x7f3a…where the call came from (rarely used)
args[6]CLONE_NEWNSthe argument values; pointers are only numbers, the filter can't read the strings they point to

The filter is a classic BPF program, the same old instruction set tcpdump uses: load a field, compare, jump forward, return. Jumps only go forward, so it always ends. The page's filter has 13 instructions: check arch, load nr, compare it against six numbers, return.

What the filter can answer

Return valueWhat happensOn the page
SECCOMP_RET_ALLOWthe call runs normallyopenat, getpid
SECCOMP_RET_ERRNO | EPERMthe handler never runs; the call returns -1 with errno = EPERMmount, unshare
SECCOMP_RET_KILL_PROCESSthe whole process dies as if by SIGSYS; exit status 159ptrace
SECCOMP_RET_LOGthe call runs, and an audit record is writtenpersonality
SECCOMP_RET_TRAP, _TRACE, _USER_NOTIFsend a catchable SIGSYS, ask a ptrace tracer, or ask a supervisor processnot drawn

ERRNO is the polite answer: the program sees an ordinary permission error and can carry on (Demo: blocked mount returns EPERM). KILL_PROCESS is for calls only an attacker would make: the process gets no chance to react (Demo: KILL_PROCESS on ptrace). LOG helps you write a filter: run the program, read the audit log, and see which calls it really needs.

Docker's default profile

Docker (through runc) installs a filter in every container just before it starts the container's program. The default profile allows about 300 system calls and answers EPERM for the rest, including mount, umount2, reboot, kexec_load, unshare, bpf, add_key and open_by_handle_at. These are the calls that would let a process in a container reach out to the host: mount the host's disk, load a new kernel, make new namespaces. The page's filter is a short deny-list that blocks the same few calls; Docker's real one is an allow-list. Run docker run --security-opt seccomp=unconfined and the filter is gone; --privileged turns it off too. See What a Container Really Is for the namespaces and cgroups that work next to it.

eBPF: load, verify, JIT, attach

eBPF (extended BPF) is the newer, bigger instruction set: 11 64-bit registers, a 512-byte stack, calls to kernel helper functions, and maps. A tool loads a program with the bpf() system call (number 321). The page follows bpftrace running kprobe:do_sys_openat2 { @opens[comm] = count(); }:

  1. bpf(BPF_MAP_CREATE) makes the map @opens, a hash table in kernel memory.
  2. bpf(BPF_PROG_LOAD) hands over the bytecode.
  3. The verifier simulates every path through the program. It must reach exit, touch only memory it is allowed to (its stack, a map value it looked up), check a pointer for NULL before using it, and call only helpers allowed at this kind of hook. If anything is not provably safe, the load fails.
  4. The JIT compiler turns the bytecode into machine code for the CPU.
  5. The program is attached to a hook: here a kprobe, a breakpoint the kernel puts on the first instruction of do_sys_openat2. Other hooks are tracepoints, network packets (XDP, tc), cgroups and LSM security hooks.
Two rows going left to right through bpftrace script, eBPF bytecode, verifier, JIT and attach to hook. Top row: count() per comm becomes 7 instructions with a lookup and a NULL check, the verifier marks it as ending and memory safe, the JIT makes 96 bytes of x86-64 code, and it is attached to kprobe do_sys_openat2: accepted. Bottom row: while ($n == 0) { } becomes 3 instructions with 1: if r0 == 0 goto 1; the verifier sees the same state at insn 1 again and rejects it; bpf() returns -EINVAL and nothing is compiled or attached.
The verifier is the gate: a program that might not end, or might touch bad memory, never reaches the JIT.

Loops are allowed (since Linux 5.3) only when the verifier can prove they end, for example for (i = 0; i < 16; i++). The page's second program spins on while ($n == 0) {}, but nothing ever changes $n. The verifier reaches instruction 1 a second time with exactly the same register state, reports infinite loop detected at insn 1, and bpf() returns -EINVAL (Demo: the verifier rejects an unbounded loop).

Maps: how the numbers get back to user space

An eBPF program can't print or write to a file. It talks to user space through BPF maps: hash tables, arrays, per-CPU arrays, ring buffers. On every openat(), the program asks for the current process name (bpf_get_current_comm), looks it up in @opens and adds 1, all in kernel memory, with no extra system call. Only when you press Ctrl-C does bpftrace read the map with bpf(BPF_MAP_GET_NEXT_KEY) and bpf(BPF_MAP_LOOKUP_ELEM) and print it (Demo: eBPF counts openat per process):

Attaching 1 probe...
^C
@opens[cat]: 1
@opens[app]: 2

opensnoop from the BCC tools does the same job differently: instead of counting, it sends one event per open (pid, name, path, result) through a ring buffer map, and the tool prints a line for each.

seccomp-BPF vs eBPF

seccomp filtereBPF program
who loads itthe process itself, for itselfa tool with CAP_BPF + CAP_PERFMON (or root)
instruction setclassic BPF, forward jumps onlyeBPF: 11 registers, helpers, maps, bounded loops
checked bya simple checkerthe verifier
runson every system call of that processon a hook, for every process that reaches it
resultallow, error, kill, logwhatever the hook allows: counts in a map, drop a packet, deny an LSM check
can be removedneveryes, when the tool closes it

Inside the kernel, the classic filter is translated to eBPF and runs on the same engine. How a system call gets from the syscall instruction to its handler is on A Linux System Call, Step by Step; the SIGSYS that ends the app is a signal like those on How Linux Delivers a Signal.

What the page leaves out

The page's filter checks only nr; real filters also check argument values (e.g. allow clone but not with namespace flags), and libseccomp writes them for you. It leaves out several stacked filters, SECCOMP_RET_USER_NOTIF (used by container managers to handle a call on the process's behalf), the cost of the filter, and the old strict mode (SECCOMP_MODE_STRICT: only read, write, _exit, sigreturn). On the eBPF side it skips BTF and CO-RE, tracepoints and fentry, XDP and networking programs, ring buffers in detail, the verifier's register range tracking, the 1 million instruction limit, unprivileged BPF, and bpftrace's real bytecode (the 7 instructions shown are simplified). The numbers are x86-64 system call numbers; pids and addresses are examples.