feat(exec): drop the host directories from a target's PATH - #446
raphaelvigee wants to merge 1 commit into
Conversation
04defbd to
cebd657
Compare
|
Pushed a fix for the CI failure, which was this PR's own. Dropping the host directories broke every in-process harness that runs a real Two changes:
That second one is a user-visible consequence worth calling out in review: a Verified locally across the e2e suite — engine_sanity, deps, group, tool_env, execrunner, clean, codegen_in_place, engine_core, group_relocate, http_fetch, plugin_buildfile, query all pass, plus plugin-exec 108. Lint clean. |
cebd657 to
7a8f19d
Compare
|
Second CI failure on this PR, and a genuine bug I introduced — worth recording rather than just fixing.
The cause: I was resolving the shell on the host's ambient PATH even for targets running under a runner. Fixed by gating the resolution on Added a unit test pinning that a local spawn does get an absolute shell, with a pointer to
|
|
Third round on this PR. Two more things it broke, both real, both found only by CI. 1. 2. A production hole, not a test one: It now gets the host's directories, deliberately and unlike a build target: its entire purpose is poking around, and it builds nothing and reaches no cache key, so the argument for keeping the host out does not apply. Documented in Running tally of what this PR has broken and fixed: harness PATH assumptions (19 sites), container shell resolution, engine harnesses (15 sites), and the interactive shell fallback. That is a fair reflection of how many seams "drop the host PATH" actually touches, and why it is worth keeping as its own revert line. |
7a8f19d to
85605bd
Compare
85605bd to
41b464e
Compare
41b464e to
2853ee9
Compare
This is what makes the toolbox worth more than convenience. A target now sees what it declares plus heph's builtins, and nothing else — which turns the sandbox from "isolated files, ambient tools" into "isolated files, declared tools", and closes the largest remaining under-hashing hole: the version of every host binary a recipe ever invoked was never in its cache key. A recipe reaching for an undeclared host binary now fails with `command not found` rather than succeeding on whichever machine happens to have it. `path:` on the driver names directories back for a workspace that needs them. Half of this already shipped: #425 made the driver skip its host-directory injection for any target that names a runner, on the reasoning that putting /usr/bin ahead of the environment lets a host tool silently shadow the one the target asked to run beside, inside a cache key claiming that environment. That argument does not stop being true when the environment is the local host. What is left is the local spawn: the `def.runner.is_none()` injection and `PathPolicy::fallback` both go. The shell is the exception, deliberately. `bash` is the *driver's* implementation detail, not something the target declared, so it is resolved on the ambient PATH — the environment heph was launched in — and spawned by absolute path. An absolute program spawns fine regardless of the child's PATH, so the shell starts and the recipe inside it still cannot reach an undeclared host binary. That must never extend to the `exec` driver's argv[0], which *is* the target's command: resolving that ambiently would hand back the whole hole this closes. The split follows the line the code already draws between `wrap_run` and `wrap_run_shell`. The shell stays out of the cache key. Folding the resolved path in would split the remote cache between every machine whose bash lives somewhere different, for a hole a provisioned shell closes properly. Nine existing tests were relying on the host PATH for `cat`/`sleep`/ `printf` while testing process plumbing. They now say so, via a `with_host_path()` test helper, rather than being defaulted into it. Verified end to end with no configuration at all: `cp --version` reports `cp (uutils coreutils) 0.10.0` and resolves to the shim directory, `git` fails with 127, and the shell that ran both is an absolute /nix/store path the target's own PATH could never have found. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0181d7hhbYWXT42Z1KQPM29Q
2853ee9 to
918d40c
Compare
The host directories —
/usr/local/bin:/usr/bin:/bin— come off every target'sPATH. Breaking, deliberately.This is what makes the toolbox worth more than convenience. It turns the sandbox from isolated files, ambient tools into isolated files, declared tools, and closes the largest remaining under-hashing hole in the cache key: the version of every host binary a recipe ever invoked was never in it.
A target's
PATHbecomes exactly[its tools] : [what it declared] : [heph's builtins]. Nothing else.This removes discovery, not visibility
The sandbox already exposes the host filesystem; dropping the entries only stops bare-name resolution.
/usr/bin/foostill runs.foostops resolving. The failure is always "not found on PATH" with the program and the searched path named — never a silent behaviour change.What stops resolving
Worth being blunt: this is larger than shadowing
cp. Shadowing changes whichcpruns; this changes whetherccruns at all.#!/usr/bin/env python3—envis exec'd by absolute path, then searchesPATHand does not find the interpreter. Declare it.cc/clang/ld/pkg-config— any recipe shelling out to a toolchain it never declared. The largest migration cost in practice.git,xcrun,codesign— usually undeclared and usually already non-hermetic.The driver's
path:option names directories back for a workspace that needs them.Two different PATHs, and that is the trick
Resolving the driver's own program and what the recipe sees are separate things:
PATH— the environment heph itself was launched in — to an absolute path, once. Only for a local spawn: under a runner the shell has to come from the environment the target actually runs in.PATHis the sandbox one above. Nothing ambient.An absolute program path spawns regardless of the child's
PATH, so the shell starts and the recipe inside it still cannot reach an undeclared host binary.Known cost
Which
bashyou get now depends on how heph was launched, where it used to be a hardcoded list. More convenient, less deterministic; the shell stays out of the cache key, because folding the resolved path in would split the remote cache across every machine whosebashlives elsewhere. An acknowledged hole that a provisioned shell would close properly.The stack
Merge bottom-up, and
gh stack syncafter each one lands —masteris squash-only, so the rebase will conflict and the resolution rule in CLAUDE.md applies.PATH— breakingtemplaterule and thetmplappletPATHseam ← base, targetsmasterOnly #451 builds automatically: since #449 a stacked PR is skipped unless it carries
ci/force-ci. Every layer was checked locally on its own —cargo build --workspace --all-targets,cargo clippy --workspace --all-targets, and its unit tests — not just at the top of the stack.🤖 Generated with Claude Code
https://claude.ai/code/session_0181d7hhbYWXT42Z1KQPM29Q