Summary
On Ubuntu hosts with the stock bwrap-userns-restrict AppArmor profile, apply-seccomp can never
obtain CAP_SYS_ADMIN, so it aborts and no sandboxed command can run at all — echo hi fails
exactly like anything else. The comment block in apply-seccomp.c anticipates this case and resolves
it with "the caller must supply CAP_SYS_ADMIN", but on Ubuntu the caller cannot supply it either.
This is reported downstream in anthropics/claude-code#43454 (open since 2026-04-04, 22 comments,
regression traced to Claude Code 2.1.92) and anthropics/claude-code#81799 (2.1.220, with an A/B
canary). There appears to be no issue for it in this repo, which is where a fix would land.
Every workaround circulating in those threads is a global AppArmor weakening, because the mechanism
has been mis-attributed. Details below.
Environment
- Ubuntu 25.10, kernel 6.17.0-41-generic
- bubblewrap 0.11.0, not setuid (mode 0755)
kernel.apparmor_restrict_unprivileged_userns = 0, kernel.unprivileged_userns_clone = 1
- Launching process is unconfined, in the initial user namespace, with no inherited seccomp filter
(Seccomp: 0, NoNewPrivs: 0)
- Stock
/etc/apparmor.d/bwrap-userns-restrict and /etc/apparmor.d/unprivileged_userns
Symptom
apply-seccomp: write /proc/self/setgroups (nested userns is capability-restricted; caller must provide CAP_SYS_ADMIN): Permission denied
Root cause
sandbox/linux-sandbox-utils.js composes the sandbox as "Stage 1: Outer bwrap … Stage 2:
apply-seccomp", with bwrapArgs.push(applySeccompCmd) — so apply-seccomp runs as bwrap's child.
vendor/seccomp-src/apply-seccomp.c (main, ~L687-699) documents the two attempts and the abort:
Two paths to get CAP_SYS_ADMIN for the unshare:
(a) The caller (bwrap) kept CAP_SYS_ADMIN in this user namespace via --cap-add. …
(b) We don't have the cap. Create a nested user namespace to get it …
Path (b) can itself fail on hosts where unprivileged user namespaces are gated by an LSM (Ubuntu
24.04's AppArmor restriction, for example) … In that case we abort: the caller must supply CAP_SYS_ADMIN.
Ubuntu strips capabilities from bwrap's children deliberately. From the shipped profile:
bwrap is allowed access to user namespaces and capabilities within the user namespace, but its
children do not have capabilities, blocking bwrap from being able to be used to arbitrarily
by-pass the user namespace restrictions.
Implemented as allow pix /** -> &bwrap//&unpriv_bwrap, where unpriv_bwrap carries
audit deny capability. (A sibling profile, unprivileged_userns, applies the same denial to
unconfined processes that create a user namespace, so the standalone path is covered too.)
Why path (a) is not available as an escape hatch either
- AppArmor mediates the
capable() check per profile. A capability present in the kernel
capability set is still refused, so having the caller pass --cap-add CAP_SYS_ADMIN does not help.
- In an AppArmor stack the effective permission is the intersection, and
deny always wins. No
profile attached to the helper can restore the capability, and local/* includes cannot override a
deny in the parent profile.
kernel.apparmor_restrict_unprivileged_userns is not the relevant knob — it governs whether
userns creation is blocked, not the capability-stripping profile transition. It is already 0 on
this host and the failure persists. Nearly every downstream comment points at this sysctl, which is
why those threads never converged.
The misleading part
bwrap --unshare-user <cmd> succeeds standalone on the same host, because /usr/bin/bwrap has its
own profile granting allow capability. Only its children are stripped. So the host looks fully
capable of unprivileged userns work while apply-seccomp fails.
Confirmed workaround
Setting network.allowAllUnixSockets skips Stage 2 entirely; the capability is never requested and
the rest of the sandbox arms normally (filesystem allow/deny and --unshare-net isolation verified
working afterwards). The cost is exactly the feature that stage provides: Unix-socket filtering.
Approaches that do not work
| Approach |
Outcome |
apparmor_restrict_unprivileged_userns=0 |
Already 0; wrong knob |
flags=(unconfined) AppArmor profile on the helper |
Stacked with the denying profile; deny wins |
local/unpriv_bwrap / local/unprivileged_userns overrides |
Cannot override a parent deny |
setuid bwrap to obtain the capability |
bubblewrap 0.11.2 (CVE-2026-41163) deprecates setuid, defaults -Dsupport_setuid=false, and plans removal |
enableWeakerNestedSandbox: true |
No effect |
Substituting a pre-7f650392 apply-seccomp |
Argv contract changed once the BPF filter was baked in; also discards io_uring blocking (7ee4ac60) and the USER_NOTIF observer (1640f71f, 5ce6b3e5) |
Proposal
The code already falls back (a) → (b). A third step would turn a hard failure into graceful
degradation: when neither path yields the capability, apply the seccomp filter without the nested
PID/mount namespace — the pre-7ee4ac60 behaviour — and log that io_uring blocking and the violation
observer are unavailable, instead of die().
That keeps Unix-socket filtering working on every LSM-restricted host (i.e. all of Ubuntu 24.04+),
where today the entire sandbox refuses to start. If silently reducing isolation is undesirable,
gating it behind an explicit opt-in, or emitting a one-time warning, would still be a large
improvement over a total outage.
Related: anthropics/claude-code#44180 asks for Unix-socket filtering that does not depend on the
seccomp helper, which would address this from the other direction.
Diagnosed empirically on the affected host with Claude Code; every claim above was verified against
the running system or the source rather than inferred.
Summary
On Ubuntu hosts with the stock
bwrap-userns-restrictAppArmor profile,apply-seccompcan neverobtain
CAP_SYS_ADMIN, so it aborts and no sandboxed command can run at all —echo hifailsexactly like anything else. The comment block in
apply-seccomp.canticipates this case and resolvesit with "the caller must supply CAP_SYS_ADMIN", but on Ubuntu the caller cannot supply it either.
This is reported downstream in anthropics/claude-code#43454 (open since 2026-04-04, 22 comments,
regression traced to Claude Code 2.1.92) and anthropics/claude-code#81799 (2.1.220, with an A/B
canary). There appears to be no issue for it in this repo, which is where a fix would land.
Every workaround circulating in those threads is a global AppArmor weakening, because the mechanism
has been mis-attributed. Details below.
Environment
kernel.apparmor_restrict_unprivileged_userns = 0,kernel.unprivileged_userns_clone = 1(
Seccomp: 0,NoNewPrivs: 0)/etc/apparmor.d/bwrap-userns-restrictand/etc/apparmor.d/unprivileged_usernsSymptom
Root cause
sandbox/linux-sandbox-utils.jscomposes the sandbox as "Stage 1: Outer bwrap … Stage 2:apply-seccomp", with
bwrapArgs.push(applySeccompCmd)— soapply-seccompruns as bwrap's child.vendor/seccomp-src/apply-seccomp.c(main, ~L687-699) documents the two attempts and the abort:Ubuntu strips capabilities from bwrap's children deliberately. From the shipped profile:
Implemented as
allow pix /** -> &bwrap//&unpriv_bwrap, whereunpriv_bwrapcarriesaudit deny capability. (A sibling profile,unprivileged_userns, applies the same denial tounconfined processes that create a user namespace, so the standalone path is covered too.)
Why path (a) is not available as an escape hatch either
capable()check per profile. A capability present in the kernelcapability set is still refused, so having the caller pass
--cap-add CAP_SYS_ADMINdoes not help.denyalways wins. Noprofile attached to the helper can restore the capability, and
local/*includes cannot override adenyin the parent profile.kernel.apparmor_restrict_unprivileged_usernsis not the relevant knob — it governs whetheruserns creation is blocked, not the capability-stripping profile transition. It is already
0onthis host and the failure persists. Nearly every downstream comment points at this sysctl, which is
why those threads never converged.
The misleading part
bwrap --unshare-user <cmd>succeeds standalone on the same host, because/usr/bin/bwraphas itsown profile granting
allow capability. Only its children are stripped. So the host looks fullycapable of unprivileged userns work while
apply-seccompfails.Confirmed workaround
Setting
network.allowAllUnixSocketsskips Stage 2 entirely; the capability is never requested andthe rest of the sandbox arms normally (filesystem allow/deny and
--unshare-netisolation verifiedworking afterwards). The cost is exactly the feature that stage provides: Unix-socket filtering.
Approaches that do not work
apparmor_restrict_unprivileged_userns=0flags=(unconfined)AppArmor profile on the helperdenywinslocal/unpriv_bwrap/local/unprivileged_usernsoverridesdenybwrapto obtain the capability-Dsupport_setuid=false, and plans removalenableWeakerNestedSandbox: true7f650392apply-seccomp7ee4ac60) and the USER_NOTIF observer (1640f71f,5ce6b3e5)Proposal
The code already falls back (a) → (b). A third step would turn a hard failure into graceful
degradation: when neither path yields the capability, apply the seccomp filter without the nested
PID/mount namespace — the pre-
7ee4ac60behaviour — and log that io_uring blocking and the violationobserver are unavailable, instead of
die().That keeps Unix-socket filtering working on every LSM-restricted host (i.e. all of Ubuntu 24.04+),
where today the entire sandbox refuses to start. If silently reducing isolation is undesirable,
gating it behind an explicit opt-in, or emitting a one-time warning, would still be a large
improvement over a total outage.
Related: anthropics/claude-code#44180 asks for Unix-socket filtering that does not depend on the
seccomp helper, which would address this from the other direction.
Diagnosed empirically on the affected host with Claude Code; every claim above was verified against
the running system or the source rather than inferred.