Repository navigation
Agent-mode Go apply writes a go-patches replace for a module version the build graph doesn't select, reports it applied, and on a go 1.16 go.mod apply --check and vex also report it as patched #392
Description
[agent] Found by the scheduled Go modules bug-hunt routine (ledger #317).
Summary
Go discovery crawls the whole GOMODCACHE. Agent-mode apply then writes replace M vX => ./.socket/go-patches/M@vX for any cached M@vX that has a patch, without checking that the project's build graph actually selects vX. When MVS selects a different version, or when M isn't in the graph at all, the replace is inert. Even so, apply prints "Patched packages: … applied" and exits 0.
Hosted mode already refuses exactly this case with redirect_golang_not_in_module_graph (crates/socket-patch-core/src/patch/redirect/mod.rs:7366, "Its replace would be inert, and confirming it would attest a patch no build links"). The agent path (golang_local::apply_go_redirect) has no equivalent gate.
It gets worse with a go 1.16 (or older) go.mod. Those files don't list transitive requirements, so verify_go_redirect_state skips the require cross-check. Its comment says "A module absent from require is harmless — it isn't built", which is false for pre-1.17 module graphs. As a result, apply --check says "in sync" and vex attests not_affected while the binary links the unpatched version.
Impact
A project stays vulnerable while every socket-patch signal (apply, apply --check as a CI gate, and OpenVEX) says it is patched. The module cache routinely holds several versions of a module, for example from other projects or from before an upgrade, so discovery offers patches for versions this project doesn't build.
Repro (Linux, go 1.24.7, hermetic file GOPROXY)
The upstream module example.com/upstream has v1.0.0 and v1.0.1, both Greeting() = "PRISTINE". mida@v1.0.0 requires upstream v1.0.0, and midb@v1.0.0 requires upstream v1.0.1. The manifest and blob are hand-staged (same shape as tests/e2e_golang_build.rs) and patch upstream v1.0.0 to "PATCHED", with setup.manual: ["golang"]. Both upstream versions are in GOMODCACHE.
export GOPROXY=file://$T/proxy GOMODCACHE=$T/modcache GOSUMDB=off GOFLAGS=-mod=mod GOTOOLCHAIN=local
# consumer go.mod: go 1.16; require ( example.com/mida v1.0.0 ; example.com/midb v1.0.0 )
go mod tidy
go list -m example.com/upstream # example.com/upstream v1.0.1 (MVS selection)
socket-patch apply # exit 0 — "pkg:golang/example.com/upstream@v1.0.0 (via blob)" applied
# go.mod += replace example.com/upstream v1.0.0 => ./.socket/go-patches/...
go build -o app . && ./app # OUT: PRISTINE PRISTINE <- unpatched
socket-patch apply --check # exit 0 — "Patch redirects are in sync (1 redirect checked)."
socket-patch vex --output v.json # exit 0 — "status": "not_affected"Variants:
- Same graph with a
go 1.21go.mod (tidy addsrequire example.com/upstream v1.0.1 // indirect):applystill exits 0 and reports the patch applied, and its ownapply --checkimmediately fails withResolvedVersionMismatch. VEX still attests; that part is Agent-mode Go vex attests not_affected aftergo getupgrades the patched module, although the go-patches replace no longer applies and the build links the unpatched code #391. - Project that doesn't depend on upstream at all (go.mod has only
module+go 1.21), upstream@v1.0.0 cached:applyexits 0 and adds the replace to an unrelated project's go.mod.
Expected vs actual
- Expected: README ("socket-patch vex") says the attestation "only covers patches that are actually applied". The hosted rewriter's documented rule (CLI_CONTRACT.md,
scan --mode hosted) says: "A golang module that go.mod does not require and go.sum does not list at the patched version is outside the build graph and is refused withredirect_golang_not_in_module_graph(nothing written)". Agentapplyshould apply the same build-graph gate, and ideally use the selected build-list version rather than only the go.modrequirelines, so pre-1.17 graphs are covered.apply --checkandvexshould flag or omit such a redirect. - Actual: apply exit 0 (applied), check exit 0 (in sync), vex exit 0 (
not_affected); the binary is unpatched.
OS × version
| OS | go toolchain | go.mod go directive |
socket-patch | apply reports applied | --check passes |
vex attests | binary patched |
|---|---|---|---|---|---|---|---|
| Linux | 1.24.7 | 1.16 | main f6b7fb9 |
yes | yes | yes | no |
| Linux | 1.24.7 | 1.21 | main f6b7fb9 |
yes | no (drift) | yes (#391) | no |
| Linux | 1.24.7 | 1.16 | 4.0.0 release | yes | yes | — | no |
It reproduced twice on main in fresh fixtures. It's platform-independent (go.mod logic). It isn't a regression; 4.0.0 behaves the same way.
Suspect code
crates/socket-patch-core/src/patch/redirect/golang_local.rs:170apply_go_redirect: no "is this module@version in the build graph" gate before writing the replace (compareredirect/mod.rs:7366).crates/socket-patch-core/src/patch/redirect/golang_local.rs:510-525verify_go_redirect_state: skips the cross-check when the module is absent fromrequire.go.sumhere lists onlyexample.com/upstream v1.0.0/go.mod(noh1:zip line for v1.0.0) next to the full v1.0.1 pair, which already shows v1.0.0 isn't built.
Related: #391 (vex doesn't cross-check the replace version at all).
Activity
mikolalysenko commentedon Sep 30, 2026
[agent] Triaged as priority:p2 (Go). Shares root cause with #391: agent-mode Go never checks that its version-pinned go.mod
replacematches the version the build graph selects (apply has no gate like hosted mode'sredirect_golang_not_in_module_graph;verify_go_redirect_stateskips the cross-check for pre-1.17 go.mod, andvexnever runs it). Will be fixed together. See also #393.Generated by Claude Code
mikolalysenko commentedon Oct 1, 2026
[agent] Re-triage from the Go modules bug-hunt (ledger #317): this still reproduces on main
2463257(v5 consolidation, #277), Linux, go 1.24.7. In a project whose go.mod is onlymodule+go 1.21(no dependency on upstream) and withexample.com/upstream@v1.0.0in GOMODCACHE,apply --offline --ecosystems golangexits 0 ("1 of 1 targeted patch applied") and addsreplace example.com/upstream v1.0.0 => ./.socket/go-patches/….apply --checkthen reports "Patch redirects are in sync (1 redirect checked)", exit 0.Related new report: #458, the same "inert replace reported as applied" outcome when a go.work doesn't
usethe root module.Generated by Claude Code
go: run 4 ledger (#458, re-triage #343 #392)
mikolalysenko commentedon Oct 1, 2026
[agent] New information: vendored mode has the same gap. Reproduced 2× on main
61cfb9b(Linux, go 1.24.7). The agent go 1.16 variant also still reproduces on61cfb9b(apply 0, build PRISTINE,--check0, vexnot_affected).This is the same go 1.16 graph as the report (
mida→ upstream v1.0.0,midb→ upstream v1.0.1, MVS selects v1.0.1, the patch targets v1.0.0), with a local mock vendoring service in thevendor/golang.rsmount_go_grantedshape:Hosted mode does have a gate, but the gate misses this go 1.16 shape because it accepts a go.sum
/go.mod-only line. That's filed separately as #509.Generated by Claude Code
mikolalysenko commentedon Oct 1, 2026
[agent] Shares root cause with #509 (and #391/#392): no Go mode determines the build-list-selected version before treating a version-pinned
replaceas live; #509 is the hosted gate's variant (GoSum::has_module_versioncounts a/go.mod-only go.sum line). Will be fixed together.Generated by Claude Code
mikolalysenko commentedon Oct 11, 2026
[agent] Claiming this issue (with #391, #509; shared root cause: no Go path determines the build-list-selected version before treating a version-pinned
replaceas live; each uses its own go.mod/go.sum heuristic). Branch: agent/fix-go-selected-version-gate. Claim-ID: 2026-10-11T22:21:19Z-e670efGenerated by Claude Code
mikolalysenko commentedon Oct 11, 2026
[agent] Draft PR: #1403
Generated by Claude Code
Gate Go replaces on the build-selected version
Expect apply to refuse a bumped Go version