[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". [email protected] requires upstream v1.0.0, and [email protected] 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/[email protected] (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:
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 with redirect_golang_not_in_module_graph (nothing written)". Agent apply should apply the same build-graph gate, and ideally use the selected build-list version rather than only the go.mod require lines, so pre-1.17 graphs are covered. apply --check and vex should 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:170 apply_go_redirect: no "is this module@version in the build graph" gate before writing the replace (compare redirect/mod.rs:7366).
crates/socket-patch-core/src/patch/redirect/golang_local.rs:510-525 verify_go_redirect_state: skips the cross-check when the module is absent from require. go.sum here lists only example.com/upstream v1.0.0/go.mod (no h1: 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).
[agent] Found by the scheduled Go modules bug-hunt routine (ledger #317).
Summary
Go discovery crawls the whole
GOMODCACHE. Agent-modeapplythen writesreplace M vX => ./.socket/go-patches/M@vXfor any cachedM@vXthat has a patch, without checking that the project's build graph actually selectsvX. When MVS selects a different version, or whenMisn't in the graph at all, the replace is inert. Even so,applyprints "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, soverify_go_redirect_stateskips therequirecross-check. Its comment says "A module absent fromrequireis harmless — it isn't built", which is false for pre-1.17 module graphs. As a result,apply --checksays "in sync" andvexattestsnot_affectedwhile the binary links the unpatched version.Impact
A project stays vulnerable while every socket-patch signal (apply,
apply --checkas 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/upstreamhas v1.0.0 and v1.0.1, bothGreeting() = "PRISTINE".[email protected]requires upstream v1.0.0, and[email protected]requires upstream v1.0.1. The manifest and blob are hand-staged (same shape astests/e2e_golang_build.rs) and patch upstream v1.0.0 to"PATCHED", withsetup.manual: ["golang"]. Both upstream versions are in GOMODCACHE.Variants:
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.module+go 1.21), [email protected] cached:applyexits 0 and adds the replace to an unrelated project's go.mod.Expected vs actual
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.not_affected); the binary is unpatched.OS × version
godirective--checkpassesf6b7fb9f6b7fb9It 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).