Wire vendored cargo patches through Cargo.toml and tag the copy - #254
Merged
Mikola Lysenko (mikolalysenko) merged 13 commits intoSep 25, 2026
Merged
Conversation
Vendored cargo patches are now wired through a Socket-owned [patch.crates-io] key in the workspace-root Cargo.toml instead of .cargo/config, so the patch uuid is visible from the manifest alone. Legacy config wiring is migrated on re-run, repair and uuid bumps. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A vendored cargo copy's own Cargo.toml version is now rewritten to <version>+socket.<uuid> (existing build metadata is kept: 2.0.1+zstd.1.5.2.socket.<uuid>), and the detached Cargo.lock entry records that tagged version, with every reference that spells the old version rewritten, in lock formats v1-v4. This is the lock cargo itself writes for the tagged copy, so the patch uuid of the copy that actually builds can be read from Cargo.lock alone. The patched crate now sees the tag in CARGO_PKG_VERSION. A lock that cannot be kept consistent is refused before anything is written (cargo_lock_untaggable). Revert restores the original lock byte for byte. A uuid bump re-tags the copy and lock, and a re-run or repair tags projects vendored before this change (cargo_version_tagged). VEX discovery treats the tagged lock version as the primary identity signal. The old-toolchain e2e tests prefer the rust:1.41-slim and rust:1.56-slim docker images. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Keep a recorded whole-tree inventory when repair tags a pre-tag copy instead of re-baselining it over unverified bytes; the inventory check accepts the copy's Cargo.toml only with exactly this uuid's tag dropped, the cargo inventory carries forward for the same copy, and repair refreshes it only from a member-verified rebuild. - Select Cargo.lock entries by rank (this uuid's tagged copy, another tagged copy, a registry entry, then an untagged sourceless one), so a user's same-version path fork locked beside the copy is never restored, retagged or counted as a multi-source conflict; the revert spells the restored registry entry by its full id, exactly as cargo writes it. - GC keeps an entry whose lock tag is stale while the manifest still wires its copy (cargo re-locks to it; the next re-run retags). - Revert drops the tag the first build locked when no lock originals were recorded (vendored before any Cargo.lock), so a hosted takeover finds the crate again. - Dry runs preview "would tag" and the cargo_lock_untaggable / cargo_copy_untaggable refusals. - lock_inventory lists a sourced Socket-tagged entry with no verifier. - Docs: semver Version equality/ordering sees the tag; 1.56 is built in CI docker; an untagged detached entry counts only beside an untagged copy. - Tests: fork-shape lock units (v1/v4, cargo's exact lock), vendor-flow tests for revert/dry-run/untaggable copies, tag-aware inventory and carry-forward units, VEX discovery copy-tag units, VEX e2e split and ledger leg, and real-cargo cells for a transitive registry dependent, the repair inventory refresh and a lockless vendor + hosted takeover. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A failed Cargo.lock retag write (read-only project root) untags the copy again, through both the vendor re-run and repair's migration. - repair's migration over a copy whose Cargo.toml cannot take the tag reports cargo_version_untagged and writes nothing. - The cargo 1.41 two-version leg now proves the documented remedy: a populated crates.io index in $CARGO_HOME builds and runs both patched copies with no network, instead of only accepting the failure. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The cross-mode guard that refuses to wire [patch.crates-io] on top of a
live hosted redirect read Cargo.toml with a single-line regex, so it saw
only `<crate> = { ... registry = "socket-patch-<uuid>" }`. The hosted
rewriter also writes a standalone `registry = ...` line under a
`[dependencies.<crate>]` header, pins renamed declarations by their
`package` key, and accepts a quoted key — all read as "not redirected".
With a Cargo.lock restored from version control (so the lock probe sees
crates.io again) and no redirect ledger, vendor then reported success on
a project pinned to the hosted registry and patched through
[patch.crates-io] at once: cargo resolves the crate from a non-crates.io
source, which [patch.crates-io] does not cover, so the build either
fails or silently links the unpatched copy.
The probe now reuses the rewriter's own manifest reader, so detection
and writing stay in step, and the refusal names the registry it found.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A cargo patch that edits the crate's own Cargo.toml verifies with the Socket version tag dropped. The whole-tree inventory check drops only THIS entry's tag; the per-file check dropped whatever Socket tag it found, so a copy under <uuidA>'s path whose manifest is tagged for <uuidB> — a hand edit, a merged vendored tree, a half-applied uuid bump — verified clean and repair reported it healthy, while VEX discovery refused the same bytes as dead wiring. Cargo builds it as the version the tag names, which is not the version the detached lock pins. Cargo entries record no file inventory, so this per-file path is the live one for every vendored crate, not a legacy fallback. It now applies the same uuid pin, with the four-arm test the inventory path already had. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two-version old-toolchain test only printed whether a build without --offline worked, and reached the documented 1.41 remedy (a populated crates.io index in $CARGO_HOME) only when the plain --offline build happened to fail. Both arms run with no network, so the unasserted run could only fail, for a reason nothing checked — and a cargo that stopped needing the index would have skipped the remedy and still passed. Both directions are now asserted: the run without --offline must fail on the unreachable crates.io index, and below 1.56 the empty-CARGO_HOME --offline run must fail with the missing registry index before the seeded index is required to build and run both patched copies. The docs said 1.56 needs --offline; what 1.56 needs is --offline or a reachable index, exactly like 1.41, which additionally needs the index populated. Both the changelog and the ecosystem notes now say that. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The patches API serves canonical PURLs, so a semver build-metadata version arrives percent-encoded — pkg:cargo/wasi@0.11.0%2Bwasi-snapshot- preview1. The parsers compared that raw spelling against Cargo.lock's 0.11.0+wasi-snapshot-preview1, never matched, and refused the package with vendor_fetched_missing and then locked_version_mismatch. It failed closed, but no cargo crate with build metadata could be vendored at all, and wasi is in most Rust dependency graphs. Every ecosystem's parse now percent-decodes the namespace, name and version exactly once, after the / and @ split and before the callers' path-safety guards, so an escaped separator still cannot introduce a path segment and an already-decoded %2B cannot become a second +. The crawler and the ledger revert paths that decoded by hand, or fed an already-canonical PURL back in, drop their extra pass. Hosted mode already went through a decoding parse and is unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
toml_edit renders LF only and drops the \r of every CRLF line, including the ones no edit touched, so vendoring rewrote a lock committed with Windows line endings as all-LF. vendor --revert then "restored" that file, so the rollback differed from the original in every line ending and was not byte-identical. The copy's Cargo.toml already mapped its rendering back onto the original endings; the lock did not. The lock edits — detach, retag and restore — now go through the same reconciliation, in lock formats v1 through v4. A CRLF lock stays CRLF, a missing trailing newline stays missing, and every line a mixed-ending lock's edit leaves alone keeps its own ending. A line the edit rewrites, removes or re-inserts still takes the file's dominant ending, because its original one is not recoverable from the text, so a mixed lock is promised an unchanged remainder rather than a byte-identical revert. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cargo before 1.56 resolves every source-less Cargo.lock entry for a crate through ONE [patch.crates-io] path — the entry whose key sorts last — so with two vendored versions one lock entry is pinned to the other version's copy and cargo build --locked fails closed with "patch for <crate> did not resolve to any crates". The docs claimed older cargo needed only a populated crates.io index; the index does not help. The old-toolchain e2e passed because its fixture uuids happened to sort so that the lower version's key came last, the one order 1.41 can take. Verified on the rust:1.41-slim and rust:1.56-slim images with two crates (cfg-if 1.0.5/0.1.10, lazy_static 1.5.0/0.2.11) and with three versions (bitflags 1.2.1/0.9.1/0.8.2): only the key sort order decides, the lock entry order does not, and 1.56 builds either way. The e2e fixture now uses uuids whose keys sort adversarially, asserts that order so a future uuid cannot quietly make the test toothless, and pins the refusal below 1.56 while requiring 1.56 to build both copies. Vendoring a second version of a crate warns with cargo_multi_version_old_cargo unless the project's rust-version or rust-toolchain[.toml] promises cargo 1.56 or newer; socket-patch never runs cargo, so those files are the only signal it has. A single vendored version still builds on 1.41. A key-naming scheme that puts the lowest version's key last does make 1.41 resolve every entry correctly, but it would replace the documented <name>-socket-<uuid8> key with a version-rank encoding in every project's committed manifest, and rest correctness on an undocumented cargo iteration order. Left out deliberately; the failure is loud, not silent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Wenxin Jiang (Wenxin-Jiang)
approved these changes
Sep 25, 2026
The build-metadata e2e vendored `wasi` and then ran a consumer binary calling the patched marker fns. `wasi` declares 46 `extern "C"` WASI syscalls that only resolve on wasm targets; GNU and Apple linkers dead-strip the never-called wrappers, but MSVC refuses the executable (`LNK1120: 46 unresolved externals`), so the test failed on the windows-latest leg. The fresh-checkout `cargo build` of the same binary consumer would have failed identically. Keep `wasi` as the fixture (it is the real-world build-metadata crate the bug hit) and make the consumer a library whose oracle is compile-time: the patch suffix exposes `SOCKET_PATCHED` and `SOCKET_PKG_VERSION` constants and the consumer's `src/lib.rs` holds a `const fn bytes_eq` plus two `const _: () = assert!(...)` items, so `cargo build --locked --offline` of the library proves both that the patched bytes are what cargo compiled and that `CARGO_PKG_VERSION` carries the exact `<version>.socket.<uuid>` tag, without linking anything on any OS. The byte-identical revert leg and the zero-downloads assertion are unchanged. Negative controls, run by hand against the vendored tree: removing the suffix from the copy fails with E0425 (`SOCKET_PATCHED` not found in `wasi`); stripping the tag from the copy's manifest and lock entry fails with E0080 (the const assertion). Verified under cargo 1.93.1 and 1.83.0 and all four lock formats. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CodeQL's default setup raised 14 new `rust/cleartext-logging` alerts on this branch, all in `assert_attested`'s panic messages. The SARIF code flows start at the `uuid` reads inside `cargo_tag::tag_version`'s `format!` (CodeQL's name heuristics classify any `uid`/`uuid`-named identifier as sensitive), pass through the test's `tagged_version(U)` wrapper into the `copy`/`locked` strings of `cargo_vendored_a_attests_in_every_lock_version`, and end in the `what` label interpolated into every assertion message of the helper. A Socket patch uuid is a public identifier, so the alerts are false positives, but they gate the PR and no CodeQL config file exists to exclude tests. Break the path where the value entered the messages: the loop now labels its two cases `tagged` / `untagged` instead of embedding the tagged version strings, and `assert_attested` documents that `what` is a shape label so the next test does not reintroduce the flow. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(redirect): pin and revert each cargo version separately A project that locks two versions of one crate (cfg-if 1.0.4 beside a renamed cfg-if-legacy at 0.1.10) had every same-named declaration pinned to the last patch's registry: the manifest planner matched the crate name only, so `^0.1.10` pointed at a registry that serves 1.0.4 and `cargo fetch --locked` failed. `get <uuid>` for one version broke it the same way. The planner now pins a declaration only when its version requirement selects the patched version. A requirement that also matches another locked version refuses the dep (nothing written); `workspace = true` inheritors of an entry naming another version are skipped. `remove` had the matching defect: manifest edits are keyed by crate name, so removing one version also reverted the other version's pin (leaving its lock entry hosted) and dropped its registry edit from the ledger, so the created `.cargo/config.toml` survived the second removal. Manifest edits whose pin names a sibling version's registry lineage are no longer claimed. Golden: cargo/cargo/multi-version (the depscan TS twin lags; list it in TS_LAGGING on the next submodule bump). Real-cargo regression: e2e_redirect_cargo_shapes multi-version (fresh `cargo fetch --locked` + offline build links both patched copies; removing both purls restores every byte). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(redirect): restore a cargo config's bytes on hosted remove `remove` of a hosted cargo patch deleted the `[registries.socket-patch-…]` block it had appended to an existing config but kept the blank separator line it inserted before it, so a legacy `.cargo/config` holding `[net]\nretry = 2\n` came back as `[net]\nretry = 2\n\n`. It also collapsed every run of three newlines anywhere in the file, rewriting the user's own spacing. The block now leaves through the replay path's fragment removal, which takes the block's own separator and nothing else. Real-cargo regression: e2e_redirect_cargo_shapes legacy-config. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(redirect): pin cargo workspace members' own declarations Hosted mode pinned only the root Cargo.toml. In a workspace whose member declares the patched crate itself (`cfg-if = "1.0.4"` beside siblings that inherit `[workspace.dependencies]`), the member stayed on crates.io while Cargo.lock was repointed at the patch registry, so `cargo fetch --locked` failed — and the dep was still reported redirected and attested. `scan`/`get --mode hosted` now read every workspace member manifest (`[workspace] members` globs minus `exclude`, plus in-root path dependencies) and the rewriter plans them all in the dep's single transaction: each direct declaration is pinned, `workspace = true` inheritors resolve against the root's entry, and a member that cannot be pinned refuses the dep everywhere. `remove` already reverts edits by path; its registry-block reference probe now covers member manifests too. Golden: cargo/cargo/workspace-member (the depscan TS twin is handed no member manifests and lags; list it in TS_LAGGING on the next submodule bump). Real-cargo regression: e2e_redirect_cargo_shapes workspace-direct-member. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(redirect): attest every cargo shape post-install The real-cargo shape suite now also runs `socket-patch vex` over each fresh checkout (ledger kept, patch server admitted) and requires exactly the shape's patches to be attested — multi-version and workspace-member redirects included. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(redirect): redirect CRLF cargo projects in hosted mode Every hosted cargo planner matched LF text, so a Windows checkout with CRLF Cargo.toml / Cargo.lock was refused outright (redirect_cargo_toml_dep_unrewritable / redirect_cargo_lock_pkg_not_found). A manifest, lock or config whose every line break is CRLF is now planned as LF and written back as CRLF, and its recorded edit fragments are stored CRLF so `remove` and rollback find them in the file. Files with mixed endings are left alone and keep refusing where the grammar does not match. The shared fragment remover (used for the appended registry block) inverts CRLF files as LF so it no longer strands a `\r` line. Golden: cargo/cargo/crlf (CRLF manifest, lock and legacy config; the depscan TS twin lags — list it in TS_LAGGING on the next submodule bump). Real-cargo regression: e2e_redirect_cargo_shapes crlf (endings kept, fresh `cargo fetch --locked` + offline build, byte-identical remove). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(redirect): say why a transitive-only crate is not redirected A hosted cargo redirect pins a crate with `registry = "…"` on its manifest declaration, which reaches only that declaration: a crate the project gets purely through another dependency cannot be redirected in hosted mode. That stays a refusal — nothing is written, recorded or attested, and a requested VEX fails the run — but the warning said only "no [dependencies] entry for X in Cargo.toml", which reads like a discovery bug. When Cargo.lock resolves the crate at the patched version the warning now says it is a transitive-only dependency, that it was NOT redirected and stays unpatched, and that `scan --mode vendored` (whose `[patch.crates-io]` covers the whole graph) or a direct declaration can patch it. The code (`redirect_cargo_toml_dep_not_found`) is unchanged. Golden: cargo/cargo/transitive-refusal (pins the refusal and its warning code). In-process: the refusal writes nothing, is not attested, and the envelope carries the transitive-only warning. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(redirect): run the cargo shape suite in every lock format The multi-version, workspace-member, legacy-config and CRLF shapes ran only against the toolchain's own v4 lock, so nothing in-repo checked them against a committed v1 lock (full-id dependency edges, checksums in `[metadata]`) or v2/v3. The shape suite now honours SOCKET_PATCH_CARGO_E2E_LOCK_VERSION like the other real-cargo suites and joins the cargo-vex-matrix CI job. The shared lock re-encoder kept only dependency names, which cannot tell two locked versions of one crate apart; it now keeps each edge's package id and writes the shortest form cargo writes for that format (`name`, `name version`, or the full id; always the full id in v1). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(redirect): refuse a cargo crate other crates depend on A hosted cargo redirect pins a crate with `registry = "…"` on each manifest declaration and repoints its one Cargo.lock entry. When a crates.io (or git) crate in the lock also depends on it — cfg-if, libc and serde usually are both direct and transitive — that edge keeps resolving from crates.io. The repointed lock then fails `cargo fetch --locked`, a plain build adds the crates.io copy back beside the patched one, and the dependent crate compiles the unpatched code, while scan reported the crate redirected and VEX attested it. The same happened for a path package whose manifest the rewriter never saw (outside the project root, or behind a symlink). Before committing a dep, every Cargo.lock package that depends on it (any edge spelling: `name`, `name version`, or the full v1 id) must be a source-less package whose manifest was planned and pinned. Otherwise the dep is skipped with the new additive warning `redirect_cargo_transitive_dependents`, which names the dependents, says the crate was NOT redirected and stays unpatched, and points to `scan --mode vendored`. Nothing is written, recorded or attested. Golden: cargo/cargo/shared-dependency (the depscan TS twin lags — list it in TS_LAGGING on the next submodule bump). Unit: registry, git, v1 full-id and unplanned-path dependents refuse; another version's edge and a planned member do not. Real cargo: e2e_redirect_cargo_shapes direct-and-transitive (cfg-if beside crc32fast) is refused and the untouched project still fetches `--locked`; before this change the same shape reported redirected: 1. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(redirect): never follow a symlink to a cargo member Workspace member discovery checked the project root only lexically. A literal `members = ["linked"]` entry or a path dependency through a symlinked directory was returned as a rewrite target, and `scan --mode hosted` then wrote the registry pin into a Cargo.toml outside the project (the whole-run symlink guard checks only the file itself, not its parent directories). The same link matched by a `crates/*` glob was skipped instead, because the glob does not follow links. Discovery now returns only manifests reached without crossing a symbolic link: a symlinked member directory (literal or glob), a symlinked path dependency, a symlinked intermediate directory and a symlinked Cargo.toml are all left out. Cargo still reads those manifests, so a crate one of them depends on is refused by the Cargo.lock dependents check (redirect_cargo_transitive_dependents), exactly like a member or path dependency outside the root. Unit: symlinked literal/glob/nested members, a symlinked path dependency and manifest, and out-of-root members/path dependencies are not returned. In-process: an out-of-root path dependency and a glob or literal symlinked member each refuse the crate loudly and leave every file, the one outside the project included, untouched (the literal case reported redirected: 1 and wrote outside the project before). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(redirect): remove v1-lock cargo patches in any order A v1 Cargo.lock names every dependency by its full id, so repointing a crate also rewrites its dependents' references. Each dependent's whole `[[package]]` block was recorded as one edit. When one block referenced two patched packages (the root, for two cfg-if versions or any two patched crates it declares) the second edit's `original` was the first edit's `new`, and removing the first-applied purl alone found neither fragment: `remove`, scoped rollback and the hosted-to-vendored takeover refused as "drifted" unless purls went in exact reverse apply order. The dependents' references are now one `redirect_cargo_lock_reference` edit holding just the quoted full id `"name version (source)"`. It names that package exactly, so its inverse puts back every occurrence, independently of any other package's edits; the per-purl revert and the whole-ledger replay both handle it. The v1 whole-block edits were never released, so no existing ledger carries them. Unit: two cfg-if versions and two crates sharing a dependent block each remove in both orders byte-for-byte (both refused as drifted before); a full id named by two members replays clean. Real cargo: e2e_redirect_cargo_shapes now removes every multi-patch shape in apply order and in reverse, from the same post-scan state, and passes with SOCKET_PATCH_CARGO_E2E_LOCK_VERSION=1, 2 and 4. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(redirect): restore a cargo config's exact trailing bytes The earlier bug-H fix removed the appended `[registries.…]` block with the shared fragment remover, which collapses the whole trailing newline run at EOF to one newline. That fixed a config ending in exactly one newline but lost a config's trailing blank line (restored byte-for-byte before that change) and still added a newline to a config without one. The block removal now inverts exactly what the rewriter appended: the recorded fragment plus the single blank separator before it. The newline a config without a final one needs rides in the recorded fragment, so that case is exact too; content the user appended after the block is kept; a config the rewrite created still ends empty and is deleted. `remove` and the whole-ledger replay share the helper (replay no longer leaves an emptied created config behind). All-CRLF configs are inverted as LF, and a fragment recorded with the other line endings still matches. Existing ledgers keep reverting as before. Unit: the bug-H revert test covers no final newline (LF and CRLF), trailing blank lines (LF and CRLF) and a whitespace-only config through both `remove` and the replay; helper pins for every append shape. Real cargo: e2e_redirect_cargo_shapes config-unterminated and config-trailing-blank restore the config byte-for-byte. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(redirect): remove cargo patches across line-ending changes The CRLF support records a CRLF project's edit fragments with CRLF, and the ledger is JSON, so git line-ending normalization never touches them while it does rewrite the committed Cargo.toml / Cargo.lock / .cargo/config. A hosted redirect scanned on Windows (core.autocrlf) therefore could not be removed on a Linux checkout, nor an LF scan on a CRLF checkout: `remove`, rollback and the vendored takeover matched fragments byte-for-byte and refused as "drifted", and the suggested re-scan was a no-op that kept the same fragments. The cargo per-purl revert and the whole-ledger replay now match fragments regardless of CRLF/LF: an all-CRLF file is matched as LF and written back CRLF, any other file is tried with the recorded fragments and then with their LF forms. The file keeps its current line endings. The registry-block checks and removals use the same matching. Drift of the text itself still refuses exactly as before. Unit: a ledger recorded CRLF reverts LF files and a ledger recorded LF reverts CRLF files, each through `remove` and through the replay, with the ledger round-tripped through JSON (all four refused before). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(redirect): remove each cargo version from older ledgers Manifest edits are keyed by crate name, so the per-version revert told two redirected versions' edits apart by the registry each pin names. Ledgers an older CLI wrote with its name-only matcher break that rule: each version's run pinned BOTH declarations to its own registry, and a project it mis-pinned is later repaired by superseding that pin. On such a ledger `remove pkg:cargo/cfg-if@1.0.4` claimed half of a declaration's re-pin chain (or the superseded mis-pin) and refused as "drifted" — for exactly the users bug B had already hurt — and the other removal order dropped a still-pinned registry edit, leaving the created .cargo/config.toml behind. With another version of the crate still redirected, each manifest edit is now attributed to the version its declaration's requirement selects (the planner's own rule); a line without a readable requirement falls back to the registry it pins. A sibling version's registry is never claimed, and a still-referenced block keeps its ledger edit when another recorded wiring edit still pins to it, so the removal that retires that pin removes the block. A block kept for a hand pin still leaves the ledger, and the whole-ledger replay now keeps such a block too instead of deleting it from under the hand pin. Unit: a name-only two-version ledger and a repaired mis-pin ledger each remove in both orders back to the pristine project with no config and an empty ledger (removing 1.0.4 first refused before); the replay keeps a hand-pinned block. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(redirect): keep identical hosted edits from one scan The hosted flow appends a run's edits to the redirect ledger, skipping any edit already in it. The check also ran against the edits appended earlier in the same run, so identical edits from one rewrite collapsed into one. The cargo rewriter records one edit per occurrence, and a Cargo.toml that declares the crate with the same line in two sections (`[dependencies]` and `[dev-dependencies]`) yields two identical edits. With only one in the ledger, `remove` reverted one pin, kept the other and the registry block it references, and still reported success. Edits are now deduplicated only against the ledger as the run found it, so a re-run still records nothing twice. Real cargo: e2e_redirect_cargo_shapes two-sections (scan, fresh `--locked` build, VEX, byte-identical `remove`); it failed on the leftover pin before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * docs: describe the hosted cargo redirect fixes CLI_CONTRACT.md still said the hosted rewriter reads only root candidate files, but cargo now also reads and pins workspace-member and in-root path-dependency manifests, which can show up in `rewrittenFiles`. The contract now covers that (members globs minus `exclude`, no symlinks, never `.socket/`), the new `redirect_cargo_transitive_dependents` refusal, the transitive-only and multi-version refusals, and CRLF handling across a checkout conversion. CHANGELOG [Unreleased] gains Fixed entries for this branch's user-visible hosted cargo changes, and docs/ecosystems.md notes that hosted cargo reaches direct dependencies only. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(redirect): refuse cargo requirements the patch misses A crate declared only with version requirements the patched version does not satisfy resolves those declarations to another version, so a pin cannot reach the locked crate. Hosted cargo now refuses it with redirect_cargo_toml_dep_unrewritable, the code the Socket backend uses for the same shape, instead of reporting it not found. Shared golden fixtures pin both refusals the backend and CLI must agree on: requirement-excludes-patched-version, path-dependency and transitive-dependents-git (a git crate also depending on the patched crate). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(vendor): see a hosted pin in a workspace member The hosted rewriter pins the patched crate in every manifest that declares it, workspace members included, but the guard that refuses to vendor on top of a live hosted redirect read the root Cargo.toml alone. A member-only pin was invisible, so with the redirect ledger gone vendor wired [patch.crates-io] beside a member still resolving the crate from the hosted registry — where [patch.crates-io] does not reach — and reported success. The guard now reads the root manifest and every member manifest the rewriter itself discovers, and the refusal names the file it found. Real cargo: mode_migration_cargo's takeover refusal gains the shape the guard exists for — the lock restored from version control while a table-form manifest pin survives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(redirect): repoint a v1 lock's [root] table A v1 Cargo.lock written before cargo dropped [root] keeps the root package in that table, whose dependencies spell full package ids. The reference rewrite walked the [[package]] array only, so a redirected crate left [root] naming the crates.io id of a package the lock no longer held: cargo build --locked then fails, an unlocked build discards the lock and re-resolves, and the scan reported the crate redirected either way. The walk now covers that table too, recorded by the same every-occurrence reference edit, so remove still restores the lock byte for byte in any order. The vendored backend has read [root] since it learned v1 locks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(redirect): spend one cargo edit per occurrence One scan records one cargo edit per occurrence, so a manifest that declares the crate with the same line in two sections leaves two identical edits in the ledger, and the per-purl revert unwinds one occurrence per edit. The whole-ledger replay — the path a run whose patch-record fetch failed leaves behind, records empty — instead refused as soon as the fragment appeared more than once, and that refusal dropped every other cargo edit with it: the project stayed fully redirected, with a suggested remedy (re-run the scan) that cannot change a duplication the manifest itself carries. The replay now allows as many occurrences as there are identical edits left to spend on them, and still refuses a surplus no edit accounts for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(redirect): never pin a manifest under target/ Member discovery skipped cargo's build directory only while expanding a glob: a literal `members = ["target/gen"]` entry, or a path dependency pointing into target/, was returned and pinned — which the key predicate's own doc comment said could not happen. The next `cargo clean` deletes that manifest, and a recorded hosted edit whose file no longer exists refuses the rollback of every other cargo edit. A `target` segment is now excluded wherever a member path is accepted, in discovery and in the key predicate both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(redirect): refuse a lockless cargo pin A `registry = …` pin reaches only the declarations it sits on, so a crate another package also pulls in is refused — but that check reads Cargo.lock, and a project without one was pinned unconditionally. Rust libraries routinely gitignore their lock, so the silent shape was the common one: the scan reported the crate redirected, vex attested it, and the next build resolved the patched copy for the root and an unpatched crates.io copy for whatever else depends on the crate. Without a lock the dependents question is now answered from the manifests instead: a crate declared beside any other dependency — or beside a workspace member this run could not read (a members glob, or a member outside the project or behind a symbolic link) — is refused `redirect_cargo_lockless_dependents`, whose detail names both remedies. A path dependency on a manifest the same run pins is not company, and a project whose only dependency is the patched crate has nothing that could pull it in and still redirects. Real cargo: e2e_redirect_cargo_shapes lockless-other-dependencies, the direct-and-transitive project with its lock deleted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko)
enabled auto-merge (squash)
September 25, 2026 16:22
Mikola Lysenko (mikolalysenko)
deleted the
feat/cargo-vendor-manifest-patch
branch
September 25, 2026 16:22
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Branch
feat/cargo-vendor-manifest-patch: 10 signed commits onmain@1bfe5326; 43 files, +10558/−851.Summary
Vendored cargo patches move their wiring out of
.cargo/config*and into the project itself, so the patch is recoverable from the files a scanner already reads:Cargo.toml:[patch.crates-io] <name>-socket-<uuid8> = { package = "<name>", path = ".socket/vendor/cargo/<uuid>/<name>-<version>" }. Pre-v5.cargo/config*wiring migrates on the nextvendor/scan/get/repair, and every revert path removes both spellings.1.0.4becomes1.0.4+socket.<uuid>, and a version that already carries build metadata keeps it (2.0.1+zstd.1.5.2→2.0.1+zstd.1.5.2.socket.<uuid>). Cargo ignores build metadata when matching requirements, so=1.0.4,1and a transitive^1all still resolve to the copy — andCargo.locknow records which patch was actually built.Two consequences worth stating plainly:
CARGO_PKG_VERSIONinside the patched crate carries the tag. Documented in the CHANGELOG anddocs/ecosystems.md.[patch]entry replace a manifest entry with the same key, whatever its version, so a bare key let a user'scfg-ifconfig entry silently win over the patched copy. With the Socket key a same-version conflict is a loud cargo error instead.Why
A vendored
Cargo.lockentry only loses itssourceandchecksum, so the lock could not say which patch produced it, and.cargo/config*can hold proxy credentials and registry tokens that a scanner should not have to ingest. Tagging closes both: the identity is in the lock, and the file that carries it is one every Rust project already commits.Three bugs this also fixes
Found by a 1,232-cell real-cargo matrix (cargo 1.41 → stable, lock formats v1–v4, both modes), all pre-existing on
main:397259cf).parse_name_versionreturned the purl's name and version raw, but the API percent-encodes them, so vendored cargo compared0.11.0%2Bwasi-snapshot-preview1against the lock's0.11.0+wasi-snapshot-preview1and refused.wasiis in most Rust dependency trees. The parsers now decode each component exactly once, after the split; two double-decode paths (python_crawler,takeover::find_record_key) are removed. This touches all eight ecosystems, and subsumes the golang-only decodemainshipped in Stop hosted Go redirects claiming unpatched deps #252.Cargo.lockwas rewritten as LF (4247daa8), sovendor --revertdid not restore the original bytes.toml_editrenders LF only; the lock edits now map the rendering back onto the original text, the wayCargo.tomlalready did.a18ffd14). Cargo before 1.45 resolves every source-less lock entry for a crate through whichever[patch]key sorts last, so two vendored versions of one crate fail--lockedwithdid not resolve to any crates. Measured across 1.41.1–stable in docker: 1.41–1.44 refuse the adversarial key order, 1.45+ resolve either order, each entry to its own copy — no silent mis-resolution anywhere. The e2e previously passed only because its test UUIDs happened to sort favourably; it now asserts the key order itself and exercises both sides of the boundary. A second vendored version warnscargo_multi_version_old_cargounless the project pins cargo ≥ 1.45 viarust-versionor a toolchain file.A
[patch]key scheme that makes the lowest version's key sort last would let two versions build on 1.41–1.44 as well. It is not adopted here: it would replace the documented key with a version-rank encoding in every committed manifest and lean on an undocumented cargo iteration order, to rescue a case that currently fails loudly rather than silently. The rule and the reproduction are ina18ffd14's commit message if we want it later.Refusals (all fail before any write)
cargo_manifest_unreadable,cargo_manifest_unparseable,cargo_manifest_symlink_unsupported,cargo_manifest_not_workspace_root,cargo_manifest_patch_source_alias(a[patch."https://github.com/rust-lang/crates.io-index"]table, which cargo treats ascrates-ioand which would silently drop our table),cargo_lock_untaggable,cargo_copy_untaggable, anduser_authored_patch_entry. Dry runs preview each of them.Testing
cargo test -p socket-patch-core4883 pass,-p socket-patch-cli3992 pass, clippy--workspace --all-features --all-targets -D warningsclean.SOCKET_PATCH_CARGO_E2E_LOCK_VERSION1–4, plus dockerrust:1.41-slim/rust:1.56-slimold-toolchain legs (14/14): the lock and copy carry the tag,cargo metadatareports it,--locked --offlinebuilds the patched bytes, and revert is byte-identical.Known gaps, stated in the tests rather than papered over: a mixed-ending lock cannot revert byte-identically (a removed-then-reinserted
sourceline has no recoverable original ending), andtoml_editstill drops a UTF-8 BOM from a lock.Note
High Risk
Major semver behavior change to committed
Cargo.toml/Cargo.lockwiring plus cross-ecosystem PURL decoding; incorrect tagging or migration could break--lockedbuilds or mis-attribute patches in VEX.Overview
v5 breaking change for vendored Rust: patch wiring moves from
.cargo/config*into the workspace-rootCargo.tomlunder Socket-owned keys (<crate>-socket-<uuid8>), and vendored copies plus detachedCargo.lockentries are rewritten to tagged versions (<ver>+socket.<uuid>) so scanners and builds can recover which patch was applied. Pre-v5 config wiring migrates onvendor/scan/get/repair; hosted takeover and VEX discovery are updated to read manifest wiring first and treat tag/ledger mismatches as dead wiring.Also fixes three pre-existing gaps: percent-decoded PURL coordinates across ecosystems (so build-metadata crates like
wasican vendored), CRLF-preservingCargo.lockedits on revert, and accurate cargo ≥ 1.45 documentation/warning for two vendored versions of one crate. CI adds acargo-old-toolchainsjob (dockerrust:1.41-slim/1.56-slim) and the cargo e2e suite grows to cover migration, multi-version, old toolchains, transitive deps, and refusal cases.Reviewed by Cursor Bugbot for commit a18ffd1. Configure here.