Repository navigation
A user replace in go.work silently overrides the Socket go.mod replace: Go apply and vendor report success and VEX attests not_affected while the build links the user's target #393
Description
[agent] Found by the scheduled Go modules bug-hunt routine (ledger #317).
Summary
In Go workspace mode, a replace in go.work overrides any replace for the same module in a member's go.mod. When the workspace already has a user-authored replace example.com/upstream [v1.0.0] => ../fork in go.work, both agent-mode apply and vendored-mode vendor still write their replace into the root go.mod and report the package patched. That replace is inert: go build links ../fork. apply --check then says "in sync", and vex attests not_affected (with the (vendored) marker in vendored mode).
The same user replace in go.mod is correctly refused ("go.mod already has a user-authored replace example.com/upstream v1.0.0 => ../fork; refusing to overwrite"), and crates/socket-patch-core/src/vex/discover/golang.rs:65-72 notes that "go.work replaces override go.mod replaces in Go", leaving cross-file precedence to the CLI's wiring_conflict gate. That gate never fires for a user (non-Socket) go.work replace.
Impact
The patched bytes aren't in the build, yet every signal (apply exit 0, the apply --check CI gate, and OpenVEX not_affected) says the CVE is mitigated. Committed go.work files with local-fork replaces are common in monorepos.
Repro (Linux, go 1.24.7, hermetic file GOPROXY, GOFLAGS unset because workspace mode rejects -mod=mod)
The fixture has the same shape as tests/e2e_golang_build.rs: example.com/upstream@v1.0.0 is "PRISTINE", and a hand-staged manifest plus blob patch it to "PATCHED", with setup.manual: ["golang"]. ../fork is a local module example.com/upstream returning "FORK".
export GOPROXY=file://$T/proxy GOMODCACHE=$T/modcache GOSUMDB=off GOTOOLCHAIN=local GOFLAGS=
cd consumer # go.mod: require example.com/upstream v1.0.0
printf 'go 1.21\n\nuse .\n\nreplace example.com/upstream v1.0.0 => ../fork\n' > go.work
go run . # OUT: FORK
socket-patch apply # exit 0: "pkg:golang/example.com/upstream@v1.0.0 (via blob)" applied
grep replace go.mod # replace example.com/upstream v1.0.0 => ./.socket/go-patches/example.com/upstream@v1.0.0
go run . # OUT: FORK <- patch not linked
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"This reproduced in 3 fresh fixtures on main:
- a versioned go.work replace (
M v1.0.0 => ../fork), agentapply; - a version-less go.work replace (
M => ../fork), agentapply; - a versioned go.work replace with
socket-patch vendor(vendored mode). There,vendorexit 0 writesreplace … => ./.socket/vendor/golang/<uuid>/…, the build prints FORK, and vex exit 0 givesnot_affected (vendored).
For contrast, the same line in go.mod makes apply fail with user-authored replace … refusing to overwrite (exit 1).
Expected vs actual
- Expected: The user-authored-replace refusal that go.mod gets should also cover
go.work, because that is where Go resolves the replace in workspace mode. Failing that, apply, vendor andapply --checkshould report the redirect as overridden, andvexshould omit the patch. README (vex, step 2) says the attestation "only covers patches that are actually applied"; README line 1307 listsgo.workamong the golang files socket-patch reads. - Actual: exit 0 everywhere,
not_affected, and the build links the fork.
OS × version
| OS | go | mode | go.work replace form | reproduces |
|---|---|---|---|---|
| Linux | 1.24.7 | agent apply |
M v1.0.0 => ../fork |
yes |
| Linux | 1.24.7 | agent apply |
M => ../fork |
yes |
| Linux | 1.24.7 | vendored vendor |
M v1.0.0 => ../fork |
yes |
| Linux | 1.24.7 | agent apply |
same line in go.mod instead | no (correctly refused) |
This is go.mod/go.work text logic, so it doesn't depend on the OS; go.work exists since Go 1.18. Main is f6b7fb9. Hosted mode wasn't exercised (it needs the patch API, which the sandbox blocks).
Suspect code
crates/socket-patch-core/src/vendor/go_mod_edit.rs:173ensure_replace_entry: the user-authored-replace refusal only readsgo.mod.crates/socket-patch-core/src/patch/redirect/golang_local.rs:468verify_go_redirect_stateandcrates/socket-patch-cli/src/commands/vex.rs:1333synthesize_go_patches: neither consultsgo.workreplaces.crates/socket-patch-core/src/vex/discover/golang.rs:65-72defers go.work-over-go.mod precedence to the CLIwiring_conflictgate, which doesn't cover a non-Socket go.work replace.
Related: #392 (an inert replace from a build-graph mismatch), #391 (vex doesn't check the replace version).
Backlog review — 2026-10-08
Priority: P2 → P1. A go.work replacement makes the build use unpatched code while VEX attests the Socket replacement. Supported workspace resolution must be respected.
Activity
mikolalysenko commentedon Sep 30, 2026
[agent] Triaged as priority:p2 (Go). Related to #391/#392 (Socket's go.mod replace isn't the one the build uses), but this covers vendored mode and a user go.work replace, which the
wiring_conflictgate ignores; not clustered until the fix for #391/#392 shows whether ago list -meffective-replace check covers it.Generated by Claude Code
mikolalysenko commentedon Oct 1, 2026
[agent] Re-triage on main
2463257(v5 consolidation, #277): still reproduces. The setup is a go.work withuse .andreplace example.com/upstream v1.0.0 => ../fork. Agentapplyexits 0,go run .printsFORK,apply --checksays "in sync", andvexgivesnot_affected.Generated by Claude Code
mikolalysenko commentedon Oct 1, 2026
[agent] Shares root cause with #458: the agent-mode Go redirect (
patch/redirect/golang_local.rs) never readsgo.work, so a go.workreplace(here) or a go.work that doesn'tusethe root (#458) makes Socket's root go.mod replace inert while apply,apply --checkand VEX still report it patched. Will be fixed together.Generated by Claude Code
mikolalysenko commentedon Oct 1, 2026
[agent] New information: hosted mode is affected too. Reproduced 2× on main
61cfb9b(Linux, go 1.24.7), with both the versioned (M v1.0.0 => ../fork) and the version-less (M => ../fork) go.work replace.The fixture is the shape of
e2e_golang_hosted_build.rs(get <uuid> --mode hostedagainst a local mock ofview/<uuid>+/patches/packagewith agoproxyoverride).go.workisuse .plusreplace example.com/upstream => ../fork.The hosted rewriter refuses a user-authored replace in go.mod ("User-authored conflicting replacements are preserved and the patch is refused", docs/ecosystems.md). It doesn't look at go.work, though, where the user's replace wins.
Also re-checked on
61cfb9b: the agentapplyvariant still reproduces (apply 0, build FORK,--check0, vexnot_affected).Generated by Claude Code
mikolalysenko commentedon Oct 2, 2026
[agent] Shares root cause with #458, #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
mikolalysenko commentedon Oct 2, 2026
[agent] New variant from the Go modules bug-hunt routine (ledger #317), on main
61cfb9b, go 1.24.7, agent mode.The same override happens when the workspace file lives outside the project and is selected with
GOWORK=<path>. socket-patch never readsGOWORK, so even the go.work-aware VEX discovery can't see it:Without the user replace in the external go.work,
GOWORK=<path>builds PATCHED (pass).GOWORK=offwith a root go.work that doesn'tuse .also builds PATCHED, and apply and--checkpass.Generated by Claude Code
mikolalysenko commentedon Oct 3, 2026
[agent] Re-triage from the Go modules bug-hunt routine (ledger #317): this still reproduces on main
045d7ecin agent mode, and it now also reproduces on go 1.21.13 and 1.26.8 as well as 1.24.7. Withreplace example.com/upstream v1.0.0 => ../forkin go.work,applyexits 0, the build prints FORK,apply --checkexits 0, andvexwritesnot_affected.Generated by Claude Code
mikolalysenko commentedon Oct 9, 2026
v5 triage: P2, not a release blocker. Retain Go workspace override handling at P2; resolve it as source-selection work after the basic release path.
This follows the maintainer's release scope: one normally completing CLI instance, prioritizing valid-lockfile patch/install behavior, compatibility, and actionable CLI UX.
mikolalysenko commentedon Oct 11, 2026
[agent] Claiming this issue (with #458, #531; shared root cause: the Go replace writers,
apply --check/vendor --checkand VEX never consult the go.work in effect). Branch: agent/fix-go-work-blind-redirect. Claim-ID: 2026-10-11T16:21:16Z-856761Generated by Claude Code
mikolalysenko commentedon Oct 11, 2026
[agent] Draft PR: #1393
Generated by Claude Code
Refuse Go replaces that go.work defeats
Leave go.work-defeated Go patches out of VEX