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". 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:

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

  1. mikolalysenko commented on Sep 30, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged as priority:p2 (Go). Shares root cause with #391: agent-mode Go never checks that its version-pinned go.mod replace matches the version the build graph selects (apply has no gate like hosted mode's redirect_golang_not_in_module_graph; verify_go_redirect_state skips the cross-check for pre-1.17 go.mod, and vex never runs it). Will be fixed together. See also #393.


    Generated by Claude Code

  2. mikolalysenko commented on Oct 1, 2026

    @mikolalysenko
    CollaboratorAuthor

    [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 only module + go 1.21 (no dependency on upstream) and with example.com/upstream@v1.0.0 in GOMODCACHE, apply --offline --ecosystems golang exits 0 ("1 of 1 targeted patch applied") and adds replace example.com/upstream v1.0.0 => ./.socket/go-patches/…. apply --check then 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 use the root module.


    Generated by Claude Code

  3. added a commit that references this issue on Oct 1, 2026
  4. mikolalysenko commented on Oct 1, 2026

    @mikolalysenko
    CollaboratorAuthor

    [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 on 61cfb9b (apply 0, build PRISTINE, --check 0, vex not_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 the vendor/golang.rs mount_go_granted shape:

    socket-patch vendor --vendor-source=service --api-url …   # exit 0
    grep replace go.mod     # replace example.com/upstream v1.0.0 => ./.socket/vendor/golang/<uuid>/example.com/upstream@v1.0.0
    go run .                # OUT: PRISTINE PRISTINE
    socket-patch vex …      # exit 0, "status": "not_affected" (vendored)
    

    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

  5. mikolalysenko commented on Oct 1, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Shares root cause with #509 (and #391/#392): no Go mode determines the build-list-selected version before treating a version-pinned replace as live; #509 is the hosted gate's variant (GoSum::has_module_version counts a /go.mod-only go.sum line). Will be fixed together.


    Generated by Claude Code

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