Seccomp profiles
Seccomp is a filter the kernel runs on every system call a process makes. It
can let the call through, make it fail with an error code, or kill the
process. A task installs its filters as its last restriction before exec,
so your command runs under them from its first instruction, and so do its
children.
Choosing sandbox profiles says which profile to
use when; this page shows how they are built.
Stacked filters, one answer
Section titled “Stacked filters, one answer”A task installs up to four filters, in this order:
- The x32 guard, on x86_64 only. The x32 ABI reaches the same kernel
functions under different numbers (with bit
0x40000000set), which would slip past rules written for the normal numbers. Any such call kills the process. - The unavailable filter.
clone3and the threeio_uringcalls fail withENOSYS. - The denylist of the profile, with its argument rules. A match kills
the process (
SECCOMP_RET_KILL_PROCESS). - The allowlist, for
v2profiles only. A call it does not list fails withENOSYS.
The kernel runs every installed filter on every call and applies the most severe answer, so the order in which they were installed does not matter for the result: a kill beats an error, and an error beats allowing the call.
- x86_64, and the call number has the x32 bit set?yeskilled SIGSYS:
policy_violation - On the profile's denylist, or an argument rule matches (a namespace flag to
clone, a socket family,TIOCSTI)?yeskilled SIGSYS:policy_violation -
clone3,io_uring_setup,io_uring_enterorio_uring_register?yesENOSYS the call fails, the process goes on - A
v2profile, and the call is not on its allowlist?yesENOSYS the call fails, the process goes on - allowed the kernel runs the call
clone3 under a native profile is both on the denylist and in the unavailable filter, and is killed. A kill and an error are different things
Section titled “A kill and an error are different things”- Killed.
SECCOMP_RET_KILL_PROCESSends the process that made the call at once withSIGSYS. It is notSECCOMP_RET_TRAP, so a signal handler cannot catch it and carry on. When that process is the task’s own (the command Faber started), the result isoutcome: policy_violation,termination_signal: 31,exit_code: 159. When it is a child, its parent sees a child that died ofSIGSYS, and the task ends however the parent reacts to that. ENOSYS. The call returns -1 witherrnoset toENOSYS(“Function not implemented”), exactly as on a kernel too old to have it. Programs usually have a fallback: glibc retriesclone3asclone, and libuv (Node.js) falls back fromio_uringtoepoll. Underv2, an unlisted call that a program cannot do without shows up as a toolchain error rather than as a kill.
clone3 gets ENOSYS rather than being allowed because its flags are in a
structure in memory, which seccomp cannot read. Answering ENOSYS makes
callers fall back to clone, whose flags it can check.
The four profiles
Section titled “The four profiles”Adds processes and threads, sockets of four families, System V and
POSIX IPC, inotify, ownership and extended attributes, ID changes
within the one mapped ID, locked memory, seccomp and Landlock.
For a toolchain that needs a call the allowlist lacks, while it is being added.
What a single-threaded program needs: memory, files, time, signals, its own process state.
The v1 counterpart of native_v2.
- Faber and the kernel (trusted)
native ones also kill clone, clone3, socket and socketpair. The counts are allowlist entries, and both v2 lists also allow personality, with an argument rule, and mseal. The v1 profiles are refused unless the operator adds them to ALLOWED_SANDBOX_PROFILES. What every profile kills
Section titled “What every profile kills”acct, add_key, bpf, delete_module, fanotify_init, finit_module,
fsconfig, fsmount, fsopen, fspick, init_module, kcmp,
kexec_file_load, kexec_load, keyctl, mount, mount_setattr,
move_mount, name_to_handle_at, open_by_handle_at, open_tree,
open_tree_attr, perf_event_open, pidfd_getfd, pivot_root,
process_vm_readv, process_vm_writev, ptrace, quotactl, quotactl_fd,
reboot, request_key, setns, swapoff, swapon, syslog, umount2,
unshare and userfaultfd: kernel modules, kexec, mounts and the whole new
mount API, namespaces, keyrings, BPF, perf, other processes’ memory, handles
that bypass path checks, swap, quotas, accounting, the kernel log and reboot.
The native profiles add clone, clone3, socket and socketpair, and on
x86_64 fork and vfork.
Argument rules
Section titled “Argument rules”| Call | Rule | Profiles |
|---|---|---|
clone | Any namespace flag (CLONE_NEWCGROUP, NEWIPC, NEWNET, NEWNS, NEWPID, NEWTIME, NEWUSER, NEWUTS) kills | compile |
socket | A family other than AF_UNIX, AF_INET, AF_INET6 and AF_NETLINK kills, and so does netlink with a protocol other than NETLINK_ROUTE | compile |
ioctl | TIOCSTI (0x5412) and TIOCLINUX (0x541c) kill, compared as 32-bit values so that setting high bits does not get around the rule | all |
personality | Only a query (0xffffffff) and the Linux, 32-bit and uname-2.6 personalities pass; anything else is ENOSYS | v2 |
The operator sets the default profile and the profiles a request may choose
(DEFAULT_SANDBOX_PROFILE, ALLOWED_SANDBOX_PROFILES). The API fills the
default into every task that does not name one before the request runs, so
the profile is always explicit, in the sandbox and in the cache key.
How the allowlists were traced
Section titled “How the allowlists were traced”The v2 lists are not guesses. They come from watching real toolchains:
One per language (Bun, C, C++, Deno, .NET, Go, Java, Node.js, PHP,
Python, Ruby, Rust) plus a shell script: read stdin, sort, sum in a
second thread, write and read back files in the workspace and /tmp,
start a child where the language usually does.
As UID 65534, without network and without any seccomp profile, so that nothing is refused and the trace shows what the program asks for.
The traced set, plus the rest of the ordinary interface a program may reach for (vectored I/O, timers, signal queues, extended attributes, System V IPC), minus calls that reach beyond the task.
Every workload under compile_v2, the single-threaded C, C++ and Rust
programs under native_v2. All pass except .NET, whose SDK fails
before building in a plain container too.
- Faber and the kernel (trusted)
- Outside Faber
- Starts or calls
- Carries data
Left out on purpose, though Docker’s default profile allows them: sync and
syncfs (writeback of every filesystem on the host), Linux AIO
(io_setup and friends, whose contexts are one host-wide counter),
cachestat (a side channel on what other requests read from the shared
toolchain files), process_madvise and process_mrelease (other processes’
memory), vmsplice and remap_file_pages. A unit test keeps them out.
The full method, the per-language traces and the argument values seen are in
crates/faber-runtime/seccomp/README.md,
along with how to add a call when a new toolchain needs one.
x86_64 and ARM64
Section titled “x86_64 and ARM64”ARM64 never had the old system calls that x86_64 keeps for compatibility, so its lists are shorter for the same programs:
| x86_64 | ARM64 | |
|---|---|---|
native_v2 allowlist | 172 calls | 142 calls |
compile_v2 allowlist | 277 calls | 241 calls |
x86_64-only calls in CORE (arch_prctl and older forms such as open, stat, lstat, poll, select, pipe, dup2, mkdir, rename, epoll_wait, alarm, time; 32 in all) | allowed | do not exist; ARM64 uses the *at forms, and names two calls differently (fstatat, sync_file_range2) |
fork, vfork, inotify_init, mknod, chown, lchown | in compile_v2 | do not exist |
| x32 guard | installed | no x32 ABI |
native denials | clone, clone3, socket, socketpair, fork, vfork | the same without fork and vfork |
Each list entry is a typed system call name for the architecture being built, so a name that does not exist there fails the build instead of silently allowing the wrong number.
Where this lives in the code
Section titled “Where this lives in the code”crates/faber-runtime/src/runtime/seccomp.rs: the lists, the argument rules, the four filters and their tests.crates/faber-runtime/src/task.rs:SandboxProfile.crates/faber-runtime/src/runtime/core.rs:child_setup_security, which installs the filters last, and theSIGSYSoutcome.crates/faber-runtime/seccomp/: the trace workloads,trace.sh,summarize.pyandvalidate.sh.crates/faber-api/src/handlers/execute.rsandsrc/config.rs: the default and allowed profiles.