Skip to content

Commit ad8d067

Browse files
committed
Stop hosted Go redirects claiming unpatched deps
Hosted mode counted a Go module as redirected whenever any project file held the patch-server origin (an already hosted package-lock.json) or leftover gopatch go.sum lines, even when the go.mod rewrite was refused, so VEX attested modules that still built unpatched. Go deps now count only when the rewriter reports the replace and both go.sum lines in place. A module that go.mod does not require and go.sum does not list (another project's module in the shared module cache) is refused instead of getting an inert replace. Switching a vendored Go module to hosted mode now reverts its vendored copy and ledger entry first, as cargo and npm already did, so the next vendor run no longer switches it back. Replaces the CLI did not write are left alone: only ./.socket/... paths and exactly patch.socket.dev/gopatch/<uuid> are socket-owned, not a sibling checkout's .socket/vendor copy or a deeper gopatch path. Duplicate socket replaces for one module collapse to one, a refreshed directive keeps its trailing comment, quoted module paths are recognized, and %2B-encoded +incompatible versions resolve in vendor and apply. Assisted-by: Claude Code:claude-opus-5-5
1 parent e024629 commit ad8d067

12 files changed

Lines changed: 757 additions & 67 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,41 @@ into the new version's section — see docs/releasing.md.
345345

346346
### Fixed
347347

348+
- **Hosted Go redirects no longer claim patches that did not land.**
349+
`scan`/`get --mode hosted` counted a Go module as redirected (recorded
350+
it in the redirect ledger, so `vex` attested it) whenever any project
351+
file contained the patch-server origin — e.g. an already hosted
352+
`package-lock.json` — or leftover `patch.socket.dev/gopatch/…` go.sum
353+
lines, even when the go.mod rewrite was refused. A Go module now counts
354+
only when its go.mod `replace` and both go.sum lines are in place. A
355+
module that go.mod does not require and go.sum does not list at the
356+
patched version (another project's module found in the shared module
357+
cache) is refused with `redirect_golang_not_in_module_graph` instead of
358+
getting an inert `replace`.
359+
- **Switching a vendored Go module to hosted mode cleans up the vendored
360+
copy.** `scan`/`get --mode hosted` rewrote the vendored `replace` but
361+
left `.socket/vendor/golang/<uuid>/` and its vendor-ledger entry
362+
behind, so the next `vendor` run switched the module back. The hosted
363+
run now reverts the vendored state first, as it already did for cargo
364+
and npm.
365+
- **Go `replace` directives the CLI did not write are left alone.** A
366+
replace onto another checkout's vendored copy
367+
(`../other/.socket/vendor/golang/…`) or onto a module under
368+
`patch.socket.dev/gopatch/` other than `<patch uuid>` was treated as
369+
socket-owned and could be rewritten or removed. Only
370+
`./.socket/vendor/golang/…`, `./.socket/go-patches/…` and exactly
371+
`patch.socket.dev/gopatch/<uuid>` are now owned.
372+
- **Go replace edits keep go.mod valid and readable.** A go.mod carrying
373+
two socket-owned `replace` lines for one module (for example after a
374+
merge) is collapsed to one instead of leaving a duplicate go rejects;
375+
refreshing a directive keeps its trailing `// comment`; and quoted
376+
module paths (`"github.com/x/y"`) are recognized, so a quoted user
377+
replace is no longer duplicated and a quoted `require` still gets the
378+
version check.
379+
- **`+incompatible` Go modules resolve in vendor and apply.** Purls that
380+
spell the version `v2.0.0%2Bincompatible` are now percent-decoded, so
381+
the module is found in the module cache and the `replace` and copy
382+
directory carry the real `+incompatible` version.
348383
- **A vendoring-service outage no longer re-vendors packages.** An npm
349384
re-run (every lock flavor, `bun.lockb` included) re-acquired its tarball
350385
from whichever source answered — the service's prebuilt, or a local pack

‎crates/socket-patch-cli/CLI_CONTRACT.md‎

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

‎crates/socket-patch-cli/src/commands/apply.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,7 @@ async fn try_local_go_apply(
414414
// `pkg_path` is the pristine, case-encoded module-cache dir; `module`/
415415
// `version` are the decoded PURL components keying the copy + `replace`.
416416
let (module, version) = parse_golang_purl(purl)?;
417+
let (module, version) = (&*module, &*version);
417418
Some(
418419
apply_go_redirect(
419420
purl,

‎crates/socket-patch-cli/src/commands/scan/hosted.rs‎

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -947,10 +947,9 @@ pub(crate) async fn run_redirect_selected(
947947
/// records and the confirmation probe key on; everything the probe
948948
/// needs AFTER the rewrite to decide whether the dep was actually
949949
/// redirected (artifact URL, registry index URL, fail-closed maven's
950-
/// suffixed version, golang's content-addressed module path) already
951-
/// rides the override. The single vector is filtered in place by every
952-
/// withhold/refusal step, and the rewriters' `overrides` slice is
953-
/// materialized from it once, after the last filter.
950+
/// suffixed version) already rides the override. The single vector is
951+
/// filtered in place by every withhold/refusal step, and the rewriters'
952+
/// `overrides` slice is materialized from it once, after the last filter.
954953
struct Candidate {
955954
purl: String,
956955
dep: DepOverride,
@@ -1176,7 +1175,10 @@ pub(crate) async fn run_redirect_selected(
11761175
// VENDORED — for cargo a committed `[patch.crates-io]` path entry, a
11771176
// detached Cargo.lock entry, a committed copy, and a vendored ledger
11781177
// entry; for the npm family a `file:./.socket/vendor/…` lock resolution
1179-
// (plus a berry `resolutions` pin) and its committed tarball. The hosted
1178+
// (plus a berry `resolutions` pin) and its committed tarball; for golang
1179+
// the vendor-owned go.mod `replace`, its committed module copy, and its
1180+
// ledger entry (left behind, a later `vendor` run takes the module back
1181+
// and the modes flip-flop). The hosted
11801182
// rewriters know nothing about that wiring: cargo then refuses every
11811183
// `--locked` build over the now-unused `[patch]` entry while this run
11821184
// reports success, and the npm rewriters either hijack the vendored
@@ -1192,7 +1194,9 @@ pub(crate) async fn run_redirect_selected(
11921194
// whose vendored state cannot be cleanly reverted (revert failure, or
11931195
// vendored wiring with a missing/corrupt ledger) is REFUSED — skipped
11941196
// with an actionable error — never half-migrated.
1195-
let takeover_capable = |p: &str| p.starts_with("pkg:cargo/") || p.starts_with("pkg:npm/");
1197+
let takeover_capable = |p: &str| {
1198+
p.starts_with("pkg:cargo/") || p.starts_with("pkg:npm/") || p.starts_with("pkg:golang/")
1199+
};
11961200
let mut takeover_pre_warnings: Vec<serde_json::Value> = Vec::new();
11971201
// Dry-run takeover previews: `(purl, uuid)` pairs whose vendored state
11981202
// the wet run would revert and then redirect. Withheld from the
@@ -2010,17 +2014,20 @@ pub(crate) async fn run_redirect_selected(
20102014
if purl.starts_with("pkg:cargo/") {
20112015
return rewrite.confirmed_cargo_uuids.contains(uuid);
20122016
}
2017+
// Golang likewise: the goproxy `indexUrl` is the bare
2018+
// patch-server origin (present in any other hosted lock), and
2019+
// the socket module's go.sum lines outlive a removed replace.
2020+
if purl.starts_with("pkg:golang/") {
2021+
return rewrite.confirmed_golang_uuids.contains(uuid);
2022+
}
20132023
// The override's own targets: artifact URL; per-dependency
20142024
// registry index URL; fail-closed maven's globally-unique
2015-
// `-socket.<hex8>` suffixed version (never the `.pom` URL);
2016-
// golang's content-addressed `patch.socket.dev/gopatch/<uuid>`
2017-
// module path (go.mod + go.sum never carry a URL).
2025+
// `-socket.<hex8>` suffixed version (never the `.pom` URL).
20182026
let artifact_url = c.dep.artifact_url.as_str();
20192027
let registry = c.dep.registry_override.as_ref();
20202028
let index_url = registry.map(|o| o.index_url.as_str());
20212029
let suffixed_version =
20222030
registry.and_then(|o| o.identifiers.maven_suffixed_version.as_deref());
2023-
let go_module_path = registry.and_then(|o| o.identifiers.go_module_path.as_deref());
20242031
let encoded = socket_patch_core::utils::uri::encode_uri_component(artifact_url);
20252032
final_texts.iter().any(|text| {
20262033
// The rewriters' own predicate — raw, or the `\/`-escaped
@@ -2036,7 +2043,6 @@ pub(crate) async fn run_redirect_selected(
20362043
|| text.contains(encoded.as_str())
20372044
|| index_url.is_some_and(|iu| text.contains(iu))
20382045
|| suffixed_version.is_some_and(|sv| text.contains(sv))
2039-
|| go_module_path.is_some_and(|gm| text.contains(gm))
20402046
})
20412047
})
20422048
.map(|c| (c.purl.clone(), c.dep.patch_uuid.clone()))

0 commit comments

Comments
 (0)