chore: bump toolkit to go 1.25 to fix CVEs - #18645
chore: bump toolkit to go 1.25 to fix CVEs#18645Muhammad Falak R Wani (mfrw) wants to merge 9 commits into
Conversation
Muhammad Falak R Wani (mfrw)
commented
Aug 27, 2026
- chore: bump toolkit to go 1.25 to fix CVEs
- chore: install upstream go 1.25 on Ubuntu releases without a distro package
- fix: prefer the distro golang package on Ubuntu releases that have it
- fix: do not overwrite an existing Go toolchain in /usr/local/go
- fix: warn when another go on PATH shadows the installed toolchain
- docs: drop the stale msft-golang note from the Azure Linux prerequisites
- docs: record how and when to advance the pinned Go version
- fix: explain a checksum mismatch instead of printing sha256sum's FAILED
Co-authored-by: Daniel McIlvaney <damcilva@microsoft.com>
…ackage Ubuntu 26.04 packages Go 1.25 as golang-1.25-go, but 22.04 and 24.04 do not package it at all, so installing prerequisites there leaves the toolkit without a usable Go. The apt package also is not on PATH, which is why --fix-go-links exists. Drop golang-1.25-go from the apt list and instead check the Go on PATH, falling back to the pinned upstream tarball from go.dev when it is missing or older than 1.25. The download is verified against a hardcoded SHA256 before anything is unpacked, so a corrupted or tampered archive can neither be installed nor replace an existing toolchain. Unsupported architectures fail with an actionable message rather than a 404. This gives every supported release the same Go install path and puts go and gofmt on PATH without a separate step. Signed-off-by: Muhammad Falak R Wani <falakreyaz@gmail.com>
The previous commit unconditionally downloaded Go from go.dev whenever PATH had no Go 1.25+. That regressed Ubuntu 26.04, where golang-1.25-go exists: hosts that can reach an apt mirror but not the public internet went from working to failing. Try apt first and fall back to go.dev only when the package is genuinely unavailable. Success is decided by the filesystem (/usr/lib/go-1.25/bin/go) rather than by parsing apt output, whose "Candidate:" label is translated and would misfire under a non-English locale. Since Go can now land in either root, go_root() resolves it once and both the install path and --fix-go-links share that result.
The install gate only looked at the go on PATH. A user who had unpacked Go into /usr/local/go without linking it into /usr/bin -- or whose links had been removed -- would silently have that toolchain deleted and, if it was newer than the pinned version, downgraded to 1.25.14. Look for an installed toolchain in either root before installing anything and reuse it when it is 1.25 or newer, so the run only re-creates the symlinks. When /usr/local/go really is too old to keep, say so before removing it instead of doing it silently.
The script links /usr/bin/go, but that is not necessarily the go that runs: Ubuntu's default PATH and sudo's secure_path both list /usr/local/bin ahead of /usr/bin, so a go left there keeps winning. The failure that follows is remote from its cause -- tools.mk checks go_min_version much later, from a different script, and reports only that the version is too old. Name the shadowing binary and what it reports while we are still in the script that can explain it. Compare with -ef rather than by string so that reaching the same file through the /bin -> usr/bin symlink is not mistaken for a shadow.
The document already says Azure Linux 2.0 is unsupported, then a few lines later tells the reader what install-prereqs does on Azure Linux 2.0. Neither half of that note is true any more. toolkit.mk refuses outright when ID=mariner, and prerequisites-mariner.sh installs plain 'golang' with no msft-golang handling of any kind. Remove it, and say plainly why 2.0 is out.
Go supports only its two most recent majors, so a hardcoded GO_VERSION starts aging the day it lands. Nothing said so, and nothing said that the minimum version is also spelled out in go.mod, the coverage workflow and two prerequisite documents -- so the obvious next bump is a one-line edit that leaves four other places behind. Write the cadence and the full list of coupled locations down next to the pin, and point the script's header comment at it.
GO_VERSION and the two GO_SHA256_* constants have to move together, and
the most likely way to get this wrong is to bump one and forget the
others. All that told you was:
/tmp/tmp.XXXX/go.tar.gz: FAILED
sha256sum: WARNING: 1 computed checksum did NOT match
which does not say which release was expected, which constant is stale,
where to find the right value, or whether anything was left half-installed.
It reads like a compromised download even when it is a one-line editing
mistake -- and the reverse mistake, reading a real tampering incident as a
stale constant, is worse.
Print the release, both sums, the exact constant for the architecture in
hand, and the fact that nothing was installed.
Every workflow that builds the toolkit runs make with REBUILD_TOOLS=y,
and tools.mk gates that on the go directive in toolkit/tools/go.mod.
GitHub's ubuntu-latest ships Go 1.24, so raising the directive to 1.25
turns each of those jobs into:
tools.mk:34: *** Go version 'go1.24.13' is less than minimum
required version 'go1.25.0'. Stop.
go-test-coverage.yml is unaffected only because it is the one workflow
that already installs its own Go.
Add a composite action that reads the directive and hands the release
series to actions/setup-go, and use it in the five workflows that build
tools. Deriving the version keeps CI from drifting away from the gate it
has to satisfy; asking for the series rather than the exact directive
keeps picking up patch releases instead of pinning to whichever one the
minimum happens to name.
sudo resets PATH to secure_path, which does not include the runner tool
cache, so the make calls have to run under sudo env "PATH=$PATH" for any
of this to be visible to the build. go-test-coverage.yml already does
this.
Two of the five, check-package-cgmanifest and check-source-signatures,
pass today only because they skip make when no spec changed, so they
would have failed on the next spec-touching PR regardless.
There was a problem hiding this comment.
Pull request overview
Updates the toolkit’s Go toolchain requirements and installation paths, while aligning CI and package manifests.
Changes:
- Raises the toolkit minimum to Go 1.25 and updates dependencies.
- Adds reusable CI setup and Ubuntu installation logic.
- Reorders
chkconfigbefore dependent packages in manifests.
Reviewed changes
Copilot reviewed 14 out of 15 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
toolkit/tools/go.mod |
Updates Go and module requirements. |
toolkit/tools/go.sum |
Refreshes dependency checksums. |
toolkit/resources/manifests/package/update_manifests.sh |
Corrects package generation order. |
toolkit/resources/manifests/package/pkggen_core_x86_64.txt |
Reorders x86_64 packages. |
toolkit/resources/manifests/package/pkggen_core_aarch64.txt |
Reorders Arm64 packages. |
toolkit/docs/building/prerequisites-ubuntu.sh |
Adds version-aware Go installation. |
toolkit/docs/building/prerequisites-ubuntu.md |
Documents Ubuntu Go setup and updates. |
toolkit/docs/building/prerequisites-mariner.md |
Updates Azure Linux requirements. |
.github/workflows/go-test-coverage.yml |
Updates the expected Go version. |
.github/workflows/check-srpm-duplicates.yml |
Configures Go for toolkit builds. |
.github/workflows/check-source-signatures.yml |
Configures Go and preserves its PATH. |
.github/workflows/check-package-cgmanifest.yml |
Configures Go and preserves its PATH. |
.github/workflows/check-package-builds.yml |
Configures Go for package checks. |
.github/workflows/check-circular-deps.yml |
Configures Go for graph generation. |
.github/actions/setup-toolkit-go/action.yml |
Adds shared toolkit Go setup. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
|
||
| # Returns zero when the given go binary is 1.25 or newer, e.g. go1.25.14, go1.26.0, go1.31rc1. | ||
| go_is_supported() { | ||
| "$1" version 2>/dev/null | grep -qE 'go1\.(2[5-9]|[3-9][0-9])' |
| module github.com/microsoft/azurelinux/toolkit/tools | ||
|
|
||
| go 1.24.0 | ||
| go 1.25.0 |
|
|
||
| - **Azure Linux 2.0 (CBL-Mariner)**: Validated with `msft-golang-1.24.1` | ||
| - **Azure Linux 3.0**: Validated with `golang-1.24.3` | ||
| - **Azure Linux 2.0 (CBL-Mariner)**: Not supported. `install-prereqs` refuses outright because 2.0 |
There was a problem hiding this comment.
2.0 is not supported we can remove this.
| @@ -7,10 +7,11 @@ This page outlines the requirements for building with the Azure Linux toolkit on | |||
There was a problem hiding this comment.
[Toolio Iglesias🗺️] Doc-vs-behavior check: the doc says install-prereqs "refuses outright" on Azure Linux 2.0, but prerequisites-mariner.sh (unchanged in this PR) has no distro or Go-version guard — it unconditionally runs tdnf -y install golang … with no refusal path. On 2.0 it would install the older golang package and then fail later in make with a confusing Go-version error rather than a clean refusal. Recommend either adding the refusal logic to the script so behavior matches, or softening the wording to describe what actually happens (unsupported; build fails on the Go-version check). Aligns with the existing draft note to remove the 2.0 line.
| if go_is_supported go; then | ||
| echo "Found $(go version), skipping Go installation..." | ||
| else | ||
| if go_existing_root="$(go_root)" && go_is_supported "$go_existing_root/bin/go"; then |
There was a problem hiding this comment.
Instead of having to pass potential Go commands into go_is_supported why not make go_is_supported check all paths automatically and accept no arguments? This would also make this if-else ladder a bit cleaner.
| link_go "$go_installed_root" | ||
| fi | ||
| else | ||
| echo "Skipping installation of prerequisite packages..." | ||
| fi | ||
|
|
||
| # Fix go 1.24 links if requested | ||
| # Fix go 1.25 links if requested | ||
| if [ "$FIX_GO_LINKS" = true ]; then | ||
| echo "Creating Go symlinks..." | ||
| ln -vsf /usr/lib/go-1.24/bin/go /usr/bin/go | ||
| ln -vsf /usr/lib/go-1.24/bin/gofmt /usr/bin/gofmt | ||
| if go_link_root="$(go_root)"; then | ||
| echo "Creating Go symlinks from $go_link_root..." | ||
| link_go "$go_link_root" |
There was a problem hiding this comment.
Same idea for link_go: we always pass the value from go_root into it, so why not call go_root from inside link_go and simplify things for the caller?
Also, the echo "Creating Go symlinks from $go_link_root..." call could be made from inside link_go as well. Right now we're inconsistent in printing that line btw.
| go_installed_root="$(go_root)" | ||
| link_go "$go_installed_root" |
There was a problem hiding this comment.
This seems to go against the FIX_GO_LINKS value, if it's false. Also, when it's true we end up linking twice. Why not do it only once inside if [ "$FIX_GO_LINKS" = true ]; below?
| if go_is_supported go; then | ||
| echo "Found $(go version), skipping Go installation..." | ||
| else | ||
| if go_existing_root="$(go_root)" && go_is_supported "$go_existing_root/bin/go"; then | ||
| # Installed, just not on PATH. Relinking below is enough, and reinstalling would | ||
| # overwrite a toolchain that may well be newer than the one pinned here. | ||
| echo "Found $("$go_existing_root/bin/go" version) in $go_existing_root, reusing it..." | ||
| else | ||
| echo "Checking apt for '$GO_APT_PACKAGE' (not packaged before Ubuntu 26.04)..." | ||
| if apt install -y "$GO_APT_PACKAGE" && [ -x "$GO_APT_ROOT/bin/go" ]; then | ||
| echo "Installed $GO_APT_PACKAGE from apt." | ||
| else | ||
| echo "'$GO_APT_PACKAGE' is unavailable on this release, using the upstream toolchain..." | ||
| go_arch="$(dpkg --print-architecture)" | ||
| case "$go_arch" in | ||
| amd64) go_sha256="$GO_SHA256_AMD64"; go_sha256_var=GO_SHA256_AMD64 ;; | ||
| arm64) go_sha256="$GO_SHA256_ARM64"; go_sha256_var=GO_SHA256_ARM64 ;; | ||
| *) | ||
| echo "ERROR: no upstream Go build is pinned for architecture '$go_arch'." >&2 | ||
| echo "Install Go $GO_VERSION or newer manually, then re-run with --no-install-prereqs." >&2 | ||
| exit 1 | ||
| ;; | ||
| esac | ||
|
|
||
| echo "Installing Go $GO_VERSION ($go_arch) from https://go.dev/dl..." | ||
| go_tmp_dir="$(mktemp -d)" | ||
| trap 'rm -rf "$go_tmp_dir"' EXIT | ||
| curl -fsSL -o "$go_tmp_dir/go.tar.gz" \ | ||
| "https://go.dev/dl/go${GO_VERSION}.linux-${go_arch}.tar.gz" | ||
| if echo "$go_sha256 $go_tmp_dir/go.tar.gz" | sha256sum --status -c -; then | ||
| echo "Checksum OK." | ||
| else | ||
| echo "ERROR: go${GO_VERSION}.linux-${go_arch}.tar.gz does not match its pinned checksum." >&2 | ||
| echo " expected: $go_sha256" >&2 | ||
| echo " actual: $(sha256sum < "$go_tmp_dir/go.tar.gz" | cut -d ' ' -f 1)" >&2 | ||
| echo "Either the download was corrupted or tampered with, or GO_VERSION was changed in" >&2 | ||
| echo "$0 without refreshing $go_sha256_var. The expected value for a given release is" >&2 | ||
| echo "published at https://go.dev/dl. Nothing has been installed." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| if [ -e /usr/local/go ]; then | ||
| echo "WARNING: replacing the Go installation in /usr/local/go, it is older than $GO_VERSION." >&2 | ||
| fi | ||
| rm -rf /usr/local/go | ||
| tar -C /usr/local -xzf "$go_tmp_dir/go.tar.gz" | ||
| fi | ||
| fi | ||
|
|
||
| go_installed_root="$(go_root)" | ||
| link_go "$go_installed_root" | ||
| fi | ||
| else | ||
| echo "Skipping installation of prerequisite packages..." | ||
| fi |
There was a problem hiding this comment.
This has gotten a bit lengthy. Could you extract the Go installation steps entirely into their own function (including fixing the links)?
| name: "Set up the toolkit's Go" | ||
| description: "Installs the Go release the toolkit requires, as declared by toolkit/tools/go.mod." | ||
| runs: | ||
| using: "composite" | ||
| steps: | ||
| - name: Determine the required Go version | ||
| id: required-go | ||
| shell: bash | ||
| run: | | ||
| # toolkit/scripts/tools.mk gates every REBUILD_TOOLS=y build on the 'go' directive, so | ||
| # read it here instead of hardcoding a version that would drift away from it. | ||
| directive="$(awk '/^go [0-9]+\.[0-9]+/ { print $2; exit }' toolkit/tools/go.mod)" | ||
| if [ -z "$directive" ]; then | ||
| echo "ERROR: no 'go' directive found in toolkit/tools/go.mod" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| # Ask for the release series rather than the exact directive: the directive is a minimum, | ||
| # and installing it verbatim would hold CI on a patch release that is missing later | ||
| # security fixes. | ||
| echo "version=$(echo "$directive" | cut -d. -f1,2)" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Set up Go | ||
| uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 | ||
| with: | ||
| go-version: "${{ steps.required-go.outputs.version }}" | ||
|
|
||
| - name: Report the active Go version | ||
| shell: bash | ||
| run: go version && command -v go |
There was a problem hiding this comment.
I think we should be using toolkit's install-prereqs-and-configure or install-prereqs target instead of custom-picking Go now. With this change, we're introducing parallel logic to install requirements for the toolkit, so it may get out of sync with install-prereqs-and-configure and requires extra maintenance.
Extra benefit: install-prereqs* also puts the new binary under a path seen by sudo, so we could drop all env PATH="$PATH".
| the script names it and warns that it will be used instead. Remove it, or the build will later fail | ||
| its Go version check. |
There was a problem hiding this comment.
The wording of the last sentence is a bit strong. The user doesn't have to do anything, if the /usr/local/bin version of Go is at or above the required version.
| checksums are version-specific, and a stale one aborts the install with a message naming the | ||
| constant that needs refreshing. | ||
|
|
||
| Bumping the *major* release (1.25 → 1.26) additionally requires, all in one change: |
There was a problem hiding this comment.
Major is the first digit.
| Bumping the *major* release (1.25 → 1.26) additionally requires, all in one change: | |
| Bumping the *minor* release (1.25 → 1.26) additionally requires, all in one change: |
|
|
||
| - `--fix-go-links`: Creates symbolic links for Go binaries to make them available in your PATH | ||
| - `--fix-go-links`: Re-creates the `/usr/bin` symlinks for the Go binaries, pointing them at | ||
| whichever Go root is installed (`/usr/lib/go-1.25` or `/usr/local/go`). The prerequisites |
There was a problem hiding this comment.
Nit-pick to make future updates easier:
| whichever Go root is installed (`/usr/lib/go-1.25` or `/usr/local/go`). The prerequisites | |
| whichever Go root is installed (`/usr/lib/go-1.<minor_version>` or `/usr/local/go`). The prerequisites |
| module github.com/microsoft/azurelinux/toolkit/tools | ||
|
|
||
| go 1.24.0 | ||
| go 1.25.0 |
There was a problem hiding this comment.
Considering Copilot's comment: maybe let's go to at least 1.26 now?
| module github.com/microsoft/azurelinux/toolkit/tools | ||
|
|
||
| go 1.24.0 | ||
| go 1.25.0 |
There was a problem hiding this comment.
This makes the PR check fail, because the worker images are running the old 1.24.X version of Go. It's an issue with the workers, not this change, but please hold off on merging this until we fix it. I'm looking into it right now.