Seccomp-BPF Syscall Filtering for Sandboxed Processes
Kernel-enforced syscall filtering prevents processes from accessing system calls they don't need.

Seccomp-BPF enforces process containment by filtering every system call a process attempts, evaluating it inside the kernel before the corresponding handler ever runs. This piece examines why the syscall interface is the correct place to put that enforcement, how the filter machine actually evaluates a call, what a working filter looks like in practice, and where static filtering breaks down for processes that change behavior over time.
The syscall boundary as the right place to enforce process containment
Every interaction a user-space process has with the operating system, reading a file, opening a socket, forking a child, has to cross the syscall interface. There is no side channel around it and no alternate path a process can take to reach the kernel. That singularity makes the syscall boundary the one choke point the kernel actually owns in full, so any serious containment strategy for a process starts there.
The mechanics of that crossing make the boundary enforceable rather than merely observable, since seccomp-BPF inserts itself at exactly that point, between syscall entry and handler dispatch, so the filter evaluates the call before any kernel code specific to that call has executed. If the filter's verdict is anything other than allow, the handler underneath never runs. The kernel enforces that outcome directly, rather than the process cooperating with some external monitor's request.
That distinction matters against the alternative of user-space interposition, typically built on ptrace. The Linux kernel documentation states that BPF filtering makes this class of time-of-check-to-time-of-use attack impossible, because the filter program cannot dereference pointers and therefore has nothing to re-check against a value that might change underneath it. A seccomp-BPF filter, once installed, cannot be bypassed or unloaded by the process it constrains, because the evaluation happens inside the kernel, outside the reach of anything running in user space.
The need for that kind of enforcement follows from how much surface the kernel exposes by default. Each one is a potential route into kernel code, regardless of whether the application ever calls it, so the structural attack surface facing any given process is considerably wider than what that process requires to do its job.
The mechanism used to close that gap did not arrive fully formed. Seccomp-BPF, added in Linux 3.5 in 2012, generalized the idea into a programmable per-process filter, letting a process specify exactly which syscalls it needed and under what argument conditions. That generalization is what turned seccomp from a narrow-purpose tool into the general containment mechanism examined through the rest of this piece.
Evaluating a syscall: the filter machine and verdict set
A seccomp-BPF filter is a program, and understanding what it can see and what it can decide requires understanding the data it runs against and the machine that runs it. Every syscall the kernel intercepts for filtering purposes is represented as a struct seccomp_data: the syscall number, an AUDIT_ARCH_* value identifying the calling architecture, the CPU instruction pointer at the moment of the call, and the six argument registers. That structure is fixed and read-only. The filter program can inspect it but cannot alter it, and nothing the process does afterward changes what the filter already evaluated.
The program itself runs on classic BPF, not the extended BPF (eBPF) used elsewhere in the kernel for tracing and networking. That choice was deliberate. Each instruction is eight bytes. The program itself is a linear sequence of instructions with conditional jumps, and nothing resembling a loop is permitted, so every filter is guaranteed to terminate in a bounded number of steps. That guarantee is part of what lets the kernel accept arbitrary user-supplied filter programs without opening itself to a denial-of-service through a runaway filter.
What a filter can return is similarly constrained, and the available verdicts are ranked in a strict precedence order so that when multiple filters are stacked on a process, the strictest one always wins. If a ptrace-based tracer is attached, SECCOMP_RET_TRACE hands the call off to it, and otherwise returns -ENOSYS. SECCOMP_RET_LOG, added in kernel 4.14, permits the call but logs it, useful for auditing a filter's real-world behavior before tightening it. SECCOMP_RET_ALLOW sits at the bottom of the precedence order and simply lets the syscall proceed. Because stacking filters can only make the outcome stricter, never looser, layering additional filters onto a process is always a safe operation from a security standpoint, even if the filters were written independently of each other.
Two structural constraints shape everything a policy author can do with this machine. The first is that installing a filter requires either prctl(PR_SET_NO_NEW_PRIVS, 1) or CAP_SYS_ADMIN, and the no-new-privs requirement exists specifically so a filter cannot later be attached to a child process carrying more privilege than the process that installed it. The second, and the one with the deeper consequences for policy design, is that BPF programs cannot dereference pointers. A filter can inspect the syscall number and the raw values sitting in the argument registers, but it cannot follow a pointer argument to see what it points to in memory. That constraint rules out TOCTOU attacks against the filter itself, and it also defines the outer limit of what argument-based filtering can express, a limit that becomes very concrete once policy authoring is on the table.
None of this costs much at runtime. The BPF interpreter, or the JIT compiler when net.core.bpf_jit_enable is set, evaluates each filter in nanoseconds, and published benchmarks on typical policies generally show overhead under a few percent, though syscall-heavy workloads can push that figure measurably higher depending on filter complexity and call volume.
Writing a working filter: allowlist structure, architecture checks, and argument inspection
Writing a seccomp-BPF filter correctly starts with a step that is easy to skip and expensive to skip wrongly: checking the calling architecture before anything else. On a 64-bit kernel, a 32-bit process can issue 32-bit syscall numbers, and those numbers can overlap with completely different 64-bit syscall handlers. A filter that evaluates syscall numbers without first confirming which architecture issued them is vulnerable to a process invoking the wrong handler under the wrong numbering scheme. A correct filter loads seccomp_data.arch as its first instruction and kills the process outright if that value doesn't match the expected AUDIT_ARCH_* constant. A filter that skips that check can look complete on paper while leaving an entire alternate syscall table unguarded.
Once architecture is settled, the rest of the filter is an allowlist built on a simple shape: a default action, usually SECCOMP_RET_KILL_PROCESS, and a sequence of BPF jumps, one per permitted syscall number, each returning SECCOMP_RET_ALLOW. During development, swapping SECCOMP_RET_ERRNO in for SECCOMP_RET_KILL_PROCESS as the default lets a developer run the application and observe which syscalls get rejected, without the process dying on the first missing entry. Production deployment should switch that default back to KILL, since an ERRNO default left in place means a missing syscall degrades into unpredictable application behavior rather than a clean, auditable failure.
Argument filtering extends the allowlist from syscall numbers to syscall numbers under specific conditions. Each argument is at a known offset in seccomp_data.args[N], and a filter instruction loads the relevant 32-bit word and compares it with BPF_JEQ; 64-bit arguments need their high and low words checked separately, since the comparison instruction only operates on 32 bits at a time. A filter can express something like "allow open(), but only with O_RDONLY set.
That example also exposes the hard boundary built into the mechanism. Because filters cannot dereference pointers, a filter can confirm that the flags argument to open() is O_RDONLY, but it cannot inspect the path string that open() was actually given. Path-based access control over which files a process can touch falls to a complementary mechanism such as Landlock, since seccomp-BPF by itself cannot express it.
Kubernetes deployments fold this into the platform layer: custom seccomp profiles are JSON files placed in /var/lib/kubelet/seccomp/, and a pod's securityContext references a profile by name. The Pod Security Standards require seccomp profile configuration for any workload running at the Restricted level. Whichever authoring path is used, treating the resulting JSON or C source as infrastructure-as-code, version controlled in Git alongside the application it constrains, keeps the filter auditable and reviewable as a living artifact that gets revisited.
What all of this adds up to is a filter that is correct for whatever single syscall set it was written to allow. The next question is whether one such set is ever actually sufient for the full life of a process, and the answer turns out to be no.
What a static profile cannot capture: temporal specialization and the phase problem
Most production deployments write one filter, load it at process startup, and never touch it again. SysComb, published by Rossi and colleagues at the Università degli Studi di Bergamo, documents this pattern directly, finding that production applications tend to adopt an overly permissive filter at launch and rarely revise it afterward. None of those forces are unreasonable on their own, but together they leave a filter that was only ever sized for the broadest phase of the application's life.
A typical process does not use the same syscalls throughout its existence. It moves through phases: initialization, where it opens configuration files, binds sockets, and loads libraries; serving, where it handles the actual workload and the untrusted input that comes with it; and shutdown, where it flushes state and releases resources. Each phase has its own distinct, and considerably smaller, syscall footprint. A single static filter covering all three phases at once has to be as permissive as the union of everything any phase ever needs, even though no single phase needs all of it simultaneously.
That union is where exploitation concentrates, because the serving phase, the one actually processing untrusted input, inherits syscall permissions that were only ever necessary during initialization. If the filter could be made to shrink at the moment the process transitions from initialization into serving, the kernel attack surface would shrink precisely when the process is most exposed, since it's handling input it did not generate itself. SysComb demonstrates this concretely, showing that state-aware, per-phase filters block a larger number of shellcodes than single, unified filters do, because shellcode payloads typically depend on syscalls that are legitimate during initialization but have no business being available once the process is serving traffic.
Two enforcement strategies capture the shape this kind of temporal specialization can take. A seccomp-like strategy makes filters progressively more restrictive as the process advances through its phases, so that once a syscall is removed from the permitted set, it can never come back regardless of what later phase the process enters. A least-privilege strategy instead replaces the filter wholesale at each phase transition, granting each phase exactly the syscalls it needs and nothing inherited from before. Both approaches solve the same underlying problem, and the choice between them is a question of whether a policy author wants monotonic tightening or exact per-phase precision.
ARMO's 2026 analysis of the policy layer above eBPF draws a related distinction: for traditional workloads, static definitions, TracingPolicies, seccomp profiles, NetworkPolicies, map cleanly onto discrete kernel events, and that mapping is straightforward to write and reason about. The phase problem in seccomp filtering is a specific instance of that broader distinction: a static profile is a traditional-workload tool applied to a process whose actual behavior is dynamic.
None of this is purely a technical gap, either. A static, overly broad filter is in part a predictable response to a tool that is genuinely hard to use with precision.
The real question, then, is how a filter this precise and time-sensitive can be built without asking every application developer to instrument their own code to announce its phase transitions.
SysComb and seccomp user-notification: recovering temporal precision without modifying the application
SysComb answers that question with an eBPF-based mechanism that detects an application's internal state transitions from outside the application, without requiring any change to the application's source code or the kernel it runs on. Existing approaches to phase-specific filtering have each carried a real cost: some require modifying the application to signal its own transitions, some require a patched kernel that most production environments cannot adopt, and some delegate enforcement to a user-space component that becomes a new attack surface in its own right. SysComb's contribution is doing the detection and the filter replacement transparently, observing the process's behavior with eBPF.
The SECCOMP_RET_USER_NOTIF verdict, available since kernel 5.0, is the complementary piece that makes this practical. Rather than resolving a syscall decision entirely inside the kernel filter, SECCOMP_RET_USER_NOTIF forwards the decision to a user-space supervisor process, which can then apply logic far more sophisticated than a BPF program is capable of expressing, logic that can account for the phase the monitored process is currently in. Combining eBPF-driven state detection with user-notification handling means the phase transition itself can be observed without the application's knowledge, and the resulting filter change can be enforced without the application ever being modified to ask for it.
That combination recovers the temporal precision a static profile cannot offer, while preserving the property that made the syscall boundary worth enforcing from in the first place: the kernel, not the process, decides what happens next, and the decision happens before any handler runs it.
Sources
- SysComb: Fine-Grained Transparent System Call Filtering for Attack Surface Reduction
- Seccomp BPF (SECure COMPuting with filters) — The Linux Kernel documentation
- SysComb: Fine-Grained Transparent System Call Filtering for Attack Surface Reduction
- Seccomp BPF (SECure COMPuting with filters) — The Linux Kernel documentation


