AlgoMaster Logo

Seccomp and Syscall Filtering

34 min readUpdated August 7, 2026
Listen to this chapter
Unlock Audio

A backend service may need only a small fraction of the system calls that Linux exposes.

An image worker might read from already opened files, allocate memory, synchronize threads, and write a result. It does not need to load kernel modules, configure network interfaces, create a new filesystem, or reboot the machine.

Ordinary permissions may already deny many of those operations. The process can still reach their kernel entry points, however, and every reachable entry point contributes to the kernel attack surface.

Seccomp, short for secure computing, lets a Linux thread restrict which system calls it and its descendants may make.

The filter sits on the path of every system call the thread makes. Which of the four outcomes applies is decided per call, from the syscall number and its arguments.

Seccomp is most useful as a least-privilege mechanism. Even if an attacker gains control of application code, the attacker inherits the process's restricted system-call interface rather than the complete Linux API.

System Calls as the Kernel Attack Surface

User-mode instructions cannot directly modify page tables, configure devices, or manipulate kernel data structures. Programs request kernel work through system calls.

Different system calls expose different kernel subsystems:

A process's UID, groups, capabilities, and other access-control rules determine whether a requested operation is authorized. Seccomp adds an earlier question:

Is this thread allowed to attempt this kind of system call at all?

If a filter rejects mount(), the kernel does not enter the ordinary mount implementation and then check whether the caller has sufficient privilege. The seccomp action is applied at system-call entry.

Reducing the reachable set matters because kernel code can contain bugs. A process that cannot invoke a subsystem's system calls has fewer paths through which to reach that subsystem.

Seccomp also constrains legitimate but dangerous behavior. A compromised parser may have ordinary permission to start another program, but an execve() restriction can remove that operation from the parser's available interface.

Seccomp Filtering of Operations, Not Objects

Seccomp does not replace file permissions or object-level access control.

A filter can distinguish read() from mount(). It does not normally know that file descriptor 7 refers to a customer database while descriptor 8 refers to a public image.

Likewise, a filter can reject openat(), but it cannot revoke descriptors that were opened before the filter was installed:

A descriptor opened before filtering survives a filter that later denies openat(). New pathname opens are denied, while the existing descriptor remains usable with read() and write().

This is often exactly what a sandbox needs. A trusted launcher can open the worker's input and output, pass those descriptors to the worker, and then install a filter that prevents the worker from opening anything else.

The descriptor's access mode and the filesystem policy control the object. Seccomp controls which kernel operations the worker can request.

Linux Strict and Filter Modes

Linux has two seccomp restriction modes.

Strict mode

Strict mode permits only:

Any other system call terminates the calling thread. Strict mode is intentionally tiny and can suit specialized computation that communicates through prearranged descriptors.

It is too restrictive for most ordinary applications. Dynamic runtimes, memory allocators, threading libraries, logging code, and even normal process-exit wrappers can use calls outside the strict set.

Strict mode is fixed; it does not accept a custom policy.

Filter mode

Filter mode attaches a program that examines each attempted system call and returns an action.

The kernel exposes this through seccomp(SECCOMP_SET_MODE_FILTER, ...) and, with fewer flag options, through prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, ...).

Filter mode supports application-specific policies such as:

Most practical references to “using seccomp” mean filter mode.

The Small, Fixed Seccomp Filter Input

A seccomp filter runs over a read-only structure conceptually equivalent to:

The fields contain:

  • The system call number
  • An architecture identifier describing the calling convention
  • The address of the instruction that attempted the call
  • Up to six raw system-call argument values

The filter runs before the system call itself. It does not receive the result because no kernel operation has occurred yet.

The kernel executes the policy as a classic Berkeley Packet Filter, or classic BPF, program. This is a small, verifiable instruction language. It is distinct from loading a general-purpose eBPF program.

Handwriting classic BPF jump instructions is error-prone. Libraries such as libseccomp let applications describe rules using system-call names and argument comparisons, then generate the low-level filter program.

Architecture in Seccomp Security Decisions

System call numbers are not globally unique. Number N can identify different operations under different architectures or application binary interfaces.

