Introduction

Every program that runs on Linux, macOS, or Android must ask the operating-system kernel for anything it wants to do beyond pure computation — open a file, write to a socket, spawn a child process, allocate memory. These requests are called system calls (or syscalls), and there are roughly three hundred of them on a modern Linux kernel.

That channel is precisely what an attacker exploits. When a bug lets an adversary run arbitrary code inside your process — a buffer overflow, a use-after-free, a deserialized payload — the very first thing the exploit does is reach for a syscall: connect out to a command-and-control server, write a backdoor to disk, or escalate privilege via a kernel vulnerability.

seccomp (Secure Computing mode) is a Linux kernel feature that lets a process permanently restrict the system calls it may make. The kernel enforces the policy at the hardware level; no amount of code running inside the process can lift it. A blocked syscall simply returns an error — or kills the process outright, before the payload gets to do anything useful.

The security model is principle of least privilege applied to the OS interface: give the process only the syscalls it genuinely needs, and everything else becomes a wall the exploit cannot climb.

Try It

Below is a simplified model of a web-server process. The left panel shows the syscalls it legitimately needs. The right panel shows the extra syscalls an exploit would try after taking control.

Toggle calls in the allowlist on or off, then press Run exploit to see which steps succeed and which are blocked by seccomp.

<!-- {{c_html_comment}} -->
<div class="container">
  <div class="panel">
    <div class="panel-title">{{label_allowlist}}</div>
    <p class="hint-small">{{hint_allowlist}}</p>
    <div id="allowlist" class="syscall-list"></div>
  </div>
  <div class="panel">
    <div class="panel-title">{{label_exploit_steps}}</div>
    <p class="hint-small">{{hint_exploit}}</p>
    <div id="exploit-steps" class="syscall-list readonly"></div>
  </div>
</div>
<div class="controls">
  <button id="run" type="button">{{btn_run}}</button>
  <button id="reset" type="button" class="ghost">{{btn_reset}}</button>
</div>
<div id="log" class="log" aria-live="polite"></div>
/* {{c_css_comment}} */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.container { display: flex; gap: 12px; margin-bottom: 10px; }
.panel { flex: 1; min-width: 0; }
.panel-title { font-weight: 700; font-size: .85rem; margin-bottom: 4px; color: #1d3557; }
.hint-small { font-size: .78rem; color: #555; margin: 0 0 6px; line-height: 1.4; }
.syscall-list { display: flex; flex-direction: column; gap: 4px; }
.syscall-row { display: flex; align-items: center; gap: 6px; padding: 5px 8px;
               border-radius: 6px; border: 1px solid #cdd9e3; background: #f0f4f8; }
.syscall-row .name { font-family: ui-monospace, monospace; font-size: .82rem;
                     font-weight: 600; color: #1d3557; flex: 1; }
.syscall-row .desc { font-size: .75rem; color: #555; }
.syscall-row input[type=checkbox] { cursor: pointer; accent-color: #1d3557; }
.syscall-row.readonly { background: #fff3cd; border-color: #ffc107; }
.controls { display: flex; gap: 8px; margin-bottom: 10px; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
         background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.log { font-family: ui-monospace, monospace; font-size: .8rem; background: #1a1a2e;
       color: #e0e0e0; border-radius: 8px; padding: 10px 12px; min-height: 80px;
       max-height: 160px; overflow-y: auto; white-space: pre-wrap; }
.log .ok   { color: #6fcf97; }
.log .bad  { color: #eb5757; }
.log .info { color: #56ccf2; }
// Code not found

Notice that even a single blocked syscall can stop an entire attack chain. A web server that can read and write but cannot connect can never phone home. One that cannot execve can never spawn a shell. The allowlist does not need to be perfect — it just needs to remove the one call the exploit depends on.

The Real Complexity

The original seccomp (2005, Andrea Arcangeli) was brutal: once engaged, only four syscalls were allowed — read, write, exit, and sigreturn. Useful for computation, hopeless for real services.

seccomp-BPF (merged in Linux 3.5, 2012) replaced the fixed list with a Berkeley Packet Filter program that the process itself writes and loads into the kernel. Each time the process makes any syscall, the kernel runs this tiny bytecode program synchronously — before the syscall executes — and acts on what it returns:

  • SECCOMP_RET_ALLOW — let the call through unchanged.
  • SECCOMP_RET_ERRNO — return a specified error code to the caller (the process stays alive).
  • SECCOMP_RET_KILL_PROCESS — terminate the entire process immediately, no signal handlers.
  • SECCOMP_RET_TRAP — deliver SIGSYS so a supervisor can inspect the call.
  • SECCOMP_RET_TRACE — pause and hand control to a ptrace tracer (used by container runtimes).

The BPF program can inspect the syscall number and all six arguments, so a filter can say "allow open only with O_RDONLY" or "allow socket only for AF_INET." Once a filter is loaded it is inherited by child processes and cannot be relaxed — only further restricted. A process can load multiple stacked filters; the most restrictive verdict wins.

The cost is real but small: a well-written filter adds roughly 100–300 nanoseconds per syscall, measured on hardware from 2020. For a web server making millions of syscalls per second the overhead is measurable; for most services it is negligible compared to network latency.

Tools like libseccomp let you write filters in C without touching raw BPF bytecode. Container runtimes (Docker, Podman, containerd) ship default seccomp profiles that block around 44 of the most dangerous calls while allowing everything a normal service needs. The Linux Security Module framework (AppArmor, SELinux) works at a higher level — files and capabilities — and pairs naturally with seccomp's syscall-level view.

Where It Matters

Syscall filtering is the quiet foundation of modern system security:

  • Web browsers: Chrome's renderer processes run inside a seccomp sandbox since 2009 — every tab is locked to a minimal syscall set so a JavaScript exploit cannot escape to the OS. Firefox and Safari use equivalent mechanisms.
  • Container runtimes: Docker's default seccomp profile (updated with each release) blocks calls like ptrace, reboot, kexec_load, and perf_event_open that are legitimate in a full OS but dangerous in a container. Kubernetes allows per-pod profiles.
  • Serverless / cloud functions: AWS Lambda, Google Cloud Run, and Fly.io all enforce seccomp on every invocation — user code literally cannot make many OS calls even if it tries.
  • Android: since Android 8 (Oreo, 2017), every app and system service runs under a seccomp filter tuned to its declared permissions; it is the enforcement layer below the Java permission model.
  • OpenSSH: since OpenSSH 6.0 the sandbox mode uses seccomp (on Linux) to lock down the pre-authentication privilege-separated helper, reducing the attack surface before a user even authenticates.

The pattern is always the same: compute what the process genuinely needs, install the filter at startup, and let the kernel silently enforce it for the entire lifetime of the process.

Conclusion

seccomp is one of those rare security mechanisms that is both theoretically clean and practically effective. The idea could not be simpler: a process voluntarily surrenders the system calls it does not need, and the kernel enforces that surrender forever. No matter how clever the exploit, it cannot conjure a syscall that the filter has already blocked.

What makes it powerful is also what limits it. seccomp works at the level of individual call numbers and raw arguments — it cannot understand intent or context the way a human analyst could. A misconfigured filter that blocks a needed call will break the application; a filter that allows one call too many may leave a gap. Getting the allowlist right requires careful profiling and testing.

But the effort pays off. The halting problem tells us we can never fully verify what arbitrary code will do. seccomp sidesteps the question: instead of proving the code is safe, it removes the tools the unsafe code would need. That is a fundamentally different — and much more tractable — approach to the limits of what software can do.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/sandboxing-seccomp/Content licensed under CC BY-NC 4.0.