Skip to content

ci(native-arch): verify the Go tarball before running the compiler out of it - #781

Merged
tannevaled merged 2 commits into
mainfrom
wave62-verify-go-tarball
Oct 4, 2026
Merged

tannevaled merged 2 commits into
mainfrom
wave62-verify-go-tarball

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

Deferred from #778, which fixed the other defect in these same two blocks. Found in the build/CI audit for #768.

What was there

Both remote scripts in native-arch.yml did, in one subshell:

( cd "$workdir" && curl -fsSL "https://go.dev/dl/${tarball}" -o "$tarball" && tar -xzf "$tarball" )

and then put that directory on PATH, set GOROOT to it, and executed go out of it. HTTPS and nothing else — no integrity check at all between downloading a 64 MB archive and running a compiler from it.

Where it runs is part of the finding: the GCC Compile Farm (cfarm433, cfarm95) and a LinuxONE host. Shared machines, under an acceptable-use policy, where we are a guest.

What it is now

Download → verify → extract, with the sum pinned in this file.

curl -fsSL "https://go.dev/dl/${tarball}" -o "$workdir/$tarball"
case "${GOVER}/${GOARCH}" in
  1.26.4/ppc64le) want=53f49b8c…bc22e ;;
  1.26.4/riscv64) want=2b9ba137…40555 ;;
  1.26.4/s390x)   want=1e12a231…b3ca1 ;;
  *)
    echo "== no pinned SHA256 for go${GOVER} on ${GOARCH}"
    echo "== add it from https://go.dev/dl/?mode=json; refusing to run an unverified toolchain"
    exit 1 ;;
esac
got="$(sha256sum "$workdir/$tarball" | cut -d' ' -f1)"
[ "$got" = "$want" ] || { …exit 1; }
( cd "$workdir" && tar -xzf "$tarball" )

Two choices worth arguing for explicitly

The sums are pinned here, not fetched at run time. Fetching go.dev's JSON index alongside the tarball would have been self-updating and needed no maintenance — and would protect against a truncated download and nothing else. A checksum served from the same origin as the file it describes cannot attest to that file. That is the weaker thing wearing the stronger thing's clothes, so it is not what this does.

The *) arm exits 1. The cost of pinning is that bumping GO_VERSION means updating three lines, and that arm is what makes the cost visible rather than silent. A guard that quietly does nothing when it does not recognise its input is precisely the failure mode worth designing against — this repository already has one of those in #778, where || true turned a checkout failure into a green tick.

Verification

The sums come from https://go.dev/dl/?mode=json for go1.26.4, and were cross-checked programmatically against that index after being written into the file, so a transcription slip cannot survive:

ppc64le  in workflow == official index   OK
riscv64  in workflow == official index   OK
s390x    in workflow == official index   OK

The compare logic was exercised locally in all four states, because it will next run on hosts I cannot watch:

case result
matching sum == fake.tar.gz sha256 verified, exit 0
mismatch == SHA256 mismatch, both values printed, exit 1
unpinned GOVER (1.27.1) refusing to run an unverified toolchain, exit 1
unpinned GOARCH (loong64) refusing to run an unverified toolchain, exit 1

The two failing cases matter as much as the passing one: a verification step that cannot be made to fail has not been tested. The local run used shasum -a 256 because this is a mac; the workflow uses sha256sum, which is what the Linux hosts have.

native-arch.yml still parses (jobs: guard, native-cfarm, native-s390x; GO_VERSION: 1.26.4 unchanged).

Applied at both sites

The cfarm job covers ppc64le/riscv64 and the s390x job covers s390x; they are near-duplicate blocks 100 lines apart. Fixing one would have left the other running an unverified toolchain — the same half-fix trap as #778.

This lane is schedule + workflow_dispatch only, so this PR's own checks cannot exercise it; that is why the logic was tested standalone rather than left to be reviewed by reading.

🤖 Generated with Claude Code

tannevaled and others added 2 commits October 4, 2026 22:03
…t of it

Both remote scripts did

    ( cd "$workdir" && curl -fsSL "https://go.dev/dl/${tarball}" -o "$tarball" && tar -xzf "$tarball" )

and then put that directory on PATH and executed `go` from it. HTTPS and nothing
else: no integrity check between downloading a 64 MB archive and running a
compiler out of it, on the GCC Compile Farm and a LinuxONE host -- machines
shared under an acceptable-use policy, where we are a guest.

Now it downloads, verifies SHA256 against a sum pinned in this file, and only
then extracts.

The sums are pinned HERE rather than fetched alongside the tarball on purpose: a
checksum served from the same origin as the file it describes cannot attest to
that file. Fetching go.dev's JSON index at run time would protect against a
truncated download and nothing else, which is the weaker thing wearing the
stronger thing's clothes.

The cost is that bumping GO_VERSION means updating three lines. The `*)` arm is
what makes that cost visible instead of silent: an unpinned version or
architecture exits 1 naming what to add, rather than skipping the check. A guard
that quietly does nothing when it does not recognise its input is the failure
mode worth designing against here.

Sums taken from https://go.dev/dl/?mode=json for go1.26.4, and cross-checked
programmatically against that index after being written into the file, so a
transcription slip cannot survive:

    ppc64le  53f49b8c...bc22e
    riscv64  2b9ba137...40555
    s390x    1e12a231...b3ca1

The compare logic was exercised locally in all four states before pushing --
matching sum passes; a mismatch exits 1 printing both values; an unpinned
GOVER and an unpinned GOARCH each exit 1 with the "refusing to run an
unverified toolchain" message. The local run used `shasum -a 256` since this is
a mac; the workflow uses `sha256sum`, which is what the Linux hosts have.

Applied at both sites. The cfarm job covers ppc64le and riscv64, the s390x job
covers s390x, and they are near-duplicate blocks 100 lines apart -- fixing one
would have left the other running an unverified toolchain.

Deferred from #778, which fixed the other defect in these same two blocks.
Found in the build/CI audit for #768.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit eb302eb into main Oct 4, 2026
9 checks passed
@tannevaled
tannevaled deleted the wave62-verify-go-tarball branch October 4, 2026 22:51
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.

1 participant