Skip to content

Go apply writes its replace into a root go.mod that go.work doesn't use, so the workspace build links the unpatched module while apply, apply --check and VEX all report it patched #458

Description

[agent] Found by the scheduled Go modules bug-hunt routine (ledger #317).

Summary

The project root has both a go.mod and a go.work, and the go.work doesn't list the root module (for example use ./svc only, with no use .). Agent-mode apply writes replace example.com/upstream v1.0.0 => ./.socket/go-patches/… into the root go.mod and exits 0 with "1 of 1 targeted patch applied".

In workspace mode, Go only honours replace directives from go.work and from the go.mod files of the modules in its use list. A root go.mod that isn't a member is ignored completely. So every workspace build (cd svc && go build) still links the PRISTINE upstream. Despite that:

  • apply --check reports "Patch redirects are in sync (1 redirect checked)", exit 0.
  • vex attests not_affected, exit 0.

Positive control: with use . added to the same go.work, the identical replace takes effect and the member builds PATCHED. The defect is therefore that the go.work use set isn't consulted.

This is different from #393 (a user replace in go.work overriding ours) and #392 (a replace for a version the graph doesn't select). Here the version is selected and nothing overrides the replace; the file it was written to is simply not part of the build.

Impact

A repo whose go.work leaves out the root module (a root tooling/meta module, or a go.work that lists only the services being developed) stays vulnerable, while all three socket-patch signals (apply, the apply --check CI gate and OpenVEX) say it's patched. Nothing warns.

Repro (Linux, hermetic file GOPROXY, same fixture shape as tests/e2e_golang_build.rs)

export GOTOOLCHAIN=local GOSUMDB=off GOENV=off GOFLAGS= GOPROXY=file://$T/proxy GOMODCACHE=$T/modcache
# example.com/upstream v1.0.0: Greeting() = "PRISTINE"; .socket/manifest.json + blob patch it to "PATCHED",
# setup.manual = ["golang"]
# root/go.mod:      module example.com/consumer / go 1.18   (with or without `require example.com/upstream v1.0.0`)
# root/svc/go.mod:  module example.com/svc / go 1.18 / require example.com/upstream v1.0.0 ; main prints upstream.Greeting()
# root/go.work:     go 1.18 / use ./svc
(cd svc && go run .)                                  # OUT: PRISTINE
socket-patch apply --offline --ecosystems golang      # exit 0, "1 of 1 targeted patch applied"
grep replace go.mod                                   # replace example.com/upstream v1.0.0 => ./.socket/go-patches/example.com/upstream@v1.0.0
(cd svc && go run .)                                  # OUT: PRISTINE   <- still unpatched
socket-patch apply --check --ecosystems golang        # "Patch redirects are in sync (1 redirect checked).", exit 0
socket-patch vex --offline --product pkg:golang/example.com/consumer --output v.json   # exit 0, "status": "not_affected"

# control: printf 'go 1.18\n\nuse .\nuse ./svc\n' > go.work  ->  (cd svc && go run .)  # OUT: PATCHED

Reproduced 2× per cell on go 1.24.7, and once each on 1.22.12 and 1.26.8.

Expected vs actual

  • Expected: README ("socket-patch vex") says the attestation "only covers patches that are actually applied". docs/ecosystems.md ("Go: directory replaces and go.sum") presents the go.mod replace as the whole mechanism, with apply --check as "a read-only audit that the committed redirects still match". The hosted contract (crates/socket-patch-cli/CLI_CONTRACT.md, golang row) already treats an inert replace as "diagnosed, no ref". When a go.work exists and doesn't use the directory holding the edited go.mod, apply should do one of two things: write the replace where the workspace honours it (go.work, or a member go.mod), or refuse/warn. apply --check and vex should treat that replace as inert.
  • Actual: exit 0 everywhere, and the build is unpatched.

OS × version

OS go agent apply apply --check vex
Linux 1.22.12 fail (unpatched, exit 0) in sync not_affected
Linux 1.24.7 fail in sync not_affected
Linux 1.26.8 fail in sync not_affected
Linux 1.18 not run (dl.google.com is blocked in the sandbox; the proxy toolchain module only exists for 1.21+)
macOS / Windows any not run (probe branches are blocked this run; Windows apply is also blocked by #346)

Vendored mode (vendor) couldn't be exercised: v5 vendor needs the patch service (--vendor-source=service), which this sandbox can't reach. It writes the same root go.mod replace, so it's probably affected too (unverified).

First bad version: not a regression. Release 4.0.0 behaves the same, and so does main 2463257.

Suspect code

  • crates/socket-patch-core/src/patch/redirect/golang_local.rs:169 apply_go_redirect → go_mod_edit::ensure_replace_entry: always edits the root go.mod, and never reads go.work or its use directives.
  • crates/socket-patch-core/src/patch/redirect/golang_local.rs:452 verify_go_redirect_state (apply --check): doesn't check that the root module is a workspace member when go.work exists.
  • crates/socket-patch-core/src/vex/discover/golang.rs:113-120: root go.mod replaces are extracted and checked against the root require even when the sibling go.work excludes .; the same membership gap applies to manifest-less hosted and vendored VEX.

Backlog review — 2026-10-08

Priority: P2 → P1. An inert go.mod outside the go.work selection yields a successful but false safety attestation for the workspace build.

Activity

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

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged as priority:p2 (Go). Shares root cause with #393: the agent-mode Go redirect (patch/redirect/golang_local.rs apply_go_redirect / verify_go_redirect_state) never reads go.work. It always edits the root go.mod and treats that replace as effective, whatever the workspace's use set (this issue) or a go.work replace (#393) says. Will be fixed together. Not a duplicate of #392/#393: here the version is selected and nothing overrides the replace.


    Generated by Claude Code

  3. mikolalysenko commented on Oct 1, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] New information: hosted and vendored modes are affected too. Each reproduced 2× on main 61cfb9b (Linux, go 1.24.7). With this, all three Go modes are confirmed.

    The fixture: the root has go.mod + go.sum, svc/ is a member requiring example.com/upstream v1.0.0, and go.work is go 1.21 / use ./svc (no use .). The patch API is a local mock in the shapes of e2e_golang_hosted_build.rs (hosted: view/<uuid> + /patches/package with a goproxy override) and vendor/golang.rs mount_go_granted (vendored: a granted tarball with sha512 integrity).

    Hosted:

    socket-patch get $UUID --mode hosted --yes --json …   # exit 0, "redirected": 1, "rewrittenFiles": ["go.mod","go.sum"], "warnings": []
    grep replace go.mod svc/go.mod go.work                 # only root go.mod: replace example.com/upstream v1.0.0 => patch.socket.dev/gopatch/<uuid> v1.0.0-socketpatch.1
    (cd svc && go run .)                                   # OUT: PRISTINE   (also PRISTINE on a fresh day-2 machine)
    socket-patch vex … --product pkg:golang/example.com/consumer   # exit 0, "status": "not_affected" (redirected)
    

    Vendored:

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

    So the hosted rewriter (redirect/mod.rs golang arm), vendor/golang.rs vendor_go_module, and the manifest-less VEX discovery (vex/discover/golang.rs:113-120) all have the same go.work membership gap as the agent path.


    Generated by Claude Code

  4. mikolalysenko commented on Oct 2, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Shares root cause with #393, #531: the Go redirect never reads the enclosing go.work. #531 adds a third symptom: applying in a second workspace member writes a conflicting per-member replace, which breaks the workspace build. Will be fixed together.


    Generated by Claude Code

  5. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    v5 triage: P2, not a release blocker. Retain Go workspaces whose root module is excluded from go.work at P2; ordinary member-root lockfile patching takes priority.

    This follows the maintainer's release scope: one normally completing CLI instance, prioritizing valid-lockfile patch/install behavior, compatibility, and actionable CLI UX.

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