Skip to content

fix: the sandbox stops running as root - #211

Merged
vicenteliu merged 1 commit into
mainfrom
fix/the-sandbox-stops-being-root
Aug 20, 2026
Merged

fix: the sandbox stops running as root#211
vicenteliu merged 1 commit into
mainfrom
fix/the-sandbox-stops-being-root

Conversation

@vicenteliu

Copy link
Copy Markdown
Owner

#210 left this open on purpose: the argv carried --tmpfs=…,uid=1000 but no
--user, so every command ran as root inside the container. I called it
"heavily defanged" by the other controls and flagged that closing it might break
a diagnostic. Both halves turned out to be worth checking rather than assuming.

Defanged is not closed

Dropping every capability does not stop root reading root-owned files. DAC
grants access on an ownership match — no capability is consulted — so
CapEff=0000000000000000 has nothing to say about it. Measured through the real
SandboxEngine, same image, argv otherwise identical:

as root as uid 1000
read /etc/shadow (0640 root:shadow) yes denied
list /root (0700 root:root) yes denied
read /proc/1/environ yes yes — PID 1 is our own process either way

The third row is there because it is not a gap, and a table that only showed
the wins would be selling something.

That matters more as soon as FsPolicy.mounts is used: with identity uid
mapping and no user namespace, a root process writes host files as root.

The cost I feared did not appear

Verified through the engine, not by reading the argv:

  • iduid=1000 gid=1000 groups=1000
  • CapEff / CapBnd still 0000000000000000, NoNewPrivs: 1, Seccomp: 2
  • /work still writable and now owned 1000 1000 rather than merely mounted
    for a user that was not being used
  • pipelines, ps (shows USER 1000), df, date, /etc/os-release, and the
    session's own grep / journalctl diagnostics all run unchanged

One constant, not two literals

--user=1000:1000 and --tmpfs=…,uid=1000,gid=1000 both come from
_SANDBOX_UID. The argv already carried uid=1000 while running as root
two places that have to agree, disagreeing silently, is how most of this file's
defects began (#209's 64Mi, #210's policy path).

Tests

Two, both failing on the previous argv:

  • test_the_process_is_not_root — the assertion
  • test_a_root_owned_file_cannot_be_read — the reason the first one matters

Verification

pytest -m "not slow and not requires_ollama" — CI's selection — 1374 passed.
-m requires_docker 14 passed. ruff, ruff format, mypy(154) clean.

Still not verified, unchanged from #210: the seccomp profile on the 32-bit
sub-architectures archMap claims. No 32-bit host here.

🤖 Generated with Claude Code

`--tmpfs=/work:…,uid=1000` said someone had intended a non-root process. No
`--user` flag was ever passed, so every command ran as root inside the
container.

"Root with all capabilities dropped" sounds equivalent to unprivileged and is
not. DAC grants access on an **ownership match**, with no capability involved,
so `CapEff=0` does nothing about root-owned files. Measured through the real
engine, same image, everything else identical:

                                          as root   as uid 1000
    read /etc/shadow (0640 root:shadow)      yes       denied
    list /root (0700 root:root)              yes       denied

`--user=1000:1000` now rides alongside a tmpfs owned `uid=1000,gid=1000`, both
from `_SANDBOX_UID` rather than two literals that have to agree — the argv
already carried `uid=1000` while the process ran as root, and two-places-must-
agree is how most of this file's defects began.

The cost I was worried about did not appear. Verified through `SandboxEngine`:
`id` reports 1000:1000, capabilities and NoNewPrivs and the seccomp filter are
unchanged, `/work` is writable and now *owned* by the user rather than merely
mounted for them, and pipelines, `ps`, `df`, `date`, `/etc/os-release` and the
session's own `grep`/`journalctl` diagnostics all run.

Two tests cover it, and both fail on the previous argv: one asserts the process
is not root, one asserts a root-owned file cannot be read — the second is the
one that says why the first matters.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vicenteliu
vicenteliu merged commit 0d09cb7 into main Aug 20, 2026
4 checks passed
@vicenteliu
vicenteliu deleted the fix/the-sandbox-stops-being-root branch August 20, 2026 02:09
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