Skip to content

Give a namespaced action a /proc that matches its PID namespace - #3024

Merged
MarcusSorealheis merged 8 commits into
TraceMachina:mainfrom
b7r6:namespaced-proc
Oct 23, 2026
Merged

MarcusSorealheis merged 8 commits into
TraceMachina:mainfrom
b7r6:namespaced-proc

Conversation

@b7r6

@b7r6 b7r6 commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

What and why

With use_namespaces, an action runs as PID 1 of its own PID namespace, but /proc stayed the host's, which lists processes by their host PIDs. An action that forked a child and looked it up under /proc/<pid> got whichever host process had that number, or nothing: the reporter's profiler read /proc/<its own PID>/exe, hit an unrelated host process, and died with PermissionError before it started its target.

Now the worker always takes a mount namespace alongside the PID, user, IPC and UTS ones, makes it private so nothing in it reaches the host, and the PID 1 child mounts a fresh proc over /proc right before it execs. The mount carries nosuid,nodev,noexec plus whatever read-only and atime flags the host's /proc has, because inside a user namespace the kernel refuses a new proc that is more permissive than the locked one it covers. use_mount_namespace keeps its meaning (mask the root action directory, private /tmp); it is no longer the only way to get a mount namespace.

Where the host refuses the mount, which an unprivileged container that masks paths under /proc does, a new startup probe (fresh_proc_supported) logs a warning and the action keeps the host's /proc, as it had before. Without use_mount_namespace the mount namespace itself is optional too: if a syscall policy refuses CLONE_NEWNS or the mount that makes it private, the action gets exactly the namespaces it got before this change, and the fresh /proc is skipped. With use_mount_namespace both stay required, since the root mask needs them. The probe code is shared with namespaces_supported now, instead of a second copy of the unshare-and-map sequence. Persistent worker processes get the same /proc, since they are started through the same hook.

The use_namespaces doc comment, the sandbox description on the security-hardening page and the persistent worker pool's comment say what happens now. The config reference renders only a field's first paragraph, so regenerating it produced no diff.

Fixes #3003