Some processors support multiple calling conventions on the same kernel. An x86-64 system, for example, may also support 32-bit and x32 conventions.

A raw policy that checks only:

can accidentally evaluate a different operation than its author intended.

A correct low-level filter first validates the architecture and then interprets the number and arguments according to that calling convention:

The architecture check has to come first. Syscall numbers mean different things under a different ABI, so interpreting the number before confirming the architecture would filter the wrong calls.

The x32 ABI requires additional attention because it shares an architecture identifier with x86-64 and marks its calls with a bit in the system call number.

High-level policy tools handle much of this bookkeeping, but the deployment must still decide which architectures are allowed. A service that needs only the native ABI should not expose alternative syscall tables unnecessarily.

Seccomp Filter Actions

A filter does not return a simple boolean. It returns an action, sometimes with a small data value such as an error number.

The important actions, in decreasing precedence, are:

ActionEffect
KILL_PROCESSTerminate every thread in the process
KILL_THREADTerminate only the calling thread
TRAPDo not execute the call; deliver SIGSYS to the thread
ERRNODo not execute the call; return a chosen error
USER_NOTIFSend the request to a user-space supervisor
TRACENotify an attached ptrace tracer
LOGLog the action and allow the call
ALLOWExecute the system call normally

KILL_PROCESS is generally safer than KILL_THREAD for a multithreaded application. Killing one thread can leave locks held and shared state inconsistent while other threads continue.

ERRNO lets a denied call look like an ordinary kernel failure:

This can make gradual rollout easier, but the application must handle the error safely. Some programs react to one failure by trying a different, potentially less desirable operation.

TRAP supplies diagnostic information with SIGSYS, including the attempted system call and architecture.

LOG is observational: the call still executes. It is useful for learning which operations occur, but it does not enforce denial.

The actions supported by the running kernel, already ordered by precedence, are visible through:

Newer actions should be checked for kernel support before a policy relies on them.

Monotonic Policy Tightening with Multiple Filters

A thread can have more than one seccomp filter.

For every attempted system call, the kernel evaluates all attached filters. It applies the returned action with the highest precedence.

Suppose the first filter allows openat() and a later filter returns ERRNO for it:

Adding a new ALLOW rule cannot override an older filter that denies the call with a higher-precedence action.

Filters cannot be removed or relaxed. A thread can add another layer only if its current filter still permits the operation used to install that layer.

This supports staged restriction:

  1. At program startup, a broad filter is needed for initialization.
  2. The program opens resources and creates workers.
  3. It installs an additional, narrower runtime filter.

The effective policy becomes monotonically stricter.

Stacking has an evaluation cost because every filter is still run. Prefer one well-designed policy when lifecycle phases do not require separate layers.

no_new_privs for Unprivileged Filters

An unprivileged thread must set the Linux no-new-privileges flag before installing a seccomp filter:

The alternative is to hold CAP_SYS_ADMIN in the relevant user namespace, which ordinary services should not receive merely to install a filter.

Once set, no_new_privs is inherited across process creation and execution and cannot be unset. Its central promise is that a later execve() cannot grant authority the caller did not already possess through mechanisms such as setuid or file capabilities.

This requirement prevents a subtle attack. Without it, an unprivileged process could install a malicious filter that lies about the result of a privilege-dropping call and then execute a privileged program under that filter.

The safe sequence for an ordinary process is:

Set no_new_privs, install the seccomp filter, and continue with a permanently restricted process tree.

no_new_privs is not itself a syscall filter and does not remove existing authority. It makes unprivileged, persistent filtering safe to install.

Filters Persist Across Process Creation and Execution

If fork() or clone() is allowed, a child inherits its parent's seccomp filters.

If execve() is allowed, the filters remain active while the process image is replaced:

The launcher installs the filter, calls execve(), and the new program starts under the inherited filter.

This lets a small launcher establish policy before untrusted application code runs.

It also creates a deployment hazard. A dynamically linked executable needs the dynamic loader to open libraries, map segments, inspect metadata, and apply memory protections. A pre-exec filter that blocks those calls can prevent the program from starting.

