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.
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 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 has two seccomp restriction modes.
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 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.
A seccomp filter runs over a read-only structure conceptually equivalent to:
The fields contain:
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.
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.
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:
| Action | Effect |
|---|---|
KILL_PROCESS | Terminate every thread in the process |
KILL_THREAD | Terminate only the calling thread |
TRAP | Do not execute the call; deliver SIGSYS to the thread |
ERRNO | Do not execute the call; return a chosen error |
USER_NOTIF | Send the request to a user-space supervisor |
TRACE | Notify an attached ptrace tracer |
LOG | Log the action and allow the call |
ALLOW | Execute 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.
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:
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 FiltersAn 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.
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.
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.
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.
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:
Seccomp can deny all future openat() calls or constrain scalar flags. It is not a pathname access-control language.
Loading simulation...
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.
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.
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.
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:
The goal is the smallest dependable interface, not the shortest rule file.
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:
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.
getpid() with libseccompThe 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.
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.
/procFor 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.
A violation can appear as:
EPERM, EACCES, or another configured errorSIGSYS signalFirst 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.
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.
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.
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:
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.
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.
5 quizzes