Skip to content

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.

A task installs up to four filters, in this order:

  1. The x32 guard, on x86_64 only. The x32 ABI reaches the same kernel functions under different numbers (with bit 0x40000000 set), which would slip past rules written for the normal numbers. Any such call kills the process.
  2. The unavailable filter. clone3 and the three io_uring calls fail with ENOSYS.
  3. The denylist of the profile, with its argument rules. A match kills the process (SECCOMP_RET_KILL_PROCESS).
  4. The allowlist, for v2 profiles only. A call it does not list fails with ENOSYS.

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.

  1. x86_64, and the call number has the x32 bit set?
    yes
    killed SIGSYS: policy_violation
  2. On the profile's denylist, or an argument rule matches (a namespace flag to clone, a socket family, TIOCSTI)?
    yes
    killed SIGSYS: policy_violation
  3. clone3, io_uring_setup, io_uring_enter or io_uring_register?
    yes
    ENOSYS the call fails, the process goes on
  4. A v2 profile, and the call is not on its allowlist?
    yes
    ENOSYS the call fails, the process goes on
  5. allowed the kernel runs the call
The checks in order of severity, which is how the kernel combines the filters. clone3 under a native profile is both on the denylist and in the unavailable filter, and is killed.
  • Killed. SECCOMP_RET_KILL_PROCESS ends the process that made the call at once with SIGSYS. It is not SECCOMP_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 is outcome: policy_violation, termination_signal: 31, exit_code: 159. When it is a child, its parent sees a child that died of SIGSYS, and the task ends however the parent reacts to that.
  • ENOSYS. The call returns -1 with errno set to ENOSYS (“Function not implemented”), exactly as on a kernel too old to have it. Programs usually have a fallback: glibc retries clone3 as clone, and libuv (Node.js) falls back from io_uring to epoll. Under v2, 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.

  • Faber and the kernel (trusted)
Every profile has the denylist below; the 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.

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.

CallRuleProfiles
cloneAny namespace flag (CLONE_NEWCGROUP, NEWIPC, NEWNET, NEWNS, NEWPID, NEWTIME, NEWUSER, NEWUTS) killscompile
socketA family other than AF_UNIX, AF_INET, AF_INET6 and AF_NETLINK kills, and so does netlink with a protocol other than NETLINK_ROUTEcompile
ioctlTIOCSTI (0x5412) and TIOCLINUX (0x541c) kill, compared as 32-bit values so that setting high bits does not get around the ruleall
personalityOnly a query (0xffffffff) and the Linux, 32-bit and uname-2.6 personalities pass; anything else is ENOSYSv2

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.

The v2 lists are not guesses. They come from watching real toolchains:

  • Faber and the kernel (trusted)
  • Outside Faber
  • Starts or calls
  • Carries data
Traced on 2026-10-18 on x86_64, Linux 6.12.44. ARM64 has no trace of its own: its lists are the same generic calls, and the ARM64 CI runs the security suite natively.

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.

ARM64 never had the old system calls that x86_64 keeps for compatibility, so its lists are shorter for the same programs:

x86_64ARM64
native_v2 allowlist172 calls142 calls
compile_v2 allowlist277 calls241 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)alloweddo not exist; ARM64 uses the *at forms, and names two calls differently (fstatat, sync_file_range2)
fork, vfork, inotify_init, mknod, chown, lchownin compile_v2do not exist
x32 guardinstalledno x32 ABI
native denialsclone, clone3, socket, socketpair, fork, vforkthe 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.