Skip to content

ci: pin rustup-init version and verify SHA-256 in the public toolchain template#502

Open
MGudgin wants to merge 1 commit into
mainfrom
user/gudge/pin_rustup_init_sha256
Open

ci: pin rustup-init version and verify SHA-256 in the public toolchain template#502
MGudgin wants to merge 1 commit into
mainfrom
user/gudge/pin_rustup_init_sha256

Conversation

@MGudgin
Copy link
Copy Markdown
Member

@MGudgin MGudgin commented Jun 7, 2026

Summary

Hardens the public-bootstrap rustup install used by lint, fork-PR builds, and the macOS build. The template previously fetched rustup-init from the convenience redirectors https://win.rustup.rs/x86_64 / https://sh.rustup.rs and executed it without any integrity check on every trusted run; the lint job in particular also rewires crates.io to an anonymous public mirror via .azure-pipelines/.cargo/config.public.toml, so a compromise of the redirector or the rustup origin could expand into any compiled artifact the lint job touches.

The template now:

  1. Pins the rustup version via a new rustupInitVersion parameter (default 1.28.2) and fetches from the immutable archive URL static.rust-lang.org/rustup/archive/<ver>/<host-triple>/. That URL is byte-stable for a given (version, triple), so the SHA-256 check below is meaningful — the previous latest URL would defeat any pinned hash the first time rust-lang shipped a new release.
  2. Selects the host triple from the actual agent, not from the build target. Windows agents are always x64 (windows/arm64 builds run on x64 agents and cross-compile via --target), so the Windows download is hardcoded to x86_64-pc-windows-msvc. On Linux / macOS the host arch is read from uname -m and translated to a Rust triple via an explicit per-OS switch; unknown architectures throw with a clear message rather than silently downloading an unrunnable binary. The TARGET toolchain is still installed via rustup-init --target .
  3. Verifies the SHA-256 sidecar from the same origin against the downloaded rustup-init BEFORE executing. On mismatch the job fails loudly with both expected and actual hashes. Matches what rustup itself does for self-update.
  4. Documents the residual threat clearly: SHA-256 sidecar verification defends against integrity-of-payload (in-flight corruption, TLS MITM, truncation) but not against authenticity-of-origin (a compromise of static.rust-lang.org would corrupt both the binary and the sidecar). Defending against the latter requires upstream GPG release-key verification and is out of scope.

Trade-off considered and rejected

Switching the lint job to RustInstaller@1 + CargoAuthenticate (matching Rust.Build.Steps.Official.yml) would remove the public bootstrap entirely BUT also break fork-PR linting — external contributors have no System.AccessToken for the internal Mxc-Azure-Feed. The integrity-verification approach preserves fork-PR linting while still meaningfully hardening trusted runs.

Compatibility

The two existing call sites (.azure-pipelines/jobs/Lint.Job.yml and .azure-pipelines/templates/Rust.Build.Steps.Unofficial.yml) are unchanged: the new rustupInitVersion parameter has a default, so backward compatibility is automatic. Bumping the pin in future is a single-line change in this template. The resolved host / target triples are logged to the job output on every run, so a future cross-compile mismatch is one grep away.

Verification

  • YAML parses (PyYAML): 3 parameters, 1 step
  • No call-site changes needed (default-valued parameter)
  • No Rust code touched -> no cargo build/clippy/test rerun
  • CI rerun deferred to actual pipeline execution on this PR (the lint job exercises the changed template directly)

@MGudgin MGudgin requested a review from a team as a code owner June 7, 2026 00:59
Copilot AI review requested due to automatic review settings June 7, 2026 00:59
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Hardens the Azure Pipelines “public rustup” bootstrap used by fork-PR builds, lint, and macOS builds by pinning the rustup-init version to an immutable archive URL and adding a SHA-256 sidecar verification step prior to execution.

Changes:

  • Add a rustupInitVersion template parameter (default 1.28.2) and download rustup-init from https://static.rust-lang.org/rustup/archive/<ver>/<triple>/.
  • Verify the downloaded rustup-init against the corresponding .sha256 sidecar before running the installer.
  • Expand template documentation to describe the threat model and remaining limitations (integrity vs. origin authenticity).
Show a summary per file
File Description
.azure-pipelines/templates/Rust.Toolchain.Public.yml Pins rustup-init and verifies SHA-256 before running rustup to harden public toolchain bootstrapping.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 1

Comment thread .azure-pipelines/templates/Rust.Toolchain.Public.yml Outdated
MGudgin pushed a commit that referenced this pull request Jun 7, 2026
Copilot review feedback on PR #502: `\ = \` made the
non-Windows download use the **build target** triple. That breaks cross-
compile jobs -- specifically the unofficial linux/arm64 build, which
runs on an `ubuntu-latest` x64 agent and would now try to execute an
`aarch64-unknown-linux-gnu` `rustup-init`.

