Skip to content

On a go 1.16 go.mod, Go apply, apply --check and vex trust a stale go.sum zip line left by a downgrade, so they report an inert replace as applied (or refuse a selected version) #1862

Open

Description

[agent] Found by the scheduled Go modules bug-hunt routine (ledger #317).

Summary

For an unpruned module graph (go.mod go 1.16 or older, or no go directive), go_build_list::select treats the highest of the root require version and every go.sum zip (h1:) line version as the version the build selects. go get M@<lower> lowers the require but keeps the old version's go.sum lines until go mod tidy, so after a downgrade the highest zip line is stale and the heuristic picks the wrong version. There are two outcomes:

  1. Silent, unsafe: downgrade below the patched version (require M v1.0.0 → go get M@v0.9.0, with a patch for v1.0.0 in the manifest). apply exits 0 and reports applied. It writes replace M v1.0.0 => ./.socket/go-patches/…, which is inert because the build selects v0.9.0. apply --check exits 0 and vex attests not_affected (inline_mitigations_already_exist), but the binary still links the unpatched module.
  2. False refusal: downgrade to the patched version (require M v1.1.0 → go get M@v1.0.0). apply exits 1, saying "the build selects example.com/upstream v1.1.0, not the patched v1.0.0 … or require v1.0.0". But go.mod already requires v1.0.0 and go list -m reports v1.0.0.

With a go 1.17+ directive, both cases are handled correctly, because the pruned-graph branch trusts require.

Impact

Case 1: a project stays vulnerable while apply, the apply --check CI gate and OpenVEX all say it is patched. Case 2: a valid patch can't be applied, and the error message tells the user to do what they already did.

Repro (Linux, hermetic file GOPROXY, hand-staged manifest + blob in the tests/e2e_golang_build.rs shape)

example.com/upstream v0.9.0 (Greeting()="OLDER") and v1.0.0 ("PRISTINE"). The patch is for v1.0.0 (lib.go → "PATCHED").

export GOPROXY=file://$T/proxy GOMODCACHE=$T/mc GOSUMDB=off GOFLAGS= GOTOOLCHAIN=local GOWORK=off
cat > go.mod <<'EOF'
module example.com/consumer

go 1.16

require example.com/upstream v1.0.0
EOF
# main.go prints upstream.Greeting()
go mod tidy
go get example.com/upstream@v0.9.0     # go.sum keeps the v1.0.0 h1: line
go list -m example.com/upstream         # example.com/upstream v0.9.0
# stage .socket/manifest.json + blob for pkg:golang/example.com/upstream@v1.0.0
socket-patch apply --offline --ecosystems golang --json   # exit 0, action "applied"
grep replace go.mod   # replace example.com/upstream v1.0.0 => ./.socket/go-patches/example.com/upstream@v1.0.0
GOFLAGS=-mod=readonly go run .          # OUT: OLDER   (unpatched v0.9.0)
socket-patch apply --check --ecosystems golang            # exit 0
socket-patch vex --offline --product pkg:golang/example.com/consumer@v0.0.0 --output vex.json
                                        # exit 0, 1 statement: not_affected

For case 2, publish v1.0.0 (PRISTINE) and v1.1.0, require v1.1.0, go mod tidy, then go get example.com/upstream@v1.0.0. apply then exits 1 with the message above, while go run . prints PRISTINE from v1.0.0.

Expected vs actual

Matrix (each cell run twice, both identical)

OS go toolchain go.mod directive main c8e26f6 case 1 main case 2 v5.0.1 case 1 / 2
Linux 1.21.13 1.16 fail (apply 0, check 0, vex not_affected, build OLDER) fail (false refusal) fail / fail
Linux 1.24.7 1.16 fail fail fail / fail
Linux 1.26.8 1.16 fail fail fail / fail
Linux 1.24.7 1.21 pass (refuses: selects v0.9.0) pass n/a
macOS / Windows — — untested (logic is OS-independent)

Suspect code

crates/socket-patch-core/src/vendor/go_build_list.rs:123-128 (the unpruned branch of select): highest = max(required, zips…). A zip line above the root's require can be a dependency's raise or a leftover from a downgrade, and the two can't be told apart from go.mod and go.sum alone. Possible directions: resolve the unpruned case with MVS over the cached go.mod files (the way go_workspace / #1843's graph does), or return Unknown (fail closed) when the zip-highest differs from require and no dependency's go.mod in the cache explains it. The same select feeds the hosted rewriter and vex discovery, so hosted mode very likely shares this. I haven't tested hosted.

Activity

  1. mikolalysenko commented on Oct 29, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged as priority:p2 (Go modules). Not a duplicate of #1840: that issue (open PR #1843) is about replaced_module ignoring a fork required only by a dependency, while this one is the unpruned branch of go_build_list::select trusting a stale go.sum zip line after a downgrade. Different function, different fix, so not clustered.


    Generated by Claude Code

  2. mikolalysenko commented on Oct 30, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (shared root cause: the unpruned branch of go_build_list::select trusts the highest go.sum zip line even above go.mod's own require, so a line a downgrade left behind passes for the selected version). Branch: agent/fix-go-unpruned-stale-zip. Claim-ID: 2026-10-30T07:28:23Z-98a0b3


    Generated by Claude Code

  3. mikolalysenko commented on Oct 30, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #1893 (stacked on #1843, which adds the module-graph reader this fix uses).


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions