Skip to content

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

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