The seccomp-bpf Filter: Sandboxing Syscalls with a JIT-Compiled Program the Kernel Runs on Every Entry

2026-07-21

You've seen landlock for filesystem sandboxing. But sandboxing at the syscall layer — deciding which of the ~350 Linux syscalls a process is even allowed to invoke — is done with seccomp-bpf. It's the mechanism behind Chrome's renderer sandbox, systemd's SystemCallFilter=, Docker's default profile, and Firefox's content processes.

The core idea: you install a small BPF program via prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) or seccomp(2). On every syscall entry, before the kernel dispatches to the handler, it runs your BPF program against a struct seccomp_data containing the syscall number, architecture, instruction pointer, and first six argument registers. The program returns a 32-bit action:

Two invariants keep this safe. First, filters are append-only and only allow narrowing — once installed, a process can add more restrictive filters but never remove one, so a compromised child can't escape. Second, PR_SET_NO_NEW_PRIVS must be set (or you need CAP_SYS_ADMIN), which is what lets unprivileged processes install filters without becoming a privilege-escalation vector via setuid binaries.

Concrete example — OpenSSH's privsep child: after authentication, sshd's unprivileged network-facing child installs a seccomp filter that permits roughly 40 syscalls (read, write, select, brk, mmap with anonymous flags, sigreturn, exit) and returns SECCOMP_RET_KILL_PROCESS for everything else. If a memory-corruption bug in the SSH protocol parser gives an attacker RIP control, they can't call execve, open, or socket — the kernel kills the process before the handler runs. The filter is 60 lines of hand-written BPF.

Rule of thumb — the cost: a seccomp filter adds roughly 100–300 ns per syscall depending on program length. On a workload doing 100k syscalls/second, that's ~1–3% CPU. Filters are JIT-compiled on x86-64 and arm64 (check /proc/sys/net/core/bpf_jit_enable), which is what keeps the overhead sub-microsecond — an interpreted filter would be 5–10× worse.

The classic footgun: filtering on syscall arguments that are pointers. BPF can't dereference user memory, so you can only match register values. A filter that "allows open only for /tmp/*" is impossible in seccomp alone — you match the flags and mode ints, but the path pointer is opaque. This is why USER_NOTIF exists: for path-based decisions, you delegate to a supervisor that can read the target's memory via /proc/pid/mem.

Key Takeaway: seccomp-bpf runs a JIT-compiled classifier on every syscall entry, letting an unprivileged process permanently drop access to syscalls it will never legitimately need — but only on register values, never on the memory those registers point to.

All newsletters