The filter must cover the complete execution environment, not only the application's steady-state code.

An executed setuid program or file-capability executable also cannot escape an inherited seccomp filter. The no_new_privs requirement ensures that an unprivileged caller cannot combine filter inheritance with a new privilege grant.

Synchronized Filters for Multithreaded Processes

Seccomp state belongs to threads. One thread can otherwise become filtered while another thread in the same process retains a broader system-call interface.

Installing a filter before creating additional threads avoids this mismatch.

When that is not possible, the SECCOMP_FILTER_FLAG_TSYNC flag asks the kernel to synchronize the new filter across all threads in the process:

Synchronization can fail if another thread already has an incompatible filter tree or is in strict mode. The caller must check the installation result rather than assuming every thread was updated.

libseccomp can request thread synchronization through a filter attribute when the installed library and kernel support it.

A multithreaded sandbox is incomplete if only the setup thread is restricted. Verification should inspect the actual runtime design and install the policy before untrusted work begins whenever possible.

Scalar Values for Argument Filtering

A filter can compare raw argument values. This is useful when an argument has stable, scalar meaning.

Examples include:

Arguments appear as 64-bit values in seccomp_data, even when the eventual kernel implementation uses a narrower type. The kernel may truncate an argument after the seccomp check. A raw filter must interpret signedness, width, masks, and calling convention exactly as the syscall does.

libseccomp provides comparison operators that reduce low-level mistakes, including masked comparisons for flag arguments.

Argument filtering still needs a complete view of equivalent operations. Blocking one flag combination on mmap() is weak if another allowed syscall can create an equivalent executable mapping.

Seccomp Limitations for Pathname Inspection

Many system-call arguments are pointers:

The filter sees the numeric pointer value. Classic seccomp BPF cannot dereference it and read the pathname string.

Even an imagined check that read process memory would face a race: another thread could change the string after the policy check but before the kernel used it.

This means a rule such as:

cannot be implemented safely as an ordinary seccomp BPF argument rule.

Use object-aware controls for object policy:

  • Filesystem ownership and permissions
  • Directory-based isolation
  • Pre-opened file descriptors
  • A trusted broker that opens approved objects
  • A mandatory access-control policy

Seccomp can deny all future openat() calls or constrain scalar flags. It is not a pathname access-control language.

Loading simulation...

Limits of File-Descriptor Filtering

A descriptor number is a small integer chosen from the process's descriptor table. The number can be closed and reused for a different object.

A rule that allows:

does not intrinsically mean “write only to the audit log.” It means “write to whichever object occupies descriptor slot 7 at that moment.”

Descriptor-number filtering can be useful in a tightly controlled process that cannot duplicate, close, replace, receive, or reopen descriptors. In a general application, it is brittle.

A stronger design passes only the descriptors the worker needs and prevents it from acquiring new ones. The kernel-managed access mode on each open descriptor then carries object-specific authority.

Seccomp controls operations on descriptor numbers; it does not attach a permanent semantic label to those numbers.

System Calls Hidden by Library Functions

Applications invoke library APIs, not always raw system calls.

A call written as:

may enter the kernel through openat() rather than an older open() system call.

Other common surprises include:

Some operations, such as reading time, can be served by a virtual shared object in user space without entering the kernel. The same library function may fall back to a real system call under different conditions.

This is why a policy derived only from source-code function names is unreliable. The relevant interface is the set of system calls emitted by the compiled program, its libraries, loader, runtime, and error paths.

Allowlist-Based Policy

A denylist allows every call except known dangerous ones:

This policy can miss an alternative syscall that performs similar work. It also permits new system calls added by a future kernel until the denylist is updated.

An allowlist denies every call except those required:

New and forgotten calls are denied automatically. This is why allowlists are generally more robust for a strong sandbox.

Allowlists have an operational cost. Libraries and runtimes evolve, uncommon error paths require extra calls, and the required set can differ by architecture. An overly narrow policy can turn an ordinary package update into a production failure.

The right response is disciplined policy maintenance, not an allow-everything fallback.

Building a Policy from Workload Phases

A service often needs more system calls during startup than during steady-state request handling.

