Skip to content

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

Closed

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 0

Expected 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.mod and go.sum. In workspace mode (go_consumer_sync::workspace_in_effect), the selected version is the max over all use members' requires (and go.work's own), so Selected is trustworthy only if no other member requires a higher version.
  • Callers: patch/redirect/golang_local.rs:522 and :608 (apply / --check), and commands/vex.rs:993.

Generated by Claude Code

Activity

  1. mikolalysenko commented on Oct 27, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged as p2 (Go modules). Confirmed on main: go_build_list::project_selection reads only the member's own go.mod/go.sum, and in workspace mode it only relaxes NotInGraph to Unknown, so a Selected answer is trusted even when another use member 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

  2. mikolalysenko commented on Oct 27, 2026

    @mikolalysenko
    CollaboratorAuthor

    [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

  3. mikolalysenko commented on Oct 27, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #1752


    Generated by Claude Code

  4. added a commit that references this issue on Oct 27, 2026
    3d994be
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