Repository navigation
In a go.work workspace where another member requires a newer version, Go apply writes an inert replace for the member's older version, and apply --check and VEX report it patched while the workspace build links the unpatched module #1751
Description
[agent] Found by the scheduled Go modules bug-hunt routine (ledger #317).
Summary
In a go.work workspace, the build list is computed over every use member's requirements, so MVS picks the highest version any member requires. The Go version gate added in #1403 (go_build_list::project_selection) reads only the current member's go.mod/go.sum. When member a requires example.com/upstream v1.0.0 and member b requires v1.1.0, the workspace builds a with v1.1.0, yet apply in a writes replace example.com/upstream v1.0.0 => ./.socket/go-patches/..., apply --check says "in sync", and vex attests not_affected for example.com/upstream@v1.0.0. The replace is inert and the workspace build links the unpatched module.
This is the workspace variant of #391/#392 (fixed by #1403). The code comment on project_selection already notes that other members' requirements count in workspace mode, but only uses that to relax NotInGraph to Unknown; a Selected answer from the member's own require is trusted as is.
Impact
False "patched" signal and a false VEX attestation in any Go monorepo whose go.work members pin different versions of the patched module. That's the normal state until someone runs go work sync, which is optional. The product links the vulnerable version while the attestation suppresses the finding.
Repro
Hermetic: a file GOPROXY with two pristine versions, no network. $SP is target/release/socket-patch built from main.
T=$PWD/t; UMOD=example.com/upstream
for V in v1.0.0 v1.1.0; do
S=$T/stage/$UMOD@$V; mkdir -p $S $T/proxy/$UMOD/@v
printf 'module %s\n\ngo 1.21\n' $UMOD > $S/go.mod
printf 'package upstream\n\nfunc Greeting() string { return "PRISTINE-%s" }\n' $V > $S/lib.go
echo "{\"Version\":\"$V\"}" > $T/proxy/$UMOD/@v/$V.info; cp $S/go.mod $T/proxy/$UMOD/@v/$V.mod
(cd $T/stage && zip -qrD $T/proxy/$UMOD/@v/$V.zip $UMOD@$V); echo $V >> $T/proxy/$UMOD/@v/list
done
export GOMODCACHE=$T/modcache GOPROXY=file://$T/proxy GOSUMDB=off GOFLAGS= GOTOOLCHAIN=local
W=$T/ws; mkdir -p $W/a $W/b
for m in a:v1.0.0 b:v1.1.0; do n=${m%%:*}; v=${m##*:}
printf 'module example.com/%s\n\ngo 1.21\n\nrequire %s %s\n' $n $UMOD $v > $W/$n/go.mod
printf 'package main\n\nimport (\n\t"fmt"\n\t"%s"\n)\n\nfunc main() { fmt.Println("OUT:", upstream.Greeting()) }\n' $UMOD > $W/$n/main.go
(cd $W/$n && GOWORK=off GOFLAGS=-mod=mod go mod tidy)
done
printf 'go 1.21\n\nuse (\n\t./a\n\t./b\n)\n' > $W/go.work
# Stage .socket/manifest.json + blob in $W/a for pkg:golang/example.com/upstream@v1.0.0
# (lib.go: PRISTINE-v1.0.0 -> "PATCHED"; same shape as tests/e2e_golang_build.rs write_patch).
cd $W/a
go run . # OUT: PRISTINE-v1.1.0 (workspace selects v1.1.0)
$SP apply --offline --ecosystems golang --cwd .; echo $? # Patched ... 0
go run . # OUT: PRISTINE-v1.1.0 (replace is inert)
GOWORK=off go run . # OUT: PATCHED (only outside the workspace)
go list -m example.com/upstream # example.com/upstream v1.1.0
$SP apply --check --ecosystems golang --cwd . # Patches are in sync (1 patch checked). exit 0
$SP vex --offline --product pkg:golang/example.com/a@v0.0.1 --cwd . # not_affected for upstream@v1.0.0, exit 0Expected vs actual
Expected, per docs/ecosystems.md "Go workspaces (go.work)": "A replace names an exact module version, so it only works while the build selects that version … apply and scan --mode hosted refuse to write a replace for any other version … apply --check fails and vex leaves the patch out (go_version_not_selected)". The same section says apply checks the go.work in effect before writing. The build here selects v1.1.0, so apply should refuse (or at least warn with go_version_not_selected), apply --check should fail, and vex should omit the patch.
Actual: apply exits 0 and writes the inert replace, apply --check exits 0 ("in sync"), and vex emits not_affected for example.com/upstream@v1.0.0. Running go work sync first (which bumps a/go.mod to v1.1.0) makes apply refuse correctly, so the gate works only once the member's go.mod already matches the workspace.
Matrix
| OS | go | apply writes inert replace (exit 0) | apply --check in sync |
vex not_affected |
workspace build |
|---|---|---|---|---|---|
| Linux | 1.22.12 | yes | yes | yes | PRISTINE-v1.1.0 |
| Linux | 1.24.7 | yes (2/2) | yes | yes | PRISTINE-v1.1.0 |
| Linux | 1.26.8 | yes | yes | yes | PRISTINE-v1.1.0 |
| macOS / Windows | — | untested (no probe branch this run) |
Tested on main b787454 (5.0.1+171). Not a regression: before #1403 there was no version gate at all. Vendored and hosted mode weren't tested in this run. The hosted gate (patch/redirect/mod.rs ~L9271) and the vendored gate call the same go_build_list helpers, so they likely behave the same way.
Suspect code
crates/socket-patch-core/src/vendor/go_build_list.rs:113(project_selection): reads only<project_root>/go.modandgo.sum. In workspace mode (go_consumer_sync::workspace_in_effect), the selected version is the max over allusemembers' requires (and go.work's own), soSelectedis trustworthy only if no other member requires a higher version.- Callers:
patch/redirect/golang_local.rs:522and:608(apply /--check), andcommands/vex.rs:993.
Generated by Claude Code
Activity
go: run 13 ledger (filed #1751, #549 still open)
mikolalysenko commentedon Oct 27, 2026
[agent] Triaged as p2 (Go modules). Confirmed on main:
go_build_list::project_selectionreads only the member's own go.mod/go.sum, and in workspace mode it only relaxesNotInGraphtoUnknown, so aSelectedanswer is trusted even when anotherusemember requires a higher version. Not a duplicate: #531/#1393 cover conflicting and ignored replaces, not version selection across members. No open PR covers it.Generated by Claude Code
mikolalysenko commentedon Oct 27, 2026
[agent] Claiming this issue (shared root cause: Go version gate reads only the member's go.mod, ignoring other go.work members' requirements). Branch: agent/fix-go-workspace-version-selection. Claim-ID: 2026-10-27T22:20:53Z-e51354
Generated by Claude Code
mikolalysenko commentedon Oct 27, 2026
[agent] Draft PR: #1752
Generated by Claude Code
Check other go.work members before Go patching