Observe / Harden · Linux userspace / kernel ABI

seccomp and capability boundaries

Keeps syscall allowlists distinct from privilege decomposition, and explains the order of no_new_privs, filter installation, and acquiring fds in advance.

Series
37 / 38
Build
cc -std=c17 -Wall -Wextra -O2 seccomp_strict.c -o seccomp_strict
Run
./seccomp_strict
Kernel
Linux 6.18.37 LTS

Does dropping root privilege automatically reduce a process's syscall attack surface?

UIDs and capabilities participate in kernel permission checks that decide which privileged operations are allowed. A seccomp filter evaluates the syscall number and some raw arguments, then handles the call with allow, errno, trap, kill, notify, or another action. The mechanisms complement each other but are not substitutes.

Because installing a filter can block a required open/socket syscall, acquire resources first and pass fds or use a broker. no_new_privs prevents new privilege through exec and is a prerequisite for unprivileged filter installation.

Structure diagram

Figure 1. Overlapping layers of privilege restriction on a service process
Pre-opened resourceslistener · config fd · log fd
UID/GID + capabilitieseffective · permitted · bounding · ambient
no_new_privsblock privilege gain through exec
seccomp BPFarch · syscall nr · raw args → action
LSM / namespace / mount policyadditional object-level permission

Capabilities and seccomp answer different questions. Capabilities govern permission for operations; seccomp restricts syscall entry itself.

Call path

Figure 2. From userspace code to observable results
initialize acquire config/fds/resources
drop groups/uid/caps reduce privilege
no_new_privs block exec privilege
seccomp filter syscall allowlist
service loop run in restricted state

Separate the initialization phase from the steady-state syscall set after restriction. A filter validates architecture and ABI, and starts from default deny while measured required calls are added.

Figure 3. Major points along the kernel-internal path
secure_computing syscall entry hook
BPF filter inspect nr/arch/args
action ALLOW/ERRNO/KILL
capable hook operation permission
LSM additional policy

This diagram is not for memorizing function names. Follow which return value, file descriptor, memory mapping, or wait queue is passed to the next stage.

Linux 6.18.37 LTS source locations

Go beyond the glibc function to the files where the syscall implementation meets kernel objects. Each link points to the original file at the same tag.

FileFunction / structureWhat to inspect
kernel/seccomp.c do_seccomp(), seccomp_run_filters() Install a filter and choose an action at syscall entry
kernel/capability.c cap_capable(), capable() The generic path for capability permission checks
kernel/sys.c prctl_set_seccomp(), set_user() Change process attributes and credentials

Complete runnable example

The code below is not pseudocode with explanatory lines omitted. It is a minimal example that can be built and run as a file.

Buildcc -std=c17 -Wall -Wextra -O2 seccomp_strict.c -o seccomp_strict
01#define _GNU_SOURCE
02#include <linux/seccomp.h>
03#include <stdio.h>
04#include <sys/prctl.h>
05#include <sys/syscall.h>
06#include <unistd.h>
07
08int main(void)
09{
10    const char message[] = "strict mode allows write\n";
11    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0)
12        return 1;
13    if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT) < 0)
14        return 1;
15
16    write(STDOUT_FILENO, message, sizeof(message) - 1);
17    syscall(SYS_exit, 0);
18    __builtin_unreachable();
19}

Code notes

Source line 10const char message[]

Prepares required data before strict mode is installed. After the filter, calls such as malloc, stdio, and open may not be on the allowlist.

Source line 11PR_SET_NO_NEW_PRIVS

A one-way attribute preventing this thread and later children from acquiring new privilege from setuid/file capabilities through exec.

Source line 13SECCOMP_MODE_STRICT

A fixed mode allowing only read, write, _exit, and sigreturn. A practical service uses BPF filter mode with libseccomp.

Source line 16write(STDOUT_FILENO

Uses a syscall on the strict allowlist. Be careful with APIs whose glibc wrappers internally call additional syscalls.

Source line 17syscall(SYS_exit, 0)

The glibc _exit wrapper may use exit_group, but strict mode permits only SYS_exit. The raw syscall terminates the current single thread.

Detailed behavior

01

A syscall number cannot inspect pointer contents

Classic seccomp BPF can see raw syscall-argument values, but cannot safely dereference pathname strings or mutable memory behind pointers. Combine open-path policy with preopened dirfds, namespaces, a broker, or an LSM.

For 64-bit arguments, handle endianness and word splitting correctly for each architecture.

02

There are several kinds of capability set

The permitted, effective, inheritable, bounding, and ambient sets participate in exec rules. Do not assume one capset call removes every future privilege path.

Record supplementary groups, securebits, user namespaces, and file capabilities as well.

03

Filter deployment needs an observation mode

An allowlist omission may appear only on a rare error path in a normal workload. Collect real syscalls with SECCOMP_RET_LOG, audit, and a test corpus, including per-architecture ABIs.

SIGSYS/core information includes the syscall number and architecture and helps diagnose failures.

Objects and lifetimes

ObjectCreation and releaseValues to inspect
seccomp filterInstalled on a thread and synchronizable across a thread group with TSYNC; it cannot be removedBPF program, action precedence
credential/cap setsCopied by fork and changed by set*id/capset/execeffective/permitted/bounding/ambient
pre-opened fdAcquired before restriction and used like a capability by the service loopaccess mode, CLOEXEC, owner

Failure conditions and common misconceptions

Observed symptomLikely causesHow to verify
SIGSYS or immediate terminationAn allowlist omission or architecture mismatchaudit log, siginfo syscall/arch
More privilege remains than expectedCapability bounding/ambient sets or groups were not cleared/proc/PID/status Cap*, Groups
Initialization fails after the filterA required resource was opened too lateSeparate the syscall timeline into phases

Verify it yourself

  1. Call getpid after strict mode and inspect process termination from an isolated shell.
  2. Use a libseccomp filter permitting roughly read/write/exit/futex and add missing syscalls observed through strace/audit.
  3. Record capability sets before and after UID change in a table using capsh --print and /proc/self/status.
Run./seccomp_strict
Tracestrace -e trace=prctl,write,exit_group ./seccomp_strict

Primary sources