Skip to content

Nothing asserts that CI's Go matches the pinned toolchain, so the vulnerability scan can report against a standard library the binary is not built with #128

Description

@jakewan

Context

Surfaced during the pre-PR audit of #130, the toolchain-pin refresh that moved the Go pin from 1.26.5 to 1.27.0. That change is correct; what the audit found is that the invariant it relies on is undefended.

Problem

.claude/rules/toolchain-ci-parity.md states the coupling as unconditional:

CI installs Go via actions/setup-go with go-version-file: go.mod, so the go directive in go.mod — not mise.toml — chooses the CI toolchain.

It is conditional. actions/setup-go reading go-version-file prefers a toolchain directive over the go directive when both are present. The repository has no toolchain line today, so the rule holds — but only by an absence that nothing records, asserts, or checks.

That absence is load-bearing well beyond version hygiene. go tool govulncheck reports standard-library advisories against the Go on PATH, which in CI is whatever setup-go resolved. So the go.mod field CI resolves decides which standard library the vulnerability scan is reporting on, and SECURITY.md rests a stated guarantee on it: "Dependencies and the standard library are scanned for known vulnerabilities."

If a toolchain directive ever lands — go get and go mod tidy both write one when run under a newer toolchain, and Dependabot's gomod ecosystem updates go.mod — CI silently begins building and scanning against that Go instead of the pinned one.

The failure is invisible to every gate the repository currently has:

  • just tidy-check passes — a toolchain line is tidy.
  • The Toolchain currency workflow passes — it reads mise.toml, where the Go pin still agrees with itself.
  • The Vulnerability scan passes — it reports cleanly, against the wrong standard library.
  • just vuln passes locally — it resolves Go from mise, so a developer sees the pinned toolchain's result and never observes the divergence.

The outcome is a stated security posture that becomes false with nothing red. Same class as #115 (CI executing code the repository does not pin), reached through a different mechanism.

Evidence

  • actions/setup-go README, "Breaking changes in V6": "If the toolchain directive is present, its version is used; otherwise, the action falls back to the go directive."
  • .github/workflows/vuln.yml and both Go jobs in .github/workflows/ci.yml resolve Go via go-version-file: go.mod. No toolchain directive and no GOTOOLCHAIN appears anywhere in the repository.
  • .claude/rules/toolchain-ci-parity.md § "go.mod go directive tracks the mise Go pin" states the coupling without naming the precondition it rests on.

Suggested approaches

Options, not a prescription:

  1. Assert the absence in CI — a step that fails when go.mod carries a toolchain directive, or when it carries one disagreeing with the go directive.
  2. Make the value explicit instead — write a toolchain line matching the go directive, so setup-go and mise agree by construction and any later change to it is a visible diff rather than a silent takeover.
  3. Record the precondition in .claude/rules/toolchain-ci-parity.md so a maintainer editing go.mod knows the invariant is conditional. Weakest alone — it addresses the human path but not the automated one, which is where the directive is most likely to arrive.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    buildBuild, CI, and release tooling (e.g. goreleaser, release workflows).securityCross-cutting security / supply-chain concern

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions