linux: drop a root caller's capabilities in both sandbox modes - #505
Merged
dylan-conway merged 3 commits intoSep 3, 2026
Merged
Conversation
bubblewrap run by uid 0 hands the command every capability the caller holds unless told to drop them, and the weaker nested mode never did: with CAP_SYS_ADMIN a sandboxed command could unmount a read-deny tmpfs or a write-deny bind, or remount / read-write. Strict mode dropped them but never started for root, because the seccomp helper's nested user namespace must map uid 0, which the kernel allows only when the namespace's creator held CAP_SETFCAP. A root caller now drops everything in both modes and keeps only CAP_SETFCAP while the helper is in use; the helper loses it on entering its namespace, whose copies of the mounts are locked. The Docker e2e gains an unmount case and runs in CI with and without CAP_SYS_ADMIN.
The command holds no capability in bwrap's user namespace whoever the caller is: strict mode already passed --cap-drop ALL unconditionally, and for a non-root caller the drop is a no-op, so the weaker mode can pass the same list instead of dropping only for uid 0. capabilityArgs() now always starts with --cap-drop ALL and adds CAP_SETFCAP only for a root caller using the seccomp helper; both branches push it and differ only in how /proc is mounted. The weak-mode unit test expects the drop for any caller, and the docker test loses a CapEff read it never asserted on.
dylan-conway
approved these changes
Sep 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
On Linux, bubblewrap run by uid 0 hands the sandboxed command every capability the caller holds unless told to drop them, and
enableWeakerNestedSandboxnever told it to. Inside bwrap's user namespace CAP_SYS_ADMIN is enough toumounta read-deny tmpfs or a write-deny bind, or to remount/read-write — the whole filesystem policy. Any embedder that runs as root with capabilities (a VM, a--privilegedor--cap-add SYS_ADMINcontainer) and setsenableWeakerNestedSandbox: truehad an advisory sandbox: reproduced in an Ubuntu 24.04 container with--cap-add SYS_ADMIN, where a sandboxed command read adenyReadfile afterumount, appended to adenyWritedirectory (persisted on the host), and created/etc/plantedaftermount -o remount,bind,rw /. Strict mode already passed--cap-drop ALL, but for a root caller it never started: the seccomp helper's nested user namespace must map uid 0, which the kernel (5.12+) allows only if the namespace's creator held CAP_SETFCAP, so every command died withapply-seccomp: write /proc/self/uid_map: Operation not permitted— which is what pushes root embedders to the weaker mode in the first place.Fix: a root caller drops every capability in both modes and keeps exactly CAP_SETFCAP while the seccomp helper is in use and the caller has it (
rootCapabilityArgs, readingCapEfffrom/proc/self/status). The helper loses that capability on entering its nested namespace, so the command runs with none in bwrap's namespace; the copies of bwrap's mounts in the nested namespace are locked by the kernel, so the full capability set the command holds there cannot lift them (verified:umount, remount and write all refused withCapEff: 000001ffffffffffinside). Without the helper (allowAllUnixSockets: true) it is a plain--cap-drop ALL. A non-root caller is unchanged (nothing to drop, nothing it may add).Effects, verified in the container as uid 0, with Docker's default capabilities, with
--cap-add SYS_ADMIN, and--privileged, on bubblewrap 0.9.0: weaker mode closes all three escapes and still starts, seccomp unix-socket block intact; strict mode now starts for a root caller (fresh/procand all) instead of dying atuid_map— the full suite as uid 0 goes from 109 failures onmainto 8 (three are environmental in that container; five are tests that runapply-seccompstandalone or assert a non-root uid). As uid 1001 the suite matchesmain.Tests:
wrap-with-sandbox.test.tspins the weaker branch's arguments (and--cap-drop ALLwhen the runner is root); the Docker e2e gains a case that grepsCapEff, triesumountof a read-denied directory and of the work dir plus a remount of/, and asserts the secret stays hidden and the write fails; the CI docker job installslibcap2-bin, runs with--cap-add SYS_ADMIN, and executes the e2e twice — holding the capability (a VM's root; red onmain) and with it dropped viacapsh(the unprivileged container the mode exists for).Blast radius, not behind a flag: root callers only. A sandboxed command run by a root embedder can no longer create a user namespace that maps uid 0 (
unshare -Urfails), as was already the case in strict mode; and a root caller without CAP_SETFCAP in its effective set gets no--cap-add, so the seccomp helper fails as strict mode did before (it failed there already: the nested mapping needs it). Follow-up worth doing inapply-seccomp.c:capsetto empty beforeexecvpwhen euid is 0, so the workload holds no capabilities even in the nested namespace (defence in depth; needs the vendored binaries rebuilt).