arthrop0d

Brief articles on varied topics.

Linux Seccomp-BPF System Call Filters

computer securitylinux

Seccomp-BPF is a Linux kernel mechanism that filters a process's system calls—requests from user programs to the kernel—using small programs written in Berkeley Packet Filter (BPF) instructions. A filter can allow a call, reject it with an error, turn it into a signal, or terminate the process.

The problem seccomp solves

A normal Linux process can potentially ask the kernel for a very broad set of operations: opening files, creating sockets, changing memory mappings, starting other processes, loading kernel-related resources, and more. Unix permissions and Linux capabilities limit some of those operations, but a compromised process may still have many system calls available to exploit a kernel bug or to cause damage within its existing permissions.

Seccomp provides a narrower boundary: which kernel entry points this process may use. A service that only reads a configuration file and handles network traffic might not need calls for mounting filesystems, changing namespaces, or loading programs. Blocking those calls reduces the kernel-facing attack surface.

Without seccomp, the kernel generally dispatches any system call that passes ordinary permission and validity checks. With a filter installed, the kernel evaluates that filter whenever the process attempts a system call, before the call is carried out.

You may already have encountered seccomp indirectly. Container runtimes and sandboxing tools commonly install profiles, and a blocked operation may appear as Operation not permitted, Bad system call, or a process exit caused by SIGSYS.

When the filter runs

A process installs seccomp using the seccomp system call or the older prctl interface. Installing a filter is normally a one-way restriction: the process cannot remove it or replace it with a less restrictive filter later. Child processes inherit the restriction, and it remains across execve, the system call that replaces a process image with another program.

To prevent a process from gaining privilege and then installing a misleading filter, Linux usually requires the process to set no_new_privs first. This is a per-process promise that future operations will not grant additional privileges. A privileged process can install a filter without that step, subject to the relevant privilege checks.

After installation, the rough path for a system call is:

  1. User code enters the kernel with a system call number and arguments.
  2. Seccomp evaluates the installed BPF filter or filters.
  3. The filter's result determines whether the kernel continues, returns an error, sends a signal, or terminates the process.
  4. Only an allowed call proceeds to normal system-call implementation and the other security checks.

The filter is therefore not a wrapper around a C library function. It also applies when code invokes the kernel directly, bypasses the usual library, or uses a different library implementation.

What the filter can inspect

Seccomp does not give BPF arbitrary access to the process. The filter receives a fixed record describing the attempted call, commonly represented by struct seccomp_data. Its useful fields include:

The filter can compare these values and make decisions. For example, it can allow read and write, permit socket only for a particular address family, or reject a call when a numeric flag contains an unsafe option.

A crucial limitation is that the arguments are values, not safely readable objects. If an argument is a pointer to a pathname, buffer, or structure, seccomp can inspect the pointer address but cannot use the filter to read and compare the pointed-to string or contents. It also cannot generally reason about what an ioctl request will do internally. Policy that depends on file names or complex pointed-to data belongs in another layer, such as filesystem permissions, an LSM, or application code.

Actions: allow, deny, trap, and kill

A BPF filter returns an action, optionally carrying a small data value. The main actions explain the behavior seen by applications:

Action Effect
SECCOMP_RET_ALLOW The system call continues normally. It still must pass ordinary kernel checks such as permissions and argument validation.
SECCOMP_RET_ERRNO The call is blocked and the process receives the selected error number, as though the kernel implementation had returned that error.
SECCOMP_RET_TRAP The call is blocked and the thread receives SIGSYS, a signal specifically associated with an invalid system call. The signal handler can inspect signal information, but continuing requires great care.
SECCOMP_RET_KILL_THREAD The thread making the call is terminated immediately.
SECCOMP_RET_KILL_PROCESS The entire process, including its threads, is terminated.

ERRNO is useful when a program can handle a denied operation gracefully. For example, returning EPERM makes a call look like a permission failure. TRAP is useful for diagnostics or tightly controlled emulation, but it is not a normal exception mechanism. Kill actions are appropriate when attempting the call should be treated as a definitive policy violation.

Linux also has actions for logging, tracing through ptrace, and handing the decision to a user-space supervisor through seccomp user notification. Those are extensions of the same decision point, not ways for an ordinary filter to inspect arbitrary process state.

Filters constrain; they do not grant permission

A filter is an allow-list or deny-list for system-call behavior, not a replacement for Linux authorization. SECCOMP_RET_ALLOW means only “seccomp does not block this call.” The process may still lack permission to open a file, bind a port, or change a resource.

Multiple filters can be layered. Each installed filter evaluates the call, and Linux uses the most restrictive resulting action according to seccomp's action precedence. A later filter therefore cannot undo an earlier restriction.

The strongest practical designs start with the smallest required set of calls, test the application under that profile, and treat arguments carefully. Seccomp-BPF is powerful because it runs at the kernel boundary, but its scope is precise: it limits system calls and selected numeric arguments, while other mechanisms must enforce file, network, memory, and privilege policy.

← All articles