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.
