Skip to content

Agent-mode Go vex omits an applied patch for a +incompatible module (not_applied, exit 1) because it looks up the manifest with a literal "+" while the API key spells it %2B #484

Description

[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.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent:triagedbugSomething isn't workingbughuntFound by a scheduled package-manager bug-hunt agentpm:goGo modulespriority:p2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions