Seccomp and seccomp-BPF
seccomp (“secure computing”) is the Linux kernel’s primitive for letting a process shrink the slice of the kernel it is able to reach — the single most effective tool for reducing the kernel attack surface a program exposes. The kernel exports roughly 350+ system calls; almost all of those are entry points into kernel code that a compromised process could try to abuse to escalate privilege, escape a sandbox, or trigger a kernel bug. seccomp installs a classic Berkeley Packet Filter (cBPF) program — hence “seccomp-BPF” — that the kernel runs on every system call the thread makes, and that program votes per-syscall on whether to allow it, fake an error, kill the task, or hand the decision to a userspace supervisor (seccomp_filter.rst, v6.12). The security payoff is blunt and powerful: a process can only attack the syscalls you leave open to it. This note is the security and confinement view — the threat model, allowlist-vs-denylist design, the hard limitation that filters cannot inspect pointer arguments, and the real profiles (Docker, systemd, browsers). The syscall-entry mechanics — exactly where in the entry path the filter runs, and the
struct seccomp_datalayout — live in seccomp and Syscall Filtering and are not re-derived here; the precise return-action catalog and filter flags live in seccomp Filter Modes and Return Actions.
The Threat Model: The Syscall Boundary Is the Attack Surface
To understand why seccomp matters, start from where the trust boundary actually sits. A userspace process cannot touch hardware, files, network, other processes, or kernel memory directly — every privileged operation must cross into the kernel through a system call. The system-call interface is therefore the entire surface across which an untrusted process can act on the rest of the machine. (For how that boundary works mechanically, see Linux System Call Interface MOC.)
This framing reveals the core insight: the kernel’s attack surface, as seen by one process, is exactly the set of syscalls that process can issue. Every syscall handler is kernel C code running at ring 0 with full privilege; every one is a potential target for a memory-corruption bug, a logic flaw, or a privilege-escalation path. The infamous container escapes and local-privilege-escalation CVEs of the last decade almost all route through a specific syscall: clone with namespace flags, unshare, ptrace, bpf, io_uring operations, keyctl, userfaultfd, move_pages, and so on. If a process never needs bpf(2), and the kernel refuses to even enter the bpf handler when that process calls it, then the entire class of bpf-subsystem kernel bugs becomes unreachable from that process — regardless of whether those bugs exist or get discovered later.
seccomp operationalizes this. The classic statement, from Kees Cook (the seccomp maintainer) via the kernel documentation, is that filtering the syscall surface is defense in depth: even a process running as root, or one that has already been partially compromised, can do nothing through a syscall the kernel has been told to refuse. This is why seccomp is described in the Linux Security MOC as “the single most effective sandbox primitive for reducing kernel attack surface” — it is the only mechanism that directly narrows the count of kernel entry points a process can drive.
flowchart LR subgraph BEFORE["No seccomp"] P1["Compromised<br/>process"] -->|"any of ~350 syscalls"| K1["Full kernel<br/>syscall surface"] end subgraph AFTER["With seccomp allowlist"] P2["Compromised<br/>process"] -->|"only ~60 allowed"| K2["Narrowed surface"] P2 -.->|"bpf, ptrace, mount,<br/>kexec_load, keyctl..."| BLOCKED["BLOCKED<br/>(handler never entered)"] end
The seccomp threat-model reduction. What it shows: without seccomp, a compromised process can drive any of the kernel’s hundreds of syscall handlers; with a seccomp allowlist it can reach only the handful the program legitimately needs, and the dangerous syscalls are refused before their kernel code ever runs. The insight to take: seccomp does not make the blocked syscalls safe — it makes them unreachable, which is strictly stronger, because an unreachable bug cannot be exploited even if it exists.
Allowlist vs Denylist — and Why Allowlists Win
A seccomp policy is, fundamentally, a partition of the syscall set into “permitted” and “refused.” There are two ways to author that partition, and the choice has profound security consequences.
A denylist (blocklist) enumerates the dangerous syscalls and refuses them, allowing everything else by default. A allowlist enumerates the needed syscalls and refuses everything else by default. The structural difference is the default action: a denylist defaults to ALLOW and overrides specific entries to deny; an allowlist defaults to DENY (an errno or a kill) and overrides specific entries to allow.
Allowlists are strictly safer, for a reason that is easy to state and hard to overstate: the kernel keeps adding syscalls. When kernel 6.x introduces a new syscall — io_uring (5.1), pidfd_open (5.3), process_madvise, mseal (6.10), landlock_*, cachestat, and so on — a denylist that was authored before that syscall existed silently allows it, because its default is ALLOW. The denylist author would have had to predict the future. An allowlist authored before the new syscall existed silently refuses it, because its default is DENY — the new syscall is simply not in the allowed set, so it is closed by construction. The allowlist fails safe; the denylist fails open. The history of seccomp profiles is littered with exactly this failure: profiles that did not block io_uring when it arrived (because it did not exist when they were written) became a meaningful container-escape surface until the profiles were updated. Docker’s own default profile was retroactively patched to deny the io_uring syscalls for this reason (see Production Notes).
There is a second, subtler argument for allowlists. The set of syscalls a given program needs is small and knowable — you can trace a program with strace or seccomp’s own SECCOMP_RET_LOG action and discover its working set, which is typically a few dozen syscalls. The set of syscalls that are dangerous is large, fuzzy, and version-dependent — you have to keep a complete and current mental model of every kernel subsystem. It is far easier to be exhaustive about “what my program does” than about “what an attacker might want.”
The practical wrinkle — and it is a real one — is that Docker’s “default seccomp profile” is described as a denylist (“it blocks about 44 dangerous syscalls”) but is implemented as an allowlist. Its top-level defaultAction is SCMP_ACT_ERRNO (deny-by-default), and it then enumerates the ~300+ syscalls it permits; the “44 blocked” are simply the well-known dangerous syscalls that are absent from the allow set (moby default.json, fetched 2026-06-19). The mental model “Docker blocks 44 syscalls” is a useful summary of the effect, but the mechanism is a default-deny allowlist — which is why it correctly refuses syscalls invented after the profile was written.
Uncertain
Verify: the exact count of “dangerous syscalls Docker’s default profile refuses” (commonly quoted as ~44). Reason: the figure circulates in secondary write-ups and the Docker docs prose, but the profile is structurally an allowlist (
defaultAction: SCMP_ACT_ERRNO), so “44 blocked” is a derived count of well-known syscalls absent from the allow set, not a literalSCMP_ACT_ERRNO-listed count, and the number drifts as the kernel adds syscalls. To resolve: diff the kernel’s current syscall table against thenamesarrays in moby/profiles default.json for a given arch and count the absent ones; the Docker docs’ enumerated “blocked” list ran to ~56 named syscalls when fetched 2026-06-19.
The Hard Limitation: Filters Cannot Dereference Pointers
The single most important constraint to internalize about seccomp — the one that shapes every real profile and explains why a whole second API (SECCOMP_RET_USER_NOTIF) exists — is that a cBPF seccomp filter can read the syscall’s register arguments but cannot dereference any pointer among them.
When the filter runs, it is handed a small read-only record, struct seccomp_data, containing the syscall number, the architecture, the instruction pointer, and the six argument registers as raw 64-bit values (the field layout and entry-path details are in seccomp and Syscall Filtering). Crucially, those are register values. If a syscall argument is a pointer — open(const char *pathname, ...), connect(int, const struct sockaddr *, ...), execve(const char *filename, ...) — the filter sees only the numeric address, not the bytes it points at. cBPF, by deliberate design, has no instruction that loads from an arbitrary userspace address; per the kernel documentation, “BPF programs may not dereference pointers which constrains all filters to solely evaluating the system call arguments directly” (seccomp_filter.txt).
This is not an oversight — it is the central security property. Earlier system-call-interposition frameworks (the academic ptrace-based sandboxes of the 1990s–2000s, and tools like Systrace) did read the memory behind pointer arguments to make decisions, and they were systematically broken by time-of-check-to-time-of-use (TOCTOU) races. The attack is simple: the sandbox reads the pathname behind a pointer, sees an innocuous value like "/tmp/safe", approves the call; then a second thread in the same address space overwrites that memory with "/etc/shadow" after the check but before the kernel’s syscall handler reads it. The check and the use see different bytes. As the kernel documentation puts it, “BPF makes it impossible for users of seccomp to fall prey to time-of-check-time-of-use (TOCTOU) attacks that are common in system call interposition frameworks.” LWN’s coverage states the race plainly: for connect(2), “the addr argument would be read by the seccomp filter for inspection and later read again by the underlying system call when it is used… The malicious process could trigger a race condition, where the data in memory could be an innocuous one when the seccomp inspection happens and some malicious data when the system call is used” (LWN 799557).
The consequence for policy design is concrete and constraining: seccomp can filter on which syscall is called and on scalar/flag arguments, but it cannot filter on the contents of strings, paths, or structures. You can write a filter that allows openat but refuses socket(AF_PACKET, ...) (because AF_PACKET is a scalar in a register). You cannot write a filter that allows openat only for paths under /var/log — the path is behind a pointer, and even if you could read it, the TOCTOU race would make the decision meaningless. This is why argument-based filtering on pointers is fundamentally fragile and is not done, and why seccomp is paired with Landlock (which can reason about which file a syscall touches, because it operates inside the kernel where the path is resolved race-free) when path-level confinement is needed. The kernel’s answer for the cases that genuinely need deep argument inspection is SECCOMP_RET_USER_NOTIF — see seccomp User Notification — where a privileged supervisor in a different address space is notified, can read the target’s memory through /proc/pid/mem while the target is blocked, and must itself use SECCOMP_IOCTL_NOTIF_ID_VALID to re-check the request has not been replaced, carefully managing its own TOCTOU window.
sequenceDiagram participant T1 as Target thread A participant T2 as Target thread B (attacker) participant SC as seccomp filter participant K as Syscall handler Note over T1,K: Why pointer dereference would be unsafe (the TOCTOU race) T1->>SC: open(ptr) — ptr -> "/tmp/safe" SC->>SC: (hypothetically) read *ptr = "/tmp/safe" → ALLOW T2->>T1: overwrite *ptr = "/etc/shadow" SC->>K: dispatch open(ptr) K->>K: read *ptr = "/etc/shadow" — opens the WRONG file
The TOCTOU race that pointer-dereferencing filters would suffer. What it shows: if seccomp read the path behind the pointer to decide, a second thread could swap the memory between the check and the kernel’s use of it, so the decision applies to different bytes than the syscall acts on. The insight to take: cBPF’s inability to dereference pointers is the feature — by refusing to look at pointed-to memory at all, seccomp is structurally immune to this race, at the cost of being unable to filter on string/path content.
How a seccomp Policy Is Installed (the Security-Relevant Steps)
From the security perspective the installation path has two load-bearing properties: it is irreversible, and it requires the process to have first given up the ability to gain privilege. A filter is installed with prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) or, more flexibly, the seccomp(SECCOMP_SET_MODE_FILTER, flags, &prog) syscall. Once installed, a filter can never be removed or relaxed for the lifetime of the thread — only further filters (which can only narrow, never widen) can be added. This one-way property is what makes seccomp trustworthy as a confinement: a compromised process cannot un-sandbox itself.
The precondition is the keystone of the whole design. Unless the caller holds CAP_SYS_ADMIN, it must first set the no_new_privs bit via prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); otherwise the filter install fails with EACCES. In the kernel: if (!task_no_new_privs(current) && !ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN)) return ERR_PTR(-EACCES); (seccomp.c, v6.12). The reason, from the man page, is precisely a privilege-escalation defense: “This requirement ensures that an unprivileged process cannot apply a malicious filter and then invoke a set-user-ID or other privileged program using execve(2)” (seccomp(2)). Without no_new_privs, an attacker could install a filter that, say, makes setuid silently return 0 (fake success) and then exec a setuid-root binary that believes it dropped privileges but did not. no_new_privs closes that by guaranteeing no descendant can ever acquire privileges the installer lacked. This interlock is why seccomp is safe to expose to wholly unprivileged processes — see no_new_privs and Privilege Escalation Control for the full mechanism.
Filters are inherited across fork/clone and preserved across execve, so a sandbox installed by a launcher applies to every child and to whatever binary the child execs. This is what lets a container runtime install a profile once, in the runtime, and have it bind the containerized application and all its subprocesses.
Real Users — Who Actually Deploys seccomp
seccomp is not an academic curiosity; it is load-bearing infrastructure in essentially every modern sandbox.
Container runtimes. Docker/Moby, containerd, and CRI-O all apply a default seccomp profile to every container unless explicitly disabled. This is the most widely deployed seccomp policy on Earth. The mechanics are covered in Production Notes below; the headline is that it is a default-deny allowlist generated by libseccomp and compiled into a cBPF program at container start. Kubernetes exposes this through the pod securityContext.seccompProfile field — see SecurityContext and the Linux Containers and Isolation MOC for how containers compose seccomp with capabilities, namespaces, and cgroups.
systemd. Service units use SystemCallFilter= to seccomp-confine daemons. systemd ships curated syscall groups (@system-service, @privileged, @mount, @reboot, @swap, @clock, @debug, …) so an administrator can write SystemCallFilter=@system-service (a broad allowlist of what normal services need) and then subtract dangerous groups with the ~ negation prefix. This makes seccomp hardening a one-line config change, which is why so much of a modern Linux system’s daemon fleet runs under it — see Production Notes.
Web browsers. Chromium’s renderer and GPU processes, and Firefox’s content processes, install aggressive seccomp filters: a compromised renderer (the part that parses untrusted HTML/JS/images) is reduced to a tiny syscall surface so that a renderer exploit cannot directly attack the kernel or the rest of the system. This was one of the earliest large-scale production uses of seccomp-BPF and a primary motivator for its design.
gVisor and sandboxed runtimes. Google’s gVisor implements a userspace kernel (the Sentry) that intercepts the sandboxed application’s syscalls; gVisor itself runs the Sentry under a tight seccomp filter so that even if the Sentry is compromised, its access to the host kernel is minimal. seccomp is the last-line containment for the sandbox’s own privileged component. Other examples include OpenSSH’s privilege-separated network-facing process and many language-runtime sandboxes.
Failure Modes and Common Misunderstandings
“seccomp blocks files/paths.” It does not. seccomp filters syscalls and scalar arguments, never path or string content (the pointer-dereference limitation above). If you want “this process may only read files under /srv,” that is a Landlock or MAC (AppArmor/SELinux) job, not a seccomp job. Conflating the two leads to profiles that look stricter than they are.
A too-tight allowlist crashes the app with SIGSYS or ENOSYS. The most common operational failure is forgetting a syscall the program needs — often a libc-version-dependent one (newfstatat vs stat, clone3 vs clone, rseq, getrandom). The symptom is the process dying with SIGSYS (if the default action is a kill) or a baffling ENOSYS/EPERM from glibc (if the action is errno). The robust development loop is to run with SECCOMP_RET_LOG or SECCOMP_FILTER_FLAG_LOG, exercise the program fully, harvest the logged syscalls from the audit log, and only then switch to a killing action. libc upgrades that switch to newer syscalls are a recurring cause of profiles breaking on a distro bump.
Architecture confusion (the x32/compat trap). A filter only sees the architecture in seccomp_data.arch. A classic bypass is a 64-bit process making a 32-bit (i386) or x32 syscall, which uses a different syscall-number table; a filter that only checks the x86-64 numbers and forgets to refuse the other ABIs leaves a hole. Real profiles must either pin seccomp_data.arch to the expected value and kill everything else, or enumerate every ABI. This is why Docker’s profile and systemd both let you constrain SystemCallArchitectures=/architectures explicitly.
seccomp is not a privilege boundary by itself. It reduces attack surface but does not, e.g., stop a process from reading files its UID already permits. It composes with — does not replace — DAC, capabilities, namespaces, and MAC. A hardened sandbox is all of these layered (see the decision framework in Linux Security MOC).
Alternatives and When to Choose Them
seccomp is one of several confinement primitives, and the right tool depends on what you are trying to constrain. Capabilities (POSIX Capabilities) constrain which privileged operations a process may perform (bind low ports, load modules) but do not reduce the raw syscall surface — a process can still enter every syscall handler, just be refused inside it on a capability check. seccomp is complementary: drop capabilities and refuse the syscalls. Landlock constrains which filesystem objects and network endpoints a process may touch, race-free, which is exactly the path-content filtering seccomp cannot do; use Landlock for “which files,” seccomp for “which syscalls.” MAC (SELinux, AppArmor) enforces an administrator-authored system-wide policy with rich object labeling — far more expressive than seccomp but requiring privileged policy authoring, whereas seccomp is self-imposed and needs no admin. Namespaces (Linux Containers and Isolation MOC) change what a process can see; seccomp changes what it can ask the kernel to do. The honest summary: seccomp is the cheapest, most portable, self-imposable attack-surface reducer, and it is almost always used alongside the others rather than instead of them. For the explicit three-way comparison, see Landlock vs seccomp vs Namespaces.
Production Notes
Docker / Moby default profile. The canonical profile lives at moby/profiles seccomp/default.json. Its structure (verified 2026-06-19): top-level "defaultAction": "SCMP_ACT_ERRNO" with "defaultErrnoRet": 1 — i.e. default-deny, returning EPERM (errno 1) for any syscall not explicitly allowed, making it a genuine allowlist despite the “blocks ~44” framing. It then permits the large common set with SCMP_ACT_ALLOW, with conditional rules for sensitive syscalls: clone/clone3 are allowed but with argument masks that forbid the namespace-creation flags (CLONE_NEWNS, CLONE_NEWUSER, …) when CAP_SYS_ADMIN is absent; personality is restricted to a whitelist of values; socket is constrained away from dangerous domains; and ptrace, bpf, and perf_event_open are gated behind the matching capability (CAP_SYS_PTRACE, CAP_BPF, CAP_PERFMON). The Docker docs (engine/security/seccomp) enumerate the well-known refused syscalls and their rationale: acct (could disable resource limits), add_key/keyctl/request_key (non-namespaced keyring), bpf, clock_settime/settimeofday (time is not namespaced), kexec_load/kexec_file_load, mount/umount2, ptrace, reboot, and — added after the fact — the io_uring_* syscalls due to container-escape vulnerabilities. That last addition is the allowlist-vs-denylist argument in miniature: even an allowlist needed an explicit decision once io_uring became prominent, and a denylist would have been wide open to it for years.
systemd SystemCallFilter=. systemd compiles SystemCallFilter= directives into a seccomp filter per service. The pattern most hardened units use is SystemCallFilter=@system-service (allow the broad set of syscalls a normal service needs) followed by subtractive lines like SystemCallFilter=~@privileged @resources using the ~ negation prefix to remove dangerous groups from the allowed set. By default a refused syscall causes the process to be killed with SIGSYS; setting SystemCallErrorNumber=EPERM instead makes refused syscalls return EPERM (a graceful error the program can handle) rather than dying. SystemCallArchitectures=native is the standard guard against the multi-ABI bypass described above. This turns kernel-attack-surface reduction into a few lines of unit config and is why a huge fraction of a modern systemd-based distribution’s daemons run seccomp-confined out of the box.
Uncertain
Verify: the exact systemd syntax claims — that
@system-serviceis the recommended broad allowlist group, that~is the negation/subtraction prefix, and thatSystemCallErrorNumber=switches the default-kill to a returned errno. Reason: thesystemd.exec(5)“System Call Filtering” section could not be retrieved during this task (man7.org returned a truncated page and freedesktop.org returned HTTP 403); these specifics are corroborated only by secondary write-ups. To resolve: read the “System Call Filtering” section ofsystemd.exec(5)directly (man systemd.exec) and confirm the group names and prefix semantics for the installed systemd version.
libseccomp is the universal builder. Almost nobody writes raw cBPF for seccomp by hand. The libseccomp library compiles a high-level rule set (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0), etc.) into an optimized cBPF program and installs it; its action wrappers map directly onto the kernel return actions — SCMP_ACT_ALLOW, SCMP_ACT_ERRNO(n), SCMP_ACT_KILL, SCMP_ACT_TRAP, SCMP_ACT_NOTIFY, SCMP_ACT_LOG (seccomp_rule_add(3)). Docker, containerd, runc, systemd, and most language runtimes build their filters through libseccomp. The exact return-action catalog those map to is detailed in seccomp Filter Modes and Return Actions; the practical “how do I write one” walkthrough is Writing a seccomp Filter.
See Also
- seccomp and Syscall Filtering — sibling; the syscall-entry mechanics: where in the entry path seccomp runs,
struct seccomp_data, inheritance - seccomp Filter Modes and Return Actions — sibling; the strict/filter modes, the precedence-ordered return actions, and the filter flags
- seccomp User Notification — the supervisor-based escape hatch for the deep-argument-inspection cases seccomp filters cannot handle
- Writing a seccomp Filter — the practical authoring walkthrough (libseccomp, cBPF)
- no_new_privs and Privilege Escalation Control — the interlock that makes seccomp safe for unprivileged processes
- Landlock — the complementary primitive for which files/endpoints (the path filtering seccomp cannot do)
- Landlock vs seccomp vs Namespaces — explicit three-way comparison
- POSIX Capabilities — the orthogonal “which privileged operations” axis
- Linux Containers and Isolation MOC — how containers compose seccomp with capabilities, namespaces, cgroups
- Linux Security MOC — parent; seccomp is §E, “the system-call boundary is the security perimeter”