Consider these phases:

The filter installation point determines which phases it must support.

A launcher-installed policy covers every phase, including the dynamic loader. It minimizes the unfiltered window but requires a broader allowlist.

An application-installed policy can wait until initialization finishes and apply a much narrower steady-state set. Code running before installation remains unrestricted by seccomp.

A staged design can install one baseline filter early and add a second restrictive filter before processing untrusted input.

Policy design should start from a threat model:

  • Which kernel subsystems should become unreachable?
  • Which descriptors and memory mappings already exist?
  • Can the process create children or execute new programs?
  • Which shutdown, signal, and error paths must remain functional?
  • Should a violation fail with an error or terminate the process?

The goal is the smallest dependable interface, not the shortest rule file.

Observing First, Then Test Beyond the Trace

strace can record the system calls made by an unfiltered Linux process:

The -f option follows created threads and processes. A summary view is available with:

Tracing provides a useful starting set, but it is not a proof of completeness. One run may miss:

  • Startup under a different locale or configuration
  • Failed file and network operations
  • Signal handling and shutdown
  • Memory-pressure behavior
  • Lazy library loading
  • Rare logging and recovery paths
  • CPU- or architecture-specific runtime behavior

Build tests that deliberately exercise those paths. Roll out a diagnostic action in a controlled environment before choosing a fatal production response.

A production allowlist should be reviewed when the runtime, C library, compiler, architecture, or major dependency changes.

Hands-On: Denying getpid() with libseccomp

The following Linux program uses libseccomp to allow only write(), exit(), and exit_group(). Every other system call receives EPERM.

After loading the filter, the program invokes getpid directly through syscall() and verifies that the kernel denied it:

Compile it on a Linux system with the libseccomp development files installed:

Run it:

Expected output is:

The program uses the raw SYS_getpid invocation so a user-space library optimization cannot avoid the system-call boundary.

It deliberately does not call seccomp_release() after the filter is active. Releasing library data is unnecessary before the immediate _exit(), and a general cleanup routine could require a system call that the tiny allowlist denies.

This policy is intentionally too small for a real service. Its purpose is to expose the sequence and the failure behavior clearly.

Using a Service Manager to Apply a Filter

A service does not have to contain seccomp setup code. A trusted service manager can install a filter before executing the application.

A systemd unit can include:

SystemCallArchitectures=native restricts the service to the native syscall ABI. SystemCallFilter=@system-service uses a predefined allowlist intended as a baseline for many services. SystemCallErrorNumber=EPERM returns an error instead of using systemd's default fatal action.

Inspect the group available on the current system:

System-call groups can change with the systemd version, kernel version, and architecture. Treat the group as maintained policy input, not a universal constant.

A generic group is usually broader than a carefully designed application-specific profile. It is a practical starting point and can be narrowed after testing the service's real lifecycle.

Returning EPERM is helpful during rollout, but a mature policy may use a fatal action for calls that indicate control-flow compromise. The failure strategy should match the workload.

Inspecting Seccomp State Through /proc

For process 12345, inspect:

Representative output is:

The Seccomp values mean:

Seccomp_filters, when exposed by the running kernel, reports the number of attached filters. NoNewPrivs: 1 confirms the irreversible execution restriction is set.

These fields show that filtering exists, not which syscalls are allowed. Reading or reconstructing an arbitrary process's full policy requires additional privileges and tooling.

For a systemd-managed service, inspect its effective configuration:

Always verify the running process. Editing a unit file does not change a process that has not been restarted under the new policy.

Diagnosing a Seccomp Violation

A violation can appear as:

  • A call unexpectedly returning EPERM, EACCES, or another configured error
  • A SIGSYS signal
  • A process disappearing when a fatal action is used
  • A service failing only after a runtime or library update

First identify the denied system call and ABI. Kernel audit or service logs may include a syscall number and architecture code. Resolve the number under the correct architecture rather than assuming the host's native table.

If the policy returns ERRNO, ordinary application logs may be the only visible symptom. Trace a comparable unfiltered execution or temporarily use a controlled diagnostic policy to find the missing call.

Next determine why the call occurred:

