Linux seccomp & seccomp-bpf
seccomp (Secure Computing Mode) is a Linux kernel security facility designed to restrict the system calls that a process can issue into the kernel.
Because the Linux kernel exposes over 400+ system calls—many of which are rarely used by standard application code (such as reboot, kexec_load, bpf, mount, or keyctl)—any vulnerability in an unused syscall presents an unnecessary attack vector. By using seccomp, applications and container runtimes drastically shrink their kernel attack surface.
┌────────────────────────────────────────┐
│ User Space Application │
└───────────────────┬────────────────────┘
│ System Call
▼
┌────────────────────────────────────────┐
│ seccomp-bpf Hook Entry │
│ Inspects struct seccomp_data │
└───────────────────┬────────────────────┘
│
┌─────────────────────────┼────────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────────────┐ ┌───────────────────────┐ ┌───────────────────────┐
│ SECCOMP_RET_ALLOW │ │ SECCOMP_RET_ERRNO │ │ SECCOMP_RET_KILL_PROC │
│ Proceeds to Kernel │ │ Returns Fake Error │ │ Kernel Destroys │
│ Syscall Execution │ │ (e.g. EPERM, ENOSYS) │ │ Entire Process │
└───────────────────────┘ └───────────────────────┘ └───────────────────────┘
The Two Modes of seccomp
1. Strict Mode (SECCOMP_SET_MODE_STRICT)
Introduced in Linux 2.6.12, strict mode is a simple, binary restriction:
- The process is permitted to make only 4 system calls:
read(),write(),_exit(), andsigreturn(). - Calling any other system call (even
open(),fork(), orclose()) causes the kernel to immediately terminate the process withSIGKILL. - Primary use case: Grid computing, processing untrusted computational bytecode (e.g. rendering a single image tile or processing raw crypto blocks).
2. Filter Mode (SECCOMP_SET_MODE_FILTER / seccomp-bpf)
Introduced in Linux 3.5, filter mode allows applications to attach a Classic BPF (cBPF) program to the system call entry path.
For every system call, the kernel passes a struct seccomp_data to the filter:
struct seccomp_data {
int nr; /* System call number (__NR_openat, __NR_read, etc.) */
__u32 arch; /* CPU Architecture (e.g. AUDIT_ARCH_X86_64) */
__u64 instruction_pointer; /* Instruction pointer (CPU RIP) where syscall was invoked */
__u64 args[6]; /* The 6 64-bit system call arguments */
};
The PR_SET_NO_NEW_PRIVS Requirement
Before an unprivileged process can load a seccomp filter, it must call:
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
Why is this mandatory?
Without no_new_privs, an unprivileged user could write a malicious seccomp filter that alters the behavior of a setuid binary (like /usr/bin/sudo or /bin/passwd).
For example, an attacker could instruct seccomp to make setuid() return a fake success code (0) while silently blocking the actual privilege drop, leaving the setuid process running as root under attacker control.
Setting PR_SET_NO_NEW_PRIVS guarantees that the process and all future descendants can never gain elevated privileges across execve(), making it safe for unprivileged sandboxing.
Return Actions (SECCOMP_RET_*)
When the BPF filter evaluates a system call, it returns a 32-bit unsigned integer containing an action and optional error data:
| Return Action | Value | Behavior |
|---|---|---|
SECCOMP_RET_ALLOW |
0x7fff0000 |
The system call is allowed to execute normally. |
SECCOMP_RET_ERRNO |
0x00050000 |
Syscall is blocked; returns specified errno (e.g. EPERM, ENOSYS) to user space without executing. |
SECCOMP_RET_KILL_PROCESS |
0x80000000 |
Terminates the entire process immediately (all threads). |
SECCOMP_RET_KILL_THREAD |
0x00000000 |
Terminates only the thread that executed the blocked call. |
SECCOMP_RET_TRAP |
0x00030000 |
Sends a SIGSYS signal to the process with si_syscall set to the blocked call number. |
SECCOMP_RET_LOG |
0x7ffc0000 |
Allows the call to execute, but logs an audit message to the kernel log. |
SECCOMP_RET_TRACE |
0x7ff00000 |
Pauses execution and notifies a ptrace tracer (used for debugging and sandbox proxies). |
Practical Implementation: Writing a seccomp Filter with libseccomp
While raw BPF assembly instructions (struct sock_filter) can be written manually, modern production code uses libseccomp:
#include <seccomp.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
// 1. Initialize seccomp context: Default action is to ALLOW all calls
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);
if (!ctx) {
perror("seccomp_init failed");
exit(EXIT_FAILURE);
}
// 2. Add rule: Deny the 'chmod' and 'fchmodat' syscalls, returning EPERM
seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(chmod), 0);
seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(fchmodat), 0);
// 3. Load the filter into the kernel
if (seccomp_load(ctx) < 0) {
perror("seccomp_load failed");
seccomp_release(ctx);
exit(EXIT_FAILURE);
}
seccomp_release(ctx);
// 4. Test the restriction: chmod should fail with EPERM
if (chmod("/tmp/test.txt", 0777) == -1) {
perror("chmod blocked successfully"); // Prints "Operation not permitted"
}
return 0;
}
Production Use in Containers & Browsers
- Docker Default seccomp Profile:
- Out of the ~400+ Linux system calls, Docker's default profile blocks approximately 44 syscalls (including
sys_chroot,kexec_load,acct,iopl,reboot, and rawbpfloading).
- Out of the ~400+ Linux system calls, Docker's default profile blocks approximately 44 syscalls (including
- Chromium Sandbox:
- Google Chrome sandboxes its rendering engine using seccomp-bpf, allowing renderers to only read, write, and interact with IPC pipes back to the main browser broker process.
Frequently Asked Questions (FAQ)
Why must seccomp filters verify the CPU architecture (arch)?
Because on multi-architecture systems (like x86_64 systems supporting 32-bit x86 binaries), the same numerical syscall value corresponds to completely different system calls across ABIs. If a seccomp filter does not check data->arch == AUDIT_ARCH_X86_64, an attacker could issue a 32-bit int 0x80 or sysenter call where a seemingly harmless syscall number maps to a dangerous 32-bit call, bypassing the filter.
What is the difference between seccomp and Landlock?
seccomp filters system calls by number and arguments, whereas Landlock operates at the Linux Security Module (LSM) layer, controlling access to specific filesystem directory hierarchies and network ports. While seccomp can block the open() syscall entirely, it cannot inspect deep directory paths efficiently; Landlock specializes in path-based file access without disabling the open() syscall.
What happens when an application receives SIGSYS from seccomp?
SIGSYS indicates that the process attempted an unauthorized system call where the seccomp filter returned SECCOMP_RET_TRAP. The signal delivers a siginfo_t structure containing si_syscall (the blocked syscall number) and the instruction pointer where the trap occurred, allowing custom debuggers or supervisors to record the violation.
Related Topics
- Linux Security Architecture: Discretionary vs. Mandatory Access Control.
- POSIX Signals - SIGSYS: Handling bad system call traps.
- Landlock LSM: Unprivileged filesystem and network sandboxing.
- Linux Capabilities: Fine-grained privilege division.
