Skip to content

Probe bubblewrap with the argv a launch runs - #2039

Merged
ppXD merged 1 commit into
mainfrom
fix/probe-bubblewrap-with-the-launch-argv
Sep 27, 2026
Merged

ppXD merged 1 commit into
mainfrom
fix/probe-bubblewrap-with-the-launch-argv

Conversation

@ppXD

@ppXD ppXD commented Sep 27, 2026

Copy link
Copy Markdown
Owner

Summary

  • The bubblewrap availability probe used to run a hand-picked subset of flags. It now runs BuildArgs(ProbePlan): the default tier's own launch argv, including --proc, --dev and --unshare-net, confining true (BubblewrapSandbox.Classify). The old subset passed on a host that allows user namespaces but masks /proc, for example Docker or a Kubernetes pod with seccomp Unconfined and the default masked paths or procMount. Every real launch then died with bwrap: Can't mount proc on /newroot/proc, RequireConfinement could not refuse the host, and the run record claimed isolation it never got.
  • The old minimal user-namespace argv now runs only after bwrap starts and refuses the launch argv. It tells no-userns apart from the new SandboxConfinement.ReasonMountsDenied = "mounts-denied" by which argv ran, never by parsing stderr. Under Docker's full defaults nothing changes: the default seccomp profile already blocks the user namespace, so both probes report no-userns.
  • Accepted risk: a host that allows user namespaces but denies network namespaces (user.max_net_namespaces=0, or a profile that denies only CLONE_NEWNET) now reports mounts-denied.
    • Its Trusted runs share the network and did confine; they now run unconfined, or are refused under RequireConfinement.
    • Its network-off runs already died in bwrap.
    • The refusal (BubblewrapSandbox.cs:69) and the RequireConfinement docs (appsettings.json Sandbox._doc, RuntimeSettings.RequireSandboxConfinement) now name the network namespace, so an operator on such a host is not sent to /proc. The trade-off is documented on ProbePlan.
  • The reason column has no constraint, so no migration is needed. The Room shows the reason only inside the backend-written network-posture sentence (the :stat:network StatBlock detail). The frontend renders that detail raw (SessionRoomView.tsx:1601), so no frontend change is needed. The shared wording fixture frontend/src/lib/networkPosture.fixture.json gains a mounts-denied case.

Test plan

  • Unit: the probe argv equals BuildArgs(ProbePlan) and holds --proc, --dev and --unshare-net.
  • Unit: reason truth table (Ran / Missing / Refused+Ran / Refused+Refused).
  • Unit: the fallback argv holds none of the launch's mounts.
  • Unit: the refusal names every wall the probe can report.
  • Unit: the posture-sentence rows and the fixture cover mounts-denied.
  • Unit, full suite: 11329 passed, 1 skipped.
  • Integration: RoomProjectorFlowTests, including a new mounts-denied Room row, and SandboxConfigurationStartupTests: 142/142.
  • Sandbox E2E, root lane (privileged container), BubblewrapProbeE2ETests: /dev/null over /proc/kcore gives unavailable with mounts-denied; without it, available.
  • Sandbox E2E, full Category=Sandbox: 69/69. Host left clean (ip rule, ip netns, nft tables, kcore overmounts, staged dirs).
  • Mutation: running the old argv first turns the masked-/proc arm red (it reports Available).
  • Non-root, worker image as uid 1654 with no capabilities: Docker defaults gives no-userns; seccomp Unconfined gives mounts-denied; seccomp and systempaths Unconfined gives available.
  • Staged userns-yes / netns-no host (max_net_namespaces=0 in a child user namespace): mounts-denied, as accepted above.
  • CI sandbox-isolation.yml: at least 69 tests executed and both probe-arm markers present.

The availability probe ran a hand-picked subset of flags with no --proc,
--dev or --unshare-net. On a host that allows user namespaces but masks
/proc (Docker or a Kubernetes pod with seccomp Unconfined and the
default masked paths or procMount) that subset passed, so the worker
reported confinement while every real launch died in bwrap with "Can't
mount proc". RequireConfinement could not refuse such a host, and the
run record claimed isolation the launch never got. Under Docker's full
defaults nothing changes: the default seccomp profile already blocks
the user namespace, so both probes report no-userns.

The probe now runs BuildArgs of the default tier's own plan (network
severed, running true), so "available" means the launch argv runs here.
Only when bwrap starts and refuses that argv does it re-run the old
minimal user-namespace argv, to tell no-userns apart from the new
mounts-denied reason. The two are told apart by which argv ran, not by
parsing bwrap's stderr, because the fixes differ: allow user namespaces
versus unmask /proc.

A masked-/proc host goes from "every launch fails" to an honest
Unconfined record, or a refusal under RequireConfinement. One host
loses confinement it had: one that allows user namespaces but denies
network namespaces (user.max_net_namespaces=0, or a profile that denies
only CLONE_NEWNET). Its network-off launches already died in bwrap; now
the probe fails as well, so its Trusted runs, which share the network
and did confine, run unconfined, and the record names the wall
mounts-denied. That is accepted, because "available" answers for the
default tier and no probed deployment posture denies only network
namespaces. The refusal and the RequireConfinement docs name the
network namespace, so an operator on such a host is not sent to /proc.

The reason column has no constraint, so the new value needs no
migration. It reaches user-visible text only through the network-posture
sentence, which the Room shows as its network stat line and the shared
fixture pins.
@ppXD
ppXD merged commit 4402af9 into main Sep 27, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant