Capability Dropping for Sandboxed Process Hardening
Linux sandboxes need capability dropping to block privilege escalation through child processes.

Capability dropping for sandboxed process hardening is a separate, necessary layer on top of Landlock and seccomp, and skipping it leaves a residual escalation surface sitting inside the process itself, one that sandboxed AI-generated code can reach through ordinary descendant-process behavior.
Hardening gaps left by Landlock, seccomp, and network namespaces alone
An engineer who has wired up Landlock, attached a seccomp-bpf filter, and dropped the sandboxed process into an empty network namespace has every reason to believe the job is done. Those three primitives cover three real surfaces: Landlock restricts which filesystem paths a process can touch, seccomp restricts which syscalls it can issue, and the network namespace restricts what it can reach over a socket. None of them, individually or together, touch a fourth surface: the capability bits the process itself holds, and whether a child it spawns can inherit or acquire privilege the parent's policy never accounted for. A re-exec helper that applies Landlock, seccomp, and network isolation but never touches the capability sets, never sets no_new_privs, never zeroes RLIMIT_CORE, and never calls PR_SET_DUMPABLE=0 is a common implementation pattern, and the gap it leaves has been documented directly: sandbx issue #39, filed Sep 29, 2026, describes the resulting sandbox as narrower than it was documented to be, labeled both a security issue and an enhancement request in the same breath, because the fix touches both categories at once.
The mechanism to isolate is capability inheritance across descendant processes. A sandboxed child process that inherits live effective capabilities carries the same privilege surface as its parent, regardless of what the parent's Landlock rules or seccomp filter say about paths and syscalls. It is the mundane case: a process spawns a child, the child holds capability bits nobody thought to strip, and it uses them in a way the sandboxing policy never anticipated, since the policy was written to cover paths and syscalls but never capability bits. Containers share the host kernel with every other process on the box, so syscalls remain the primary channel through which a contained process can reach kernel-level vulnerabilities, and leaving capability bits populated only widens that channel for no operational benefit. Agent workloads make the gap concrete rather than theoretical, since they run code written by someone, or something, other than the operator, across codebases copied wholesale into an isolated environment the operator did not write and cannot fully audit line by line.
Capability sets under exec and user namespaces
Linux capabilities exist to break the old root-or-nothing model into discrete, individually grantable units of superuser privilege. CAP_NET_BIND_SERVICE lets a process bind low ports without full root. CAP_SYS_ADMIN covers a wide grab-bag of administrative operations. The mechanism that makes capabilities hard to reason about is not the individual bits but the five sets that track them through a process's lifetime, and zeroing one set while leaving another populated accomplishes close to nothing.
The effective set is what the kernel actually checks at the moment of a privileged operation. The permitted set is the ceiling from which effective capabilities can be raised, since anything sitting in permitted a process can grant itself into effective at will; a populated permitted set with an empty effective set is a temporarily quiet state, not a hardened one. The inheritable set determines what capabilities survive an execve into a new binary, but only conditionally, since the target binary's own file-based inheritable set has to carry the matching bit for the privilege to transfer. That conditional protection is weaker than it looks on paper, because file capability bits on disk are something an attacker controlling the workload's files can potentially manipulate. The ambient set is the one most re-exec designs overlook entirely: ambient capabilities cross an execve unconditionally for ordinary, non-setuid binaries, which makes it the quiet vector that survives exactly the kind of re-exec hardening step that zeroes effective and permitted but forgets ambient exists. The bounding set is the outlier with a different kind of power: it's the hard ceiling no execve can ever exceed, and once a bit is removed from the bounding set it cannot be restored to that process's bounding set again under any circumstance. That irreversibility is what makes bounding-set drops the most durable capability control available, and also the one with the strictest precondition for use.
Under execve, the kernel recomputes effective and permitted from some combination of these five sets plus whatever capability bits are attached to the target binary on disk. The practical upshot: leaving any bit sitting in permitted or ambient is not a safe state just because effective happens to read zero at the moment someone checks it. User namespaces complicate the picture further. A process that creates its own user namespace holds a full set of capabilities inside that namespace, scoped to it. This is why something like CAP_SETPCAP is available or unavailable depending on which namespace the process is currently sitting in, and why the sequence of namespace creation relative to capability dropping changes what's actually achievable.
Why namespace creation must precede the bounding-set drop
Dropping a bit from the capability bounding set requires CAP_SETPCAP, and in a re-exec helper, CAP_SETPCAP is only available inside a user namespace the process itself created. That single dependency dictates the entire sequencing: user namespace creation has to happen before the bounding-set drop is attempted, never after, and the network-denied path turns out to be the only context where the helper reliably holds that capability.
On the network-allowed path, the helper never enters its own user namespace in the first place, so it never holds CAP_SETPCAP, and any attempt to call PR_CAPBSET_DROP there returns EPERM. sandbx PR #43, merged Sep 29, 2026, encodes this constraint directly into the implementation: the bounding-set drop is attempted only on the network-denied path, where the helper has entered its own user namespace and holds CAP_SETPCAP within it, while the other four capability sets and the RLIMIT_CORE zeroing apply unconditionally on both paths regardless of network configuration. Does that asymmetry leave the network-allowed path meaningfully weaker? Not in practice, because with the other four sets already emptied and no_new_privs already set, a leftover bounding bit has nowhere to go: it cannot be converted into effective privilege through any execve the process might perform afterward. The residual risk from a best-effort bounding-set drop stays bounded.
The root-caller case raises the stakes on getting this sequencing right. Anthropic's sandbox-runtime PR #505, merged before Sep 10, 2026 and shipped in v0.0.76 released that same day, shows bubblewrap running as uid 0 drops all capabilities from the sandboxed command by default unless they are explicitly added back in, and separately that the seccomp helper's own nested user namespace requires CAP_SETFCAP in the process that creates it. A root caller, as a result, has to drop all capabilities in both of the runtime's sandbox modes and retain only CAP_SETFCAP for the duration the helper is running. Get that sequencing wrong and the failure is not subtle: strict mode dies while writing the uid map, throwing "Operation not permitted" on kernel 5.12 and later, because the kernel requires the creator of a user namespace to hold CAP_SETFCAP before it will let that namespace map uid 0. The nested mode, the weaker fallback, fails more quietly: it leaves the sandboxed command with enough residual privilege, specifically CAP_SYS_ADMIN, to unmount a tmpfs mount that was supposed to deny reads, unmount a bind mount that was supposed to deny writes, or remount the root filesystem read-write.
The four unconditional hardening steps that apply regardless of user namespace context
Four other steps carry no such precondition and belong on every path a re-exec helper takes.
The effective and permitted sets need to be zeroed before the exec happens, not after, because a populated permitted set lets a process re-raise its own effective capabilities through a later capset call even if effective reads empty at the moment of inspection. The inheritable set needs to be zeroed too, since file capability bits on disk, which the sandboxed workload's own files may carry, are the mechanism by which inheritable capabilities transmit through an execve, and those bits are not something the operator necessarily controls once the workload's code is already sitting in the sandbox. The ambient set needs the same treatment: ambient capabilities cross execve unconditionally for ordinary binaries, and it is the set most often left populated because it was left out of the design.
RLIMIT_CORE=0 sits alongside the capability drops as baseline hardening rather than an optional extra, because a core dump writes the process's memory to disk, and a process's memory is exactly where secrets, credentials, and decrypted data tend to live while a workload is running. An attacker who can trigger a core dump and later read the resulting file has a path to those secrets that has nothing to do with syscalls, filesystem paths, or network sockets. prctl(PR_SET_NO_NEW_PRIVS, 1), set once, prevents any future execve anywhere in the process tree from gaining privilege through setuid or setgid bits or through file capabilities picked up from disk, and it is a precondition for applying seccomp-bpf without root privilege. The call is irreversible once made, which is the property that makes it useful as a hardening step.
On the implementation side, the caps Rust crate provides the correct interface for dropping these capability sets without resorting to unsafe code, and nix's resource feature gates the setrlimit call that RLIMIT_CORE depends on. Both are the dependencies a project adds specifically because they are the only safe way to perform these operations from Rust without hand-rolling raw syscalls. One more discipline belongs in the same basket, even though it is not a capability or rlimit operation: verifying that a setuid or setgid call actually succeeded, rather than trusting its return value, is listed as a separate omission in the original issue alongside the capability and RLIMIT_CORE gaps, and it follows the same logic. A privilege-dropping call that silently fails is worse than no call at all, because it creates the appearance of hardening without the substance of it.
PR_SET_DUMPABLE and the limits of a re-exec design
PR_SET_DUMPABLE=0 looks, on paper, like exactly the kind of call that belongs in this checklist. Setting it before exec prevents the current process from producing a core dump and, in principle, from being traced by a peer process via ptrace. The call does exactly that, but only for the helper process that made it. The kernel resets the dumpable flag back to 1 on every execve of an ordinary binary, in setup_new_exec in fs/exec.c, and that reset applies unconditionally. In a re-exec helper design, where the whole point of the helper is to call exec into the sandboxed command after applying its hardening, that reset means the sandboxed command starts life fully dumpable no matter what the helper did to its own process state beforehand.
The project's response to this was to remove the PR_SET_DUMPABLE call entirely and record the finding as a non-claim in its enforcement table, phrased as "PR_SET_DUMPABLE found ineffective for a re-exec design," a conclusion reached empirically. The ptrace-attach protection that the call would otherwise have offered, preventing a peer process from attaching to and inspecting the target, simply isn't achievable for the sandboxed binary under this architecture, whatever the helper does on its own side of the exec boundary.
RLIMIT_CORE, by contrast, persists across execve and fully covers the core-dump half of the threat PR_SET_DUMPABLE was meant to address. The two controls were never redundant with each other, but they aren't interchangeable either: RLIMIT_CORE stops the disk write a core dump would produce, while PR_SET_DUMPABLE, had it survived the exec boundary, would additionally have blocked ptrace attachment. Losing PR_SET_DUMPABLE means losing that second protection specifically, not losing coverage of the core-dump threat itself. The broader implication extends past this one call: in a re-exec design, any hardening step that needs to survive the exec boundary has to be implemented through kernel state that is inheritable across execve, meaning RLIMIT_CORE, no_new_privs, and the capability sets, rather than through process flags the kernel is simply going to reset the moment the new binary loads.
Dividing the remaining attack surface between seccomp-bpf and Landlock
With the capability sets emptied, RLIMIT_CORE zeroed, and no_new_privs set, seccomp-bpf and Landlock step in to cover what capability dropping was never designed to touch. The two address orthogonal surfaces: seccomp filters at the level of syscall numbers and their immediate arguments, while Landlock filters at the level of filesystem paths and a handful of related resources. Neither one works correctly as an unprivileged restriction until no_new_privs has been set, which ties both of them structurally back into the capability-dropping sequence rather than leaving them as independent bolt-ons applied in whatever order is convenient.
Seccomp-bpf is fast and requires no elevated privilege to install, but it operates blind to pathnames: it sees that a process called open() and sees the raw arguments, but it has no native way to express a policy like "this process may open files under /tmp but nowhere else." Landlock exists specifically to cover that gap. As of Linux 6.12, running ABI version 6, Landlock lets any unprivileged process impose restrictions on itself and its own descendants that are enforced by the kernel and cannot be reversed once applied, covering filesystem access, TCP port restrictions, IPC, and signal scoping. The division of labor that falls out of this is clean: seccomp is the right tool for hardening at the syscall interface itself, Landlock is the right tool for discretionary restriction of filesystem and IPC access, and capability dropping is the right tool for the process's own privilege surface, the question of what it's allowed to do irrespective of which syscall or which path is involved. Each one addresses a surface the other two structurally cannot reach.
Defense in depth starts from the assumption that the process will eventually be compromised in some way, and the value of layering all three controls together is that a successful bypass of any single one still leaves the other two standing. A seccomp bypass that lets an attacker issue a forbidden syscall does not thereby grant access to a filesystem path Landlock has denied. A Landlock bypass that exposes a restricted directory does not thereby restore capabilities that were dropped unconditionally before the process ever executed. Each boundary fails independently of the others, and an attacker who clears one still has to clear the rest.
Sources
- Sandlock: Confining AI Agent Code with Unprivileged Linux Primitives
- capabilities(7) - Linux manual page
- Towards Least Privilege Containers with Cimplifier
- Man page of PRCTL
- Locking Down Science Gateways with Landlock and Seccomp
- Linux sandboxing with Landlock A picture containing text, clipart
- SysComb: Fine-Grained Transparent System Call Filtering for Attack Surface Reduction