How was this verified?

  • Two new tests in namespace_utils_test.rs run the issue's shape inside the namespaces: a shell forks sleep, reads /proc/<child>/cmdline, and counts the processes /proc lists. They assert PID 1 under /proc is the shell, the child's command line is sleep 30 (the script waits up to five seconds for the forked child to exec, since right after the fork it can still be the shell), and fewer than ten processes are listed; the first also checks that the test process's own /proc mounts in /proc/self/mountinfo are the same before and after, which catches a leaked mount without assuming anything about what the host's PID 1 is. The two tests pass 400 consecutive runs, and pass inside unshare --user --pid --fork --mount-proc sh -c ..., where PID 1 is a shell. One runs without the root mask, the other with the mask and a private /tmp. With the mount_proc call temporarily replaced by Ok(()) both fail on "PID 1 under /proc is not the action" (the host's init shows through); with it, both pass.
  • A third test installs a seccomp filter in the pre-exec hook that answers every mount with EPERM, then runs an action without use_mount_namespace and asserts it still runs as PID 1 of its namespace, and that the same filter with use_mount_namespace fails the spawn with EPERM. On the previous revision the first half fails with EPERM, since the private-mount step had become mandatory.
  • cargo test -p nativelink-worker --profile=smol: all ten test binaries pass, 231 tests, including the namespace, running-actions-manager and persistent-worker suites that go through the changed hook.
  • bazel test //nativelink-worker/... //nativelink-config/... with the rustfmt and clippy aspects: 15 of 15 targets pass. The first run stopped on a nightly rustfmt difference in the test file, which pre-commit then fixed.
  • pre-commit run --files <every touched file>: rustfmt, typos and vale pass (vale also lints the config doc comment).
  • bun --filter @nativelink/docs lint:anchors and lint:snippets pass; gen:config-reference main reports no diff for the reason above.
  • The host this ran on permits the mount (unshare --user --map-current-user --pid --fork --mount-proc true succeeds), so the fallback path for a host that refuses it was exercised only through the probe's error decoding, not against a real refusal.
  • Not verified: the reporter's exact Bazel genrule against a running worker. The pre-exec hook under test is the one running_actions_manager.rs installs, unchanged in how it is called.

Risk

Every namespaced action now gets a mount namespace, which it did not have without use_mount_namespace. Inside a user namespace an action could never mount into the host's mount namespace anyway, so the only observable change for an action is /proc itself: a tool that read host processes through /proc/<hostpid> from inside a namespaced action no longer finds them. That is the behaviour the issue asks for, and it is what Bazel's own linux-sandbox does. A deployment whose environment refuses the mount keeps the old behaviour and gets one warning at startup; nothing fails that passed before. Not on a hot path: one statvfs and one mount per action spawn.

AI assistance

An agent (Claude Code) wrote the change and tests under b7r6's direction; b7r6 reviews every line before it goes upstream.

🤖 Generated with Claude Code

b7r6 and others added 4 commits October 22, 2026 13:31
Completes the no-per-PR-OCI-builds intent: image-off-pr moved image.yaml off
PRs and TraceMachina#2877 moved the nix integration job off PRs, but main.yaml's
integration-tests (docker-compose image build + run_integration_tests.sh)
fell between them and still ran on every PR. Defer to nightly + merge-to-main,
matching TraceMachina#2880's pattern. Fork-local until folded into the upstream OCI/nightly
set.

(cherry picked from commit 46e058d)
With use_namespaces an action is PID 1 of its own PID namespace, but
/proc stayed the host's, so an action looking up a child it forked
under /proc/<pid> read whichever host process had that number. The
worker now always takes a private mount namespace as well, and the
PID 1 child mounts a fresh proc over /proc before it execs, with the
host /proc's locked flags mirrored so a user namespace accepts it.

Where the host refuses the mount, as an unprivileged container that
masks paths under /proc does, a startup probe warns and the action
keeps the host's /proc as before. The probe shares its child-side
code with namespaces_supported instead of repeating it.

Fixes TraceMachina#3003

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…c tests

Right after `sleep 30 &` the child can still be the shell, so reading
its command line raced the exec and failed about once in a few hundred
runs. The script now waits for the child to report `sleep 30`. The host
leak check compared /proc/1/comm with "sh", which is wrong wherever the
enclosing PID 1 is a shell; it now compares the test process's /proc
mounts before and after.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Without use_mount_namespace the mount namespace only carries the fresh
/proc, but entering it and making it private had become mandatory, so a
syscall policy that denies mount failed the namespace probe and the
worker refused to start where it ran before. Now a refused CLONE_NEWNS
or a refused private remount there falls back to the namespaces the
action had before the fresh /proc, which is then skipped; with
use_mount_namespace both stay required for the root mask. A test
installs a seccomp filter denying mount in the pre-exec hook to show
the action still runs as PID 1, and that the masked variant still
fails with EPERM.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
nativelink Ready Ready Preview Oct 23, 2026 2:44pm UTC
nativelink-aidm Ready Ready Preview Oct 23, 2026 2:44pm UTC

Request Review

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ b7r6
❌ deus-ex-b7r6-0x02
You have signed the CLA already but the status is still pending? Let us recheck it.

@MarcusSorealheis MarcusSorealheis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@MarcusSorealheis
MarcusSorealheis merged commit db200db into TraceMachina:main Oct 23, 2026
16 checks passed

This branch was successfully deployed

2 active deployments
Preview – nativelink — b40ba3a7 Deployed Oct 23, 2026 by vercel[bot]
Preview – nativelink-aidm — b40ba3a7 Deployed Oct 23, 2026 by vercel[bot]
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.

[Bug]: With use_namespaces, /proc doesn't match the action's PID namespace

4 participants