[agent] Found by the scheduled Go modules bug-hunt routine (ledger #317).
Summary
The patch API sends +incompatible Go versions percent-encoded, as pkg:golang/M@v2.0.0%2Bincompatible. This is stated in crates/socket-patch-core/src/utils/purl.rs:284 and golang_local.rs:1898. Agent-mode apply handles that key correctly: it writes replace M v2.0.0+incompatible => ./.socket/go-patches/M@v2.0.0+incompatible, and go run prints PATCHED. apply --check reports the redirect in sync.
socket-patch vex then gets it wrong. It rebuilds the PURL from the go.mod replace with a literal + and looks it up in the manifest by exact key. That lookup misses the %2B key, so vex never finds the go-patches redirect. It falls back to verifying the pristine module cache, which still holds the original bytes, so it reports not_applied and exits 1 with no_applicable_patches.
Impact
Every agent-mode patch for a +incompatible Go module, a common shape for pre-modules v2+ repos, can't be attested. CI that runs socket-patch vex fails, and if the project has other patches, this one is silently left out of the OpenVEX document even though it is applied and built. This fails closed, so there's no false attestation, but the patch is invisible to scanners.
Repro (Linux, go 1.24.7, hermetic file GOPROXY)
The fixture follows tests/e2e_golang_build.rs. A legacy module example.com/legacy at v2.0.0+incompatible has no go.mod in its zip (go synthesizes one). There's a hand-staged .socket/manifest.json keyed pkg:golang/example.com/legacy@v2.0.0%2Bincompatible plus a blob that patches lib.go, with setup.manual: ["golang"].
export GOPROXY=file://$T/proxy GOMODCACHE=$T/modcache GOSUMDB=off GOFLAGS=-mod=mod GOTOOLCHAIN=local
cd consumer # go.mod: require example.com/legacy v2.0.0+incompatible
socket-patch apply # exit 0: "Patched packages: pkg:golang/example.com/legacy@v2.0.0+incompatible"
grep replace go.mod # replace example.com/legacy v2.0.0+incompatible => ./.socket/go-patches/example.com/legacy@v2.0.0+incompatible
go run . # OUT: PATCHED
go mod verify # all modules verified
socket-patch apply --check # exit 0: "Patch redirects are in sync (1 redirect checked)."
socket-patch vex --product pkg:golang/example.com/consumer --output v.json
# Warning: omitting pkg:golang/example.com/legacy@v2.0.0%2Bincompatible from VEX: the patched files still hold the original content (not_applied)
# Error: No applied patches with vulnerability metadata to attest. (exit 1)
--json gives events[0].errorCode: "not_applied" and error.code: "no_applicable_patches".
There are two controls in the same fixture. If the manifest key is changed to a literal + (@v2.0.0+incompatible), vex exits 0 with not_affected. A no-go.mod module at plain v1.0.0 also exits 0 with not_affected. So only the %2B spelling, which is the one the API serves, fails. rollback with the %2B key works: it drops the replace and the build goes back to PRISTINE.
It reproduced twice on main 6e7ef74 in fresh fixtures, once in human and once in --json output.
Expected vs actual
- Expected: README "socket-patch vex": the attestation covers "patches that are actually applied". CLI_CONTRACT.md property 7 (on-disk verification). The go-patches copy is applied and linked, as
go run and apply --check both show, so vex should verify the copy dir and emit not_affected.
- Actual: exit 1,
not_applied, and the patch is omitted.
OS × version
| OS |
go |
socket-patch |
result |
| Linux |
1.24.7 |
main 6e7ef74 |
reproduces (2×) |
| Linux |
1.24.7 |
4.0.0 release |
n/a: apply itself exits 1 ("No packages found that match available patches") on the %2B key, so vex never gets this far |
This isn't a regression. #252 (872b591) fixed the crawler and the redirect for %2B, but the vex synthesis path wasn't updated. The bug is in pure manifest-key logic, so it doesn't depend on the OS.
Suspect code
crates/socket-patch-cli/src/commands/vex.rs:1182-1183 (synthesize_go_patches):
let purl = build_golang_purl(&entry.module, version); // "...@v2.0.0+incompatible"
if !manifest.patches.contains_key(&purl) { // manifest key is "...@v2.0.0%2Bincompatible"
continue;
}
The lookup should compare decoded coordinates, the way golang_local.rs does since #252, or try both spellings. The lookup_entry(entries, &purl) check on the next line probably has the same exposure for a vendor takeover of a +incompatible redirect.
[agent] Found by the scheduled Go modules bug-hunt routine (ledger #317).
Summary
The patch API sends
+incompatibleGo versions percent-encoded, aspkg:golang/M@v2.0.0%2Bincompatible. This is stated incrates/socket-patch-core/src/utils/purl.rs:284andgolang_local.rs:1898. Agent-modeapplyhandles that key correctly: it writesreplace M v2.0.0+incompatible => ./.socket/go-patches/M@v2.0.0+incompatible, andgo runprints PATCHED.apply --checkreports the redirect in sync.socket-patch vexthen gets it wrong. It rebuilds the PURL from the go.mod replace with a literal+and looks it up in the manifest by exact key. That lookup misses the%2Bkey, so vex never finds the go-patches redirect. It falls back to verifying the pristine module cache, which still holds the original bytes, so it reportsnot_appliedand exits 1 withno_applicable_patches.Impact
Every agent-mode patch for a
+incompatibleGo module, a common shape for pre-modulesv2+repos, can't be attested. CI that runssocket-patch vexfails, and if the project has other patches, this one is silently left out of the OpenVEX document even though it is applied and built. This fails closed, so there's no false attestation, but the patch is invisible to scanners.Repro (Linux, go 1.24.7, hermetic file GOPROXY)
The fixture follows
tests/e2e_golang_build.rs. A legacy moduleexample.com/legacyatv2.0.0+incompatiblehas no go.mod in its zip (go synthesizes one). There's a hand-staged.socket/manifest.jsonkeyedpkg:golang/example.com/legacy@v2.0.0%2Bincompatibleplus a blob that patcheslib.go, withsetup.manual: ["golang"].--jsongivesevents[0].errorCode: "not_applied"anderror.code: "no_applicable_patches".There are two controls in the same fixture. If the manifest key is changed to a literal
+(@v2.0.0+incompatible), vex exits 0 withnot_affected. A no-go.mod module at plainv1.0.0also exits 0 withnot_affected. So only the%2Bspelling, which is the one the API serves, fails.rollbackwith the%2Bkey works: it drops the replace and the build goes back to PRISTINE.It reproduced twice on main
6e7ef74in fresh fixtures, once in human and once in--jsonoutput.Expected vs actual
go runandapply --checkboth show, so vex should verify the copy dir and emitnot_affected.not_applied, and the patch is omitted.OS × version
6e7ef74applyitself exits 1 ("No packages found that match available patches") on the%2Bkey, so vex never gets this farThis isn't a regression. #252 (
872b591) fixed the crawler and the redirect for%2B, but the vex synthesis path wasn't updated. The bug is in pure manifest-key logic, so it doesn't depend on the OS.Suspect code
crates/socket-patch-cli/src/commands/vex.rs:1182-1183(synthesize_go_patches):The lookup should compare decoded coordinates, the way
golang_local.rsdoes since #252, or try both spellings. Thelookup_entry(entries, &purl)check on the next line probably has the same exposure for a vendor takeover of a+incompatibleredirect.