From f6617f4fe85c188e1baf2b36e46951f8a10d4c11 Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Tue, 1 Sep 2026 01:53:34 +0500 Subject: [PATCH] fix(image): put the rust toolchains where a job can actually see them The b19 build survived provisioning and failed its own smoke: `rustup run 1.98.0` found no toolchain. The smoke was right twice over -- the channels were installed system-wide under /usr/local/rustup and exported through /etc/environment, and no job would ever have seen them either: runuser starts the runner without pam_env, so no login file reaches a job's environment. The root-eye view was green while every job saw nothing. rustup now installs in the runner's own home, as the runner, which is where actions-rust-lang/setup-rust-toolchain and a bare cargo resolve toolchains -- the same shape bun already uses in this image. The /usr/local/bin shims point into the runner's cargo bin, and both smoke scripts verify every pinned channel, clippy, rustfmt and the default exactly as the runner user, so a root-visible-only install can never pass again. Workers are disposable one-job containers, so runner ownership shares nothing with a later job. Claude-Session: https://claude.ai/code/session_01LsGid6U5RrQdFvJmvYdGCF --- internal/imagebuild/assets/provision.sh | 37 ++++++++++++------- .../imagebuild/assets/smoke-integration.sh | 25 ++++++------- internal/imagebuild/assets/smoke.sh | 25 ++++++------- internal/imagebuild/orchestrator_test.go | 2 +- 4 files changed, 46 insertions(+), 43 deletions(-) diff --git a/internal/imagebuild/assets/provision.sh b/internal/imagebuild/assets/provision.sh index b87fc3d..4a281ac 100644 --- a/internal/imagebuild/assets/provision.sh +++ b/internal/imagebuild/assets/provision.sh @@ -333,12 +333,22 @@ for toolchain_name in "${toolchain_names[@]}"; do [[ "$(pnpm --version)" == "${toolchain_version}" ]] ;; rustup) - install -o root -g root -m 0755 "${toolchain_archive}" "${toolchain_scratch}/rustup-init" - export RUSTUP_HOME=/usr/local/rustup CARGO_HOME=/usr/local/cargo - install -d -o root -g root -m 0755 "${RUSTUP_HOME}" "${CARGO_HOME}" - "${toolchain_scratch}/rustup-init" -y --no-modify-path --profile minimal \ + # rustup lives in the runner's own home, exactly where + # actions-rust-lang/setup-rust-toolchain and a bare cargo on PATH + # resolve it. It was installed system-wide once and exported through + # /etc/environment -- and no job ever saw it: runuser starts the runner + # without pam_env, so no login file reaches a job's environment. + # Workers are disposable one-job containers, so runner ownership + # shares nothing with a later job. + export RUSTUP_HOME=/home/runner/.rustup CARGO_HOME=/home/runner/.cargo + install -d -o runner -g runner -m 0755 "${RUSTUP_HOME}" "${CARGO_HOME}" + install -o runner -g runner -m 0755 "${toolchain_archive}" "${toolchain_scratch}/rustup-init" + chmod 0755 "${toolchain_scratch}" + runuser -u runner -- env HOME=/home/runner \ + RUSTUP_HOME="${RUSTUP_HOME}" CARGO_HOME="${CARGO_HOME}" \ + "${toolchain_scratch}/rustup-init" -y --no-modify-path --profile minimal \ --default-toolchain none >/dev/null - [[ "$("${CARGO_HOME}/bin/rustup" --version 2>/dev/null)" == "rustup ${toolchain_version} "* ]] + [[ "$(runuser -u runner -- env HOME=/home/runner "${CARGO_HOME}/bin/rustup" --version 2>/dev/null)" == "rustup ${toolchain_version} "* ]] # Every channel the estate pins, with the two components its # rust-toolchain.toml files name, so the action finds them already present. mapfile -t rust_channels < <(jq -r '.channels[]?' <<<"${entry}") @@ -348,20 +358,19 @@ for toolchain_name in "${toolchain_names[@]}"; do exit 1 fi for channel in "${rust_channels[@]}"; do - "${CARGO_HOME}/bin/rustup" toolchain install "${channel}" \ + runuser -u runner -- env HOME=/home/runner \ + "${CARGO_HOME}/bin/rustup" toolchain install "${channel}" \ --profile minimal --component clippy --component rustfmt >/dev/null - [[ "$("${CARGO_HOME}/bin/rustup" run "${channel}" cargo clippy --version)" == "clippy "* ]] - [[ "$("${CARGO_HOME}/bin/rustup" run "${channel}" rustfmt --version)" == "rustfmt "* ]] + [[ "$(runuser -u runner -- env HOME=/home/runner "${CARGO_HOME}/bin/rustup" run "${channel}" cargo clippy --version)" == "clippy "* ]] + [[ "$(runuser -u runner -- env HOME=/home/runner "${CARGO_HOME}/bin/rustup" run "${channel}" rustfmt --version)" == "rustfmt "* ]] done - "${CARGO_HOME}/bin/rustup" default "${rust_default}" >/dev/null - # Readable by the runner without being writable by it: a job must not be - # able to edit a toolchain the next job inherits. - chmod -R a+rX "${RUSTUP_HOME}" "${CARGO_HOME}" + runuser -u runner -- env HOME=/home/runner "${CARGO_HOME}/bin/rustup" default "${rust_default}" >/dev/null + # Shims for jobs that call cargo or rustc without the action; the rustup + # proxy resolves the toolchain through the calling user's home, which + # for every job is the runner's. for shim in rustup cargo rustc rustfmt cargo-clippy cargo-fmt clippy-driver; do ln -sfn "${CARGO_HOME}/bin/${shim}" "/usr/local/bin/${shim}" done - printf 'RUSTUP_HOME=%s\nCARGO_HOME=%s\n' "${RUSTUP_HOME}" "${CARGO_HOME}" \ - >> /etc/environment ;; uv) tar --extract --gzip --file "${toolchain_archive}" \ diff --git a/internal/imagebuild/assets/smoke-integration.sh b/internal/imagebuild/assets/smoke-integration.sh index 0b58b5d..8dbaafe 100644 --- a/internal/imagebuild/assets/smoke-integration.sh +++ b/internal/imagebuild/assets/smoke-integration.sh @@ -129,25 +129,22 @@ for smoke_toolchain in "${smoke_toolchain_names[@]}"; do ;; pnpm) [[ "$(pnpm --version)" == "${expected_version}" ]] ;; rustup) - # rustup owns the whole Rust surface: the estate pins channels in - # rust-toolchain.toml, actions-rust-lang/setup-rust-toolchain resolves - # them through rustup, and the shims on PATH serve jobs that never call - # the action. Every pinned channel must already hold clippy and rustfmt, - # or the per-job download this image exists to remove comes back. - [[ "$(rustup --version 2>/dev/null)" == "rustup ${expected_version} "* ]] - [[ "$(stat --format='%U' -- /usr/local/rustup)" == root ]] - [[ "$(stat --format='%U' -- /usr/local/cargo)" == root ]] - grep -q '^RUSTUP_HOME=/usr/local/rustup$' /etc/environment + # Verified exactly as a job sees it: through the runner user, whose + # home is where the action and the shims resolve toolchains. The + # root-eye view once stayed green while every job saw nothing. + [[ "$(runuser -u runner -- env HOME=/home/runner rustup --version 2>/dev/null)" == "rustup ${expected_version} "* ]] + [[ "$(stat --format='%U' -- /home/runner/.rustup)" == runner ]] + [[ "$(stat --format='%U' -- /home/runner/.cargo)" == runner ]] mapfile -t smoke_rust_channels < <(jq -r '.channels[]?' <<<"${entry}") smoke_rust_default="$(jq -er '.default_channel' <<<"${entry}")" [[ ${#smoke_rust_channels[@]} -gt 0 ]] for smoke_rust_channel in "${smoke_rust_channels[@]}"; do - [[ "$(rustup run "${smoke_rust_channel}" rustc --version)" == "rustc ${smoke_rust_channel}"* ]] - [[ "$(rustup run "${smoke_rust_channel}" cargo clippy --version)" == clippy* ]] - [[ "$(rustup run "${smoke_rust_channel}" rustfmt --version)" == rustfmt* ]] + [[ "$(runuser -u runner -- env HOME=/home/runner rustup run "${smoke_rust_channel}" rustc --version)" == "rustc ${smoke_rust_channel}"* ]] + [[ "$(runuser -u runner -- env HOME=/home/runner rustup run "${smoke_rust_channel}" cargo clippy --version)" == clippy* ]] + [[ "$(runuser -u runner -- env HOME=/home/runner rustup run "${smoke_rust_channel}" rustfmt --version)" == rustfmt* ]] done - [[ "$(rustc --version)" == "rustc ${smoke_rust_default}"* ]] - [[ "$(cargo --version)" == "cargo "* ]] + [[ "$(runuser -u runner -- env HOME=/home/runner rustc --version)" == "rustc ${smoke_rust_default}"* ]] + [[ "$(runuser -u runner -- env HOME=/home/runner cargo --version)" == "cargo "* ]] ;; uv) [[ "$(uv --version)" == "uv ${expected_version}"* ]] diff --git a/internal/imagebuild/assets/smoke.sh b/internal/imagebuild/assets/smoke.sh index dc76ab1..95b3bac 100644 --- a/internal/imagebuild/assets/smoke.sh +++ b/internal/imagebuild/assets/smoke.sh @@ -116,25 +116,22 @@ for smoke_toolchain in "${smoke_toolchain_names[@]}"; do ;; pnpm) [[ "$(pnpm --version)" == "${expected_version}" ]] ;; rustup) - # rustup owns the whole Rust surface: the estate pins channels in - # rust-toolchain.toml, actions-rust-lang/setup-rust-toolchain resolves - # them through rustup, and the shims on PATH serve jobs that never call - # the action. Every pinned channel must already hold clippy and rustfmt, - # or the per-job download this image exists to remove comes back. - [[ "$(rustup --version 2>/dev/null)" == "rustup ${expected_version} "* ]] - [[ "$(stat --format='%U' -- /usr/local/rustup)" == root ]] - [[ "$(stat --format='%U' -- /usr/local/cargo)" == root ]] - grep -q '^RUSTUP_HOME=/usr/local/rustup$' /etc/environment + # Verified exactly as a job sees it: through the runner user, whose + # home is where the action and the shims resolve toolchains. The + # root-eye view once stayed green while every job saw nothing. + [[ "$(runuser -u runner -- env HOME=/home/runner rustup --version 2>/dev/null)" == "rustup ${expected_version} "* ]] + [[ "$(stat --format='%U' -- /home/runner/.rustup)" == runner ]] + [[ "$(stat --format='%U' -- /home/runner/.cargo)" == runner ]] mapfile -t smoke_rust_channels < <(jq -r '.channels[]?' <<<"${entry}") smoke_rust_default="$(jq -er '.default_channel' <<<"${entry}")" [[ ${#smoke_rust_channels[@]} -gt 0 ]] for smoke_rust_channel in "${smoke_rust_channels[@]}"; do - [[ "$(rustup run "${smoke_rust_channel}" rustc --version)" == "rustc ${smoke_rust_channel}"* ]] - [[ "$(rustup run "${smoke_rust_channel}" cargo clippy --version)" == clippy* ]] - [[ "$(rustup run "${smoke_rust_channel}" rustfmt --version)" == rustfmt* ]] + [[ "$(runuser -u runner -- env HOME=/home/runner rustup run "${smoke_rust_channel}" rustc --version)" == "rustc ${smoke_rust_channel}"* ]] + [[ "$(runuser -u runner -- env HOME=/home/runner rustup run "${smoke_rust_channel}" cargo clippy --version)" == clippy* ]] + [[ "$(runuser -u runner -- env HOME=/home/runner rustup run "${smoke_rust_channel}" rustfmt --version)" == rustfmt* ]] done - [[ "$(rustc --version)" == "rustc ${smoke_rust_default}"* ]] - [[ "$(cargo --version)" == "cargo "* ]] + [[ "$(runuser -u runner -- env HOME=/home/runner rustc --version)" == "rustc ${smoke_rust_default}"* ]] + [[ "$(runuser -u runner -- env HOME=/home/runner cargo --version)" == "cargo "* ]] ;; uv) [[ "$(uv --version)" == "uv ${expected_version}"* ]] diff --git a/internal/imagebuild/orchestrator_test.go b/internal/imagebuild/orchestrator_test.go index 70ae123..af4bd09 100644 --- a/internal/imagebuild/orchestrator_test.go +++ b/internal/imagebuild/orchestrator_test.go @@ -164,7 +164,7 @@ func TestRecipeFingerprintIsDeterministic(t *testing.T) { // alias is part of the recipe, so a manifest whose contents changed under an // unchanged alias would otherwise ask the builder to produce different bytes // for a name that is already promoted. - if first != "sha256:6b92afb396a9d31a6772e489343006f2ce05c93a59a641701e2bf49ff8fe23fc" { + if first != "sha256:522a61fbf8567198ac81be2a0b6ffe5cdbc1a7cdf5e8beec13d415db470ab83e" { t.Fatalf("deployed standard recipe fingerprint drifted: %q", first) } smoke, err := SmokeFingerprint(plan)