Skip to content

test(head): don't pass the runner's environment to the unprivileged child - #705

Merged
matt-dz merged 1 commit into
mainfrom
matthew.deguzman/head-test-child-env
Oct 28, 2026
Merged

matt-dz merged 1 commit into
mainfrom
matthew.deguzman/head-test-child-env

Conversation

@matt-dz

@matt-dz matt-dz commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Gives the unprivileged child in TestHeadPermissionDenied (the root re-exec path added in #688) only PATH and its own TMPDIR, instead of the parent's whole environment.

Motivation

Upstreams a fix from the rshell → datadog-agent migration (DataDog/datadog-agent#58502, commit e790263d9a5). Under bazel coverage as root, the inherited environment contains COVERAGE_OUTPUT_FILE, XML_OUTPUT_FILE and TEST_SRCDIR, all pointing at root-owned paths, so the child exited 2 ("opening coverage data output file ... permission denied") and failed the Agent's tests_linux-* jobs.

To be clear about scope: this does not reproduce under rshell's own go test CI. I tried go test -cover as root with GOCOVERDIR pointing at a root-only directory, and the child still passes because Go's coverage runtime only warns when it can't write. So this is test hardening, and it keeps both copies identical so future syncs stay clean. It is not a fix for a failure rshell sees today.

Testing

  • go test ./builtins/head/ ./analysis/: pass
  • Root path: built the test binary and ran sudo ./head.test -test.run '^TestHeadPermissionDenied$' -test.v. The unprivileged child runs and passes.

Checklist

  • Tests added/updated
  • Documentation updated (if applicable)

🤖 Generated with Claude Code

…hild

TestHeadPermissionDenied re-runs itself as UID 65534 when the parent is
root and passed the parent's whole environment to that child. Under
Bazel coverage, as root, the environment includes COVERAGE_OUTPUT_FILE,
XML_OUTPUT_FILE and TEST_SRCDIR, all pointing at root-owned paths, so
the child exits 2 trying to write its coverage profile. Give the child
only PATH and its own TMPDIR.

Plain `go test` is not affected (Go's coverage runtime only warns when
GOCOVERDIR is unwritable); this keeps rshell in sync with the copy in
datadog-agent (DataDog/datadog-agent#58502), where it broke CI.

Environment: Datadog workspace

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@matt-dz
matt-dz marked this pull request as ready for review October 28, 2026 19:19
@matt-dz
matt-dz requested review from a team as code owners October 28, 2026 19:19
@matt-dz
matt-dz requested a review from pducolin October 28, 2026 19:19
@matt-dz
matt-dz enabled auto-merge October 28, 2026 20:48

@julesmcrt julesmcrt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed ce42337ba4dbadb692d01637feabb74de70d6563.
Findings first — both are non-blocking nits:

  1. The env isolation is enforced by construction only, not pinned by an assertion (builtins/head/head_unix_test.go:74). If someone later reverts this line to os.Environ(), nothing fails on a non-root dev machine or a macOS CI job — only root-under-Bazel CI would catch it. A canary check (set a var in the parent, assert it's empty via os.Getenv in the child branch) would make the property self-verifying in one line. Optional.
  2. PATH is inherited but never used: the re-exec'd child resolves no external programs (everything runs in-process), so it could be dropped for a truly minimal env. Harmless either way.
    Everything else looks right. This changes no assertion about head's behavior — the child-side checks (open succeeds pre-chmod; exit 1 + permission denied post-chmod) are untouched, and child failures still propagate to the parent via CombinedOutput. What it does change is the environment handed to the uid-65534 re-exec, and replacing os.Environ() with an explicit PATH/TMPDIR allowlist is the correct hygiene for any exec that drops credentials: it stops leaking the runner's env (including any CI tokens under Bazel) into a nobody-uid process, and it stops LD_PRELOAD/DYLD_* from riding along into a credential-dropping exec even though the direction here is de-escalation so the practical hijack risk was low. The comment explaining the Bazel coverage/report/runfiles motivation is helpful.
    Confirmed the package suite passes and the full file compiles under its //go:build unix tag (Windows keeps head_windows_test.go). Merge against main is clean, single commit, one file. I couldn't exercise the root re-exec branch locally (non-root host, no container available), but the revert failure mode is clear from reading: the nobody-uid child would inherit Bazel's root-owned GOCOVERDIR/report paths, fail its writes, and the parent's require.NoError would fire.

@matt-dz
matt-dz added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit d4f0352 Oct 28, 2026
49 checks passed
@matt-dz
matt-dz deleted the matthew.deguzman/head-test-child-env branch October 28, 2026 22:22
matt-dz added a commit to DataDog/datadog-agent that referenced this pull request Oct 29, 2026
…verage

The cgo-free sandbox tests re-exec themselves as restricted helpers.
Under bazel coverage the helpers then failed after passing, trying to
write coverage data:
- the landlock helpers restrict themselves to the test paths, so they
  now exit directly once their checks pass;
- the seccomp elevation helper drops privileges, so it no longer
  inherits the runner's environment and its root-owned coverage paths
  (as DataDog/rshell#705 did for head).

Environment: Datadog workspace

Co-Authored-By: Claude Code Opus 5.5 <noreply@anthropic.com>
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.

2 participants