diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 8cefb9c28e..52b3113a52 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -4048,6 +4048,10 @@ running architecture and tag. A missing exact archive leaves the download URL empty and the update unavailable; it must not fall back to another component, architecture or version. This keeps an incomplete release visible as metadata without presenting an unusable or wrong installation path. +ARMv6 and ARMv7 are separate server archives despite both binaries reporting +`GOARCH=arm`; the updater uses the release binary's embedded `GOARM` build +setting to choose between them, and offers neither if that setting is missing +or unrecognised. It must not assume ARMv7 for every ARM installation. Malformed metadata stays a hard error and only the typed over-limit condition may fall through to the feed. Proof: `internal/updates/issue2282_release_metadata_stream_test.go`. diff --git a/internal/updates/manager.go b/internal/updates/manager.go index 19705b18b8..dc1ecb3734 100644 --- a/internal/updates/manager.go +++ b/internal/updates/manager.go @@ -18,6 +18,7 @@ import ( "path/filepath" "regexp" "runtime" + "runtime/debug" "sort" "strconv" "strings" @@ -112,12 +113,20 @@ func updateReleaseDownloadPrefix() string { } func updateReleaseAssetForRuntime(tagName string) (ReleaseAsset, bool) { - targetArch, ok := map[string]string{ - "amd64": "amd64", - "arm64": "arm64", - "arm": "armv7", - "386": "386", - }[runtime.GOARCH] + armVariant := "" + if runtime.GOARCH == "arm" { + // GOARCH alone cannot distinguish the separately published ARMv6 and + // ARMv7 server binaries. Release builds record their GOARM setting. + if info, ok := debug.ReadBuildInfo(); ok { + for _, setting := range info.Settings { + if setting.Key == "GOARM" { + armVariant = setting.Value + break + } + } + } + } + targetArch, ok := updateReleaseArchitecture(runtime.GOARCH, armVariant) if !ok { return ReleaseAsset{}, false } @@ -132,6 +141,20 @@ func updateReleaseAssetForRuntime(tagName string) (ReleaseAsset, bool) { return ReleaseAsset{Name: assetName, BrowserDownloadURL: downloadURL}, true } +func updateReleaseArchitecture(goarch, goarm string) (string, bool) { + switch goarch { + case "amd64", "arm64", "386": + return goarch, true + case "arm": + // GOARM=5 has no server archive. An absent or unknown variant must not + // silently select ARMv7, which may not run on an ARMv6 host. + if goarm == "6" || goarm == "7" { + return "armv" + goarm, true + } + } + return "", false +} + func updateReleaseAPIPath() string { return fmt.Sprintf("/repos/%s/releases", updateReleaseRepo()) } diff --git a/internal/updates/manager_runtime_arch_test.go b/internal/updates/manager_runtime_arch_test.go new file mode 100644 index 0000000000..387e07fd82 --- /dev/null +++ b/internal/updates/manager_runtime_arch_test.go @@ -0,0 +1,25 @@ +package updates + +import "testing" + +func TestUpdateReleaseArchitectureRequiresMatchingARMVariant(t *testing.T) { + for _, tc := range []struct { + goarch, goarm, want string + ok bool + }{ + {goarch: "amd64", want: "amd64", ok: true}, + {goarch: "arm64", want: "arm64", ok: true}, + {goarch: "386", want: "386", ok: true}, + {goarch: "arm", goarm: "6", want: "armv6", ok: true}, + {goarch: "arm", goarm: "7", want: "armv7", ok: true}, + {goarch: "arm", goarm: "5"}, + {goarch: "arm"}, + {goarch: "arm", goarm: "unknown"}, + {goarch: "riscv64"}, + } { + got, ok := updateReleaseArchitecture(tc.goarch, tc.goarm) + if got != tc.want || ok != tc.ok { + t.Errorf("updateReleaseArchitecture(%q, %q) = (%q, %t), want (%q, %t)", tc.goarch, tc.goarm, got, ok, tc.want, tc.ok) + } + } +}