This episode dissects seccomp's architecture: the SECCOMP_MODE_STRICT and SECCOMP_MODE_FILTER modes based on BPF, the role of prctl and the seccomp syscall, the libseccomp and seccomp_export_bpf components, and how seccomp integrates with runc, crun, systemd, and OpenSSH.

In episode 1 you understood seccomp's history and the problems it solves. Now it's time to open the hood: how does seccomp work inside the system? This episode dissects its architecture — from the path of a syscall, the two operating modes (SECCOMP_MODE_STRICT and SECCOMP_MODE_FILTER), the division of roles between the kernel and libseccomp, to how container runtimes and systemd leverage it.
If episode 1 answered why, episode 2 answers how. Internalize the architecture well, because every subsequent episode — return actions, the libseccomp API, argument filters — is just an extension of the framework we build here.
Before discussing seccomp, let's clarify what happens when a program makes a syscall. Consider the following flow:
Application (user mode)
│ makes a syscall, e.g. execve
▼
glibc / libc (translates it into a syscall instruction)
▼
Kernel: receives the syscall
▼
Seccomp filter installed? ── no ──► run the syscall normally
│ yes
▼
Kernel evaluates the BPF filter
│
├── ALLOW ──► run the syscall
└── DENY ──► kernel rejects it according to the return actionNote the key point: the filter is evaluated in the kernel, right after the syscall is received and before the syscall handler executes. This means there is no way for an application to evade it — even if the application is fully compromised by an attacker, the decision still rests with the kernel.
Seccomp has two operating modes, and both are answers to the two historical eras we covered in episode 1.
The oldest mode (kernel 2.6.12). Once enabled, a process may only call read, write, _exit, and sigreturn. Any violation immediately kills the process. It's enabled via prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT) and is irreversible.
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/prctl.h>
#include <linux/seccomp.h>
int main(void) {
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT) == -1) {
perror("prctl");
syscall(SYS_exit, 1);
}
write(STDOUT_FILENO, "strict mode active\n", 19);
syscall(SYS_exit, 0);
}Note
Note that the program above doesn't use return 0 at the end of main. On Linux, returning from main triggers the exit_group syscall — which strict mode does NOT allow — so the process would be killed. That's why we call syscall(SYS_exit, 0) directly, and it's also proof of just how impractical strict mode is for real applications.
The modern mode (kernel 3.5). A process installs one or more BPF filters via prctl or the seccomp() syscall. Each filter is a BPF program that receives seccomp data — containing the syscall number, architecture, instruction pointer, and six arguments — and returns a return action (allow, deny, kill, and so on).
Unlike strict mode, seccomp-bpf filters:
Nearly all real-world seccomp usage — Docker, Kubernetes, systemd, Chromium — uses this mode.
Seccomp consists of two cooperating layers: the kernel as the policy enforcer, and libseccomp as the policy-building helper.
On the kernel side, filters are installed through two paths:
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) — the traditional way.seccomp(2) syscall — the modern way, supporting flags like SECCOMP_FILTER_FLAG_TSYNC to synchronize filters across all threads.Every time a syscall is made, the kernel gathers the following data into a seccomp_data structure and hands it to the BPF program:
| Field | Contents |
|---|---|
nr | The syscall number (e.g. 59 for execve on x86_64) |
arch | The architecture identifier (e.g. SCMP_ARCH_X86_64) |
instruction_pointer | The address of the instruction that made the syscall |
args[0] through args[5] | The six syscall arguments |
The BPF program reads these fields and returns a return action. The beauty of it: seccomp BPF is guaranteed to terminate — the kernel rejects filters that could loop forever, so the overhead is predictable.
Writing BPF by hand is possible, but painful. Imagine having to write bytecode for a simple rule like "deny execve but allow everything else" — let alone handling syscall number differences across architectures. That's where libseccomp comes in.
libseccomp is a C library that abstracts all that complexity. You describe the policy declaratively:
SCMP_ACT_ALLOW for a deny-list, SCMP_ACT_ERRNO for an allow-list).seccomp_rule_add.And since the policy is produced as a BPF program, it can be exported to a file and dissected — that's the role of seccomp_export_bpf:
# libseccomp also ships tooling to dissect BPF
seccomp-tools dump ./program-with-filter
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x00 0xc000003e if (A != ARCH_X86_64) goto 0003
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x15 0x00 0x01 0x0000003b if (A == execve) goto 0005
0004: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0005: 0x06 0x00 0x00 0x00050000 return ERRNO(1)OCI runtimes like runc and crun read the seccomp profile from config.json every time a container starts. That profile is a JSON description very similar to what we build via libseccomp:
{
"defaultAction": "SCMP_ACT_ERRNO",
"defaultErrnoRet": 1,
"architectures": ["SCMP_ARCH_X86_64", "SCMP_ARCH_X86"],
"syscalls": [
{
"names": ["read", "write", "openat", "close", "mmap", "exit_group"],
"action": "SCMP_ACT_ALLOW"
},
{
"names": ["ptrace", "reboot", "kexec_load", "swapon"],
"action": "SCMP_ACT_ERRNO"
}
]
}This is the default profile Docker generates and installs on every container — you can see the full version on your machine with docker info and look for the SecurityOptions: seccomp entry.
For daemons running as service units, systemd provides seccomp directives without needing to write JSON. Just add SystemCallFilter to the unit file:
[Service]
ExecStart=/usr/sbin/sshd -D
NoNewPrivileges=yes
SystemCallFilter=@system-service
SystemCallFilter=~@obsolete @mount @privileged
SystemCallErrorNumber=EPERMSystemCallFilter accepts an allow-list (@system-service) and a deny-list (the ~ prefix). systemd translates these directives into the same kind of seccomp filter that libseccomp builds — with SystemCallErrorNumber=EPERM as the default action.
OpenSSH uses seccomp for privilege separation: the preauth process that faces network connection requests (and is therefore the most at risk) runs under a strict seccomp filter so dangerous syscalls like execve, connect, and openat are blocked. Once authentication succeeds, a new process with the user's privileges is built in a more controlled way. This is the same pattern you'll encounter again and again: the most vulnerable part of a program runs under the strictest seccomp filter, while the trusted parts run normally.
In episode 2 you understood seccomp's architectural framework:
SECCOMP_MODE_STRICT (4 syscalls, rigid) and SECCOMP_MODE_FILTER (BPF, flexible).prctl / the seccomp(2) syscall; libseccomp composes the BPF program.seccomp_data provides the syscall number, architecture, and six arguments for BPF to evaluate.runc/crun load the JSON profile from config.json, systemd via SystemCallFilter, and OpenSSH via privilege separation.In the next episode 3, we'll dissect the heart of seccomp's decision-making: modes & return actions — an in-depth comparison of strict vs filter, plus guidance on when to use SECCOMP_RET_KILL_PROCESS, TRAP, ERRNO, TRACE, ALLOW, LOG, and NOTIFY. There you'll learn how a filter "answers" every syscall. See you then!