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().
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:
| Field | Example | Why the filter wants it |
|---|---|---|
nr | 165 (mount) | which system call |
arch | AUDIT_ARCH_X86_64 | numbers differ between 32- and 64-bit calls, so check this first |
instruction_pointer | 0x7f3a… | where the call came from (rarely used) |
args[6] | CLONE_NEWNS | the 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 value | What happens | On the page |
|---|---|---|
SECCOMP_RET_ALLOW | the call runs normally | openat, getpid |
SECCOMP_RET_ERRNO | EPERM | the handler never runs; the call returns -1 with errno = EPERM | mount, unshare |
SECCOMP_RET_KILL_PROCESS | the whole process dies as if by SIGSYS; exit status 159 | ptrace |
SECCOMP_RET_LOG | the call runs, and an audit record is written | personality |
SECCOMP_RET_TRAP, _TRACE, _USER_NOTIF | send a catchable SIGSYS, ask a ptrace tracer, or ask a supervisor process | not 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(); }:
bpf(BPF_MAP_CREATE)makes the map@opens, a hash table in kernel memory.bpf(BPF_PROG_LOAD)hands over the bytecode.- 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. - The JIT compiler turns the bytecode into machine code for the CPU.
- 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.
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 filter | eBPF program | |
|---|---|---|
| who loads it | the process itself, for itself | a tool with CAP_BPF + CAP_PERFMON (or root) |
| instruction set | classic BPF, forward jumps only | eBPF: 11 registers, helpers, maps, bounded loops |
| checked by | a simple checker | the verifier |
| runs | on every system call of that process | on a hook, for every process that reaches it |
| result | allow, error, kill, log | whatever the hook allows: counts in a map, drop a packet, deny an LSM check |
| can be removed | never | yes, 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.