Do not automatically allow every observed call. An attempted call may be evidence that the process did something outside its intended contract.

When a filter works for one thread but not another, inspect installation timing and thread synchronization. When a program fails before main(), include dynamic-loader requirements or move the installation point.

Supervised Decisions Through User-Space Notification

Some policies need a trusted process to make a decision that static BPF cannot express.

With SECCOMP_FILTER_FLAG_NEW_LISTENER, filter installation returns a notification descriptor. A USER_NOTIF action sends matching syscall information to a supervisor, which can respond with a value, an error, or in carefully controlled cases allow the kernel to continue the original call.

The sandboxed thread never performs the operation itself. A more privileged process decides what happens and hands back only what it chooses to.

A supervisor can, for example, open an approved object itself and inject the resulting descriptor into the sandboxed process. This keeps object policy in a component with more context while the worker remains unable to call openat() directly.

Notification introduces protocol complexity. The requesting thread can exit, signals can interrupt the exchange, and pointer arguments can change while the supervisor examines process memory. The supervisor must validate notification IDs and avoid treating mutable pointed-to data as a stable security decision.

User notification is a broker mechanism, not permission for ordinary BPF filters to dereference pointers.

Seccomp vs. Complete Sandboxing

A process with a perfect syscall allowlist can still misuse authority it already holds.

It can read memory already mapped as readable. It can corrupt objects inside its writable address space. It can write sensitive data through an allowed, already open network socket. It can consume CPU in a loop without making any system calls.

Seccomp also does not decide which pathname is safe, which customer record belongs to a caller, or whether bytes sent through an allowed descriptor contain a secret.

A robust sandbox combines independent boundaries:

Each answers a different question. Seccomp's question is deliberately narrow: which kernel entry points may this thread invoke, with which inspectable scalar arguments?

That narrowness is a strength when the filter is used for its intended purpose and a weakness when it is mistaken for complete isolation.

A Backend Worker Design

Consider a service that receives uploaded images. A front-end process validates the request and sends a worker two descriptors:

Before decoding untrusted image bytes, the worker:

  1. Loads its libraries and configuration.
  2. Creates its thread pool.
  3. Receives or opens the required descriptors.
  4. Installs a synchronized seccomp allowlist.
  5. Begins decoding.

The runtime filter permits the calls required for descriptor I/O, memory management, thread synchronization, signals, time, and clean exit. It denies new file opens, network socket creation, process execution, and unrelated kernel administration.

If the decoder is compromised, the attacker can still manipulate the worker's writable memory and use allowed operations. The attacker cannot simply call openat() to search the filesystem or socket() to create a new outbound connection.

Existing descriptors remain important. If the worker inherited the front-end's connected network socket accidentally, blocking socket() would not prevent writes through that connection. Descriptor hygiene and syscall filtering must agree.

The exact allowlist depends on the decoder library, C library, threading runtime, architecture, and deployment. It should be measured, tested under failure conditions, and verified on the running worker.

Summary

Seccomp filter mode evaluates attempted Linux system calls before their normal implementations run. A filter sees the syscall number, architecture, instruction pointer, and raw argument values, then returns an allow, error, notification, signal, logging, or termination action.

Correct filters validate the syscall ABI and prefer allowlists. Classic seccomp BPF can compare scalar arguments but cannot safely inspect pointed-to pathnames or assign stable meaning to reused descriptor numbers.

Unprivileged installation requires no_new_privs. Filters persist across process creation and execve(), cannot be removed, and combine so that a later filter cannot relax an earlier restriction.

Seccomp state belongs to threads. Install filters before creating threads or synchronize them explicitly, and account for dynamic loaders, libraries, runtimes, error paths, and shutdown behavior.

libseccomp and service managers make policies easier to express, while /proc, tracing, and controlled diagnostics help verify them. Observation is a starting point; comprehensive testing is still required.

Seccomp is one layer of a sandbox. It reduces the reachable kernel interface but does not replace object permissions, capability reduction, descriptor hygiene, memory isolation, or application-level authorization.

Quiz

Seccomp and Syscall Filtering Quiz

5 quizzes