Skip to content

apply-seccomp aborts on Ubuntu: AppArmor denies CAP_SYS_ADMIN to bwrap children, so no sandboxed command can run #429

Description

@dorrogeray

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 allecho 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions