Repository navigation
Fix hosted cargo contested lock in vex and restore (#679, #863) - #1313
Conversation
After a hosted cargo scan pins cfg-if 1.0.4, a dependency added later can lock its own crates.io cfg-if 1.0.4 beside the Socket one. Cargo cannot unify the two sources, so the build compiles the unpatched copy too. - vex: the hosted ref is withheld (patched_ref_unattributable, naming the crates.io entry) instead of attested not_affected. It stays a shadowed ref, so rollback, remove and list still unwind it (#679). - remove / rollback: the upstream restore merges the Socket block into the existing crates.io block. It drops the block (and a v1 lock's [metadata] line) and respells dependents' references the way cargo writes them. Before, it left two identical blocks that cargo refused to parse while the command reported success (#863). No index lookup is needed, so this also works offline. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
BugBot review |
|
bugbot run Generated by Claude Code |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 659fcbc. Configure here.
| // crates.io's source into the Socket block would leave two | ||
| // identical blocks, which cargo refuses to parse (#863). The twin | ||
| // already carries the checksum, so no registry lookup is needed. | ||
| let twin_of = |h: &LockHit| { |
There was a problem hiding this comment.
🔒 Agentic Security Review
Severity: MEDIUM
Description: Hosted Cargo restore treats a contested crates.io twin as already bound and skips the sparse-index checksum rebind. twin_of matches only name, version, and source registry+https://github.com/rust-lang/crates.io-index, then those hits are filtered out of UpstreamClient::cargo_cksum (the lookup that refuses offline and returns the public index cksum). Restore deletes the Socket [[package]] block and its v1 [metadata] checksum line, respells dependents onto the surviving twin, and marks the uuid handled, so the Cargo.toml registry pin and unused [registries.socket-patch-<uuid>] block are removed. The twin checksum bytes are never read or compared. A later cargo fetch from the canonical index still fails closed when that checksum differs from the index cksum, before crate code runs. A [source.crates-io] replace-with registry or vendor directory does not consult that index: cargo accepts the .crate when its sha256 equals the lock checksum. This merge moves former Socket-sourced dependents, which were outside crates.io replacement, onto that checksum.
Impact: Rollback, remove, and vendored takeover all run this restore. A lockfile author can leave a non-index sha256 on the crates.io twin; restore now succeeds offline and publishes that sha256 as the only pin. Builds that use the real crates.io index error on the mismatch. Builds whose crates.io source is replaced or vendored, including a replacement committed next to the lockfile, run the bytes that hash to the preserved checksum (crate code and build scripts). Re-resolving cargo_cksum would have written the public-index checksum and made those divergent bytes fail closed. An honest twin produced by cargo against the real index is unchanged, because that checksum is already the index cksum.
Remediation: Still call cargo_cksum for a contested twin and refuse that pin when the lookup fails or the twin's inline or v1 metadata checksum is not exactly the sparse-index cksum, including under --offline. Only after that match, delete the Socket block, keep the twin, and mark the uuid handled.
LLM Description written by Claude Code:claude-opus-5-5
Fixes #679
Fixes #863
Summary
After
scan --mode hostedpins a crate such ascfg-if 1.0.4, a dependency added later (cargo add, a merge or a path member) can lock its own crates.io copy of the samename@version. Cargo never unifies packages from different sources, soCargo.lockholds twocfg-if 1.0.4blocks and the build compiles the unpatched one for that dependent.vex/discover/cargo.rsnow records each non-Socket block (crates.io, git) of a hosted crate'sname@versionas a same-lockunpatched_copy. The sharedcontest_within_locksrule then withholds the hosted ref from attestation, with apatched_ref_unattributablediagnostic naming the crates.io entry. Before, it was attestednot_affected. The ref is kept asshadowed, sorollback/remove/list/ the takeover still unwind it (npm hosted pin next to a bundled copy can't be unwound: rollback/remove refuse it, and the vendored takeover skips the restore, so vendor --revert lands back on hosted and allow-remote=all stays #828 semantics). It is notrewirable, because a re-scan can't rewire a transitive crates.io dependent (redirect_cargo_transitive_dependents).patch/redirect/upstream/cargo.rsmerges the Socket block into an existing crates.io block for the samename@version. Before, it spliced crates.io's source into the Socket block and left two identical blocks, which cargo refuses to parse. Now it drops the Socket block (and a v1 lock's[metadata]line) and respells dependents' references the way cargo writes them: the bare name when the crate is unique,"name version"when only the version is, and the full id in v1. The twin already carries the checksum, so this path needs no index lookup and works offline.Root cause
Neither the cargo VEX extractor nor the cargo upstream restore handled a lock in which the hosted pin and a crates.io copy share one
name@version.Tests (red → green)
socket-patch-corevex::discover::cargo::tests::crates_io_twin_of_the_hosted_crate_withholds_the_ref(crates.io and git twins; another version doesn't count)patch::redirect::upstream::cargo::tests::restore_merges_into_an_existing_crates_io_twin,restore_merge_keeps_versioned_refs_while_the_name_is_ambiguous,restore_merge_in_a_v1_locke2e_redirect_cargo_shapes::cargo_hosted_contested_by_a_later_crates_io_copy: hosted scan → fresh checkout addscrc32fast = "=1.5.0"→ contested lock (2 blocks);vexdoesn't attest cfg-if;removeexits 0 leaving one crates.io block;cargo build --locked --offlinesucceedsCLI_CONTRACT.md: the cargo discovery row and the cargo bullet in "Hosted unwind coverage" are updated.
Commands run
cargo clippy --workspace --all-features -- -D warnings: cleancargo test -p socket-patch-core --lib: 6113 passedcargo test -p socket-patch-cli --test e2e_redirect_cargo_shapes: 28 passed (cargo 1.93.1)cargo test -p socket-patch-cli --test mode_migration_cargo: 28 passede2e_vendor_cargo_build: the 2 old-toolchain legs (*_old_toolchains,*_below_1_45) fail locally. They exercise vendored wiring under old rustup toolchains, which this diff doesn't touch, so I'm leaving them to CI.🤖 Generated with Claude Code
Note
Medium Risk
Changes hosted Cargo lockfile restore and VEX attestation for duplicate name@version blocks; incorrect merge or reference respelling could break
cargo build --lockedor mis-attest patches.Overview
Fixes hosted Cargo when
Cargo.lockholds both a Socket sparse-index block and a separate crates.io (or git) block for the samename@version—typically after a dependency is added post-scan (#679, #863).VEX / discovery adds
unpatched_twins: any non-Socket twin of a hosted crate is recorded viaunpatched_copy, so attestation is withheld (patched_ref_unattributable, “UNPATCHED”) while the pin stays shadowed sorollback/remove/liststill unwind it. Upstream restore no longer rewrites the Socket block to crates.io (which duplicated blocks and brokecargoparsing); it drops the Socket[[package]](and v1[metadata]checksum lines), skips index lookup when the twin already has the checksum, and respells dependent references withmerged_referencesto match what Cargo would write.CLI_CONTRACT.md documents the contested-lock behavior for discovery and hosted unwind. Tests cover unit restore/merge paths, discovery withholding, and a new e2e shape
cargo_hosted_contested_by_a_later_crates_io_copy.Reviewed by Cursor Bugbot for commit 659fcbc. Configure here.
Generated by Claude Code