`rustup-init` must match the agent host architecture; the target
toolchain is selected separately via the `--target \` flag we
already pass.

Fix: detect the host architecture from `uname -m` and translate to a
Rust triple via an explicit `switch` per OS (macOS / Linux). Unknown
host arches throw with a clear message rather than silently downloading
an unrunnable binary. The Windows path is unchanged -- both
windows/x64 and windows/arm64 builds use x64 agents, so the hardcoded
`x86_64-pc-windows-msvc` host triple is correct there.

Also logs the resolved host vs. target triples to the job output so a
future cross-compile mismatch is one `grep` away.

## Verification

  python -c \"import yaml; yaml.safe_load(open(<file>))\"        OK
  parameters: 3, steps: 1                                       OK

No call-site or behaviour change for the in-use jobs as long as the
agent host architecture matches what `uname -m` returns on it
(`x86_64` on the ubuntu-latest pool, `arm64` on the macOS arm64
pool used by `Mac.Build.Job.yml` for the aarch64 build).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@MGudgin
Copy link
Copy Markdown
Member Author

MGudgin commented Jun 7, 2026

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 1 pipeline(s).

…n template

`.azure-pipelines/templates/Rust.Toolchain.Public.yml` is the public-
bootstrap rustup install used by lint, fork-PR builds, and the macOS
build. It previously fetched `rustup-init` from the convenience
redirectors `https://win.rustup.rs/x86_64` and `https://sh.rustup.rs`
and executed it **without any integrity check** on every trusted run.

## Threat

Those URLs are convenience redirectors that always return the latest
rustup release. A compromise of the redirect, the rustup origin, or
the TLS chain would silently propagate into every trusted build that
uses this template. The lint job in particular also rewires crates.io
to an anonymous public mirror via
`.azure-pipelines/.cargo/config.public.toml`, so a compromised
`rustup-init` could expand the blast radius into any compiled
artifact the lint job touches.

## Fix

This template now:

1. **Pins the rustup version** via a new `rustupInitVersion` parameter
   (default `1.28.2`) and fetches from the immutable archive URL
   `static.rust-lang.org/rustup/archive/<ver>/<host-triple>/`. That
   URL is byte-stable for a given (version, triple); the previous
   `latest` URL would defeat any pinned SHA the first time rust-lang
   shipped a new release, which is what made hash pinning impossible
   under the old layout.

2. **Selects the host triple from the actual agent**, not from the
   build target. Windows agents are always x64 (windows/arm64 builds
   run on x64 agents and cross-compile via `--target`), so the
   Windows download is hardcoded to `x86_64-pc-windows-msvc`. On
   Linux / macOS the host arch is read from `uname -m` and
   translated to a Rust triple via an explicit per-OS switch; unknown
   architectures throw with a clear message rather than silently
   downloading an unrunnable binary. The TARGET toolchain is still
   installed via `rustup-init --target \`. The resolved
   host / target triples are logged to the job output so any future
   cross-compile mismatch is one `grep` away.

3. **Verifies the SHA-256 sidecar** from the same origin against the
   downloaded `rustup-init` BEFORE executing. This matches what
   `rustup` itself does for self-update. On mismatch the job fails
   loudly with both expected and actual hashes so an operator can
   audit.

4. **Documents the residual threat clearly:** SHA-256 sidecar
   verification defends against integrity-of-payload (in-flight
   corruption, TLS MITM, truncation) but **not** against
   authenticity-of-origin (a compromise of `static.rust-lang.org`
   itself would corrupt both the binary and the sidecar). Defending
   against the latter requires upstream GPG release-key verification
   and is out of scope here.

## Trade-off considered and rejected

Switching the lint job to `RustInstaller@1` + `CargoAuthenticate`
(matching `Rust.Build.Steps.Official.yml`) would have removed the
public bootstrap entirely BUT also broken fork-PR linting -- external
contributors have no `System.AccessToken` for the internal
`Mxc-Azure-Feed`. The integrity-verification approach preserves
fork-PR linting while still meaningfully hardening trusted runs.

## Compatibility

The two existing call sites
(`.azure-pipelines/jobs/Lint.Job.yml` and
`.azure-pipelines/templates/Rust.Build.Steps.Unofficial.yml`) are
unchanged: the new `rustupInitVersion` parameter has a default, so
backward compatibility is automatic. Bumping the pin in future is a
single-line change in this file.

## Verification

  YAML parses (PyYAML): 3 parameters, 1 step                OK
  No call-site changes needed (default-valued parameter)    OK
  No Rust code touched -> no cargo build/clippy/test rerun  N/A
  CI rerun deferred to actual pipeline execution on this PR
    (the lint job exercises the changed template directly).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@MGudgin MGudgin force-pushed the user/gudge/pin_rustup_init_sha256 branch from 2e1f71d to 65add56 Compare June 7, 2026 01:48
@MGudgin
Copy link
Copy Markdown
Member Author

MGudgin commented Jun 7, 2026

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 1 pipeline(s).

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