Repository navigation
Hosted cargo scan redirects a crate the user overrides with [patch.crates-io], silently dropping the override and breaking --locked #480
Description
[agent] Found by the scheduled Cargo bug-hunt routine (ledger #315).
Summary
When the root Cargo.toml overrides a crate with [patch.crates-io] cfg-if = { path = "cfg-if-local" }, Cargo.lock records that crate with no source (it resolves to the local path). scan --mode hosted still treats it as the crates.io package pkg:cargo/cfg-if@1.0.4. It pins the [dependencies] declaration to the per-patch registry, inserts a source = "sparse+…/patch-registry/…" line into the sourceless lock block, and reports redirected: 1 with no warnings.
What happens next:
- The user's
[patch.crates-io]override stops applying. Cargo warnspatch `cfg-if v1.0.4 (…/cfg-if-local)` was not used in the crate graph, so the user's local fork is silently replaced by the Socket-patched crates.io bytes. - The rewritten lock lacks the
[[patch.unused]]entry cargo now needs, so every freshcargo fetch --locked/cargo build --lockedfails withcannot update the lock file … because --locked was passed.
The same crate declared directly as a path dependency (cfg-if = { path = "cfg-if-local", version = "1.0.4" }) is correctly refused with redirect_cargo_toml_dep_unrewritable ("declared as a path/git dependency"). A [patch.crates-io] override is the same situation, but the rewriter doesn't check for it.
Impact
- CI using
--locked/--frozenbreaks right after a scan that reported success. - Without
--locked, cargo re-resolves and quietly stops using the user's own fork, which may carry the user's own security fix. scan never mentions the override.
Repro
This uses the wiremock harness in crates/socket-patch-cli/tests/e2e_redirect_cargo_shapes.rs. Add this shape. run_shape also needs the registry copy of cfg-if-1.0.4 extracted to build the served crate, so I fetched it from a sibling helper project that depends on cfg-if = "=1.0.4" in the same CARGO_HOME.
fn local_crate(name: &str, version: &str) -> String {
format!("[package]\nname = \"{name}\"\nversion = \"{version}\"\nedition = \"2018\"\n")
}
#[tokio::test(flavor = "multi_thread")]
async fn hosted_user_patch_override_same_crate() {
let shape = Shape {
tag: "user-patch-same-crate",
files: vec[
("Cargo.toml", format!("{}\n[patch.crates-io]\ncfg-if = {{ path = \"cfg-if-local\" }}\n",
consumer_manifest("cfg-if = \"1.0.4\"\n"))),
("src/main.rs", "fn main() {}\n".to_string()),
("cfg-if-local/Cargo.toml", local_crate("cfg-if", "1.0.4")),
("cfg-if-local/src/lib.rs", "pub fn user_fork() {}\n".to_string()),
],
patches: vec[CFG_IF_1],
oracle: Vec::new(),
crlf: false, lockless: false,
refused: Some("redirect_cargo_toml_dep_unrewritable"), // or a new dedicated code
};
let _ = run_shape(shape).await;
}The lock before the scan (cfg-if has no source):
[[package]]
name = "cfg-if"
version = "1.0.4"After scan --mode hosted --json (redirect: {"redirected":1,"rewrittenFiles":[".cargo/config.toml","Cargo.lock","Cargo.toml"],"warnings":[]}):
# Cargo.toml
[dependencies]
cfg-if = { version = "1.0.4", registry = "socket-patch-c1f90104-…" }
[patch.crates-io]
cfg-if = { path = "cfg-if-local" }
# Cargo.lock
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "sparse+http://127.0.0.1:40563/patch-registry/cargo/…/index/"
checksum = "b9e4eadc…"A fresh checkout then fails:
$ cargo fetch --locked
Updating `socket-patch-c1f90104-5a0c-4e7a-9c0d-1a2b3c4d5e01` index
warning: patch `cfg-if v1.0.4 (/tmp/…/fresh/cfg-if-local)` was not used in the crate graph
= help: perhaps you meant one of the following:
socket-patch-c1f90104-5a0c-4e7a-9c0d-1a2b3c4d5e01
error: cannot update the lock file /tmp/…/fresh/Cargo.lock because --locked was passed to prevent this
Expected vs actual
- Expected: the dependency is skipped with a loud warning and nothing is rewritten. This is how a path/git declaration is already handled (
redirect_cargo_toml_dep_unrewritable), and docs/ecosystems.md says a hosted redirect leaves the project buildable with--locked. A crate that doesn't resolve to crates.io (a sourceless lock entry) isn't thepkg:cargocrates.io package the patch targets. - Actual:
redirected: 1, no warnings, the user's override is dropped, and--lockedfails.
Matrix
| OS | cargo | [patch.crates-io] overrides the patched crate |
[patch.crates-io] overrides an unrelated crate (used and unused) |
path dep with version |
|---|---|---|---|---|
| Linux | 1.93.1 (repo MSRV toolchain) | fail (2/2) | pass | pass (refused) |
| Linux | 1.97.0 (stable) | fail (1/1) | not run | not run |
| macOS / Windows | any | untested (the rewriter is OS-independent) | untested | untested |
Tested on main 6e7ef74. No cargo code changed since 2463257 (v5), so this probably dates back to at least the v5 consolidation. I didn't bisect it.
Suspect code
crates/socket-patch-core/src/formats/cargo/hosted.rs:162: theNone =>arm of the lock-block rewrite inserts asource = <patch index>into a block that has no source (a path /[patch]resolution) instead of declining it.crates/socket-patch-core/src/patch/redirect/mod.rs:2558: the manifest check refusespath/giton the dependency itself, but never consults the root manifest's[patch.crates-io](or[patch."https://github.com/rust-lang/crates.io-index"]) for the same crate name.
Activity
mikolalysenko commentedon Oct 1, 2026
Correction to the "Expected" citation above: docs/ecosystems.md doesn't literally promise
--lockedbuilds for hosted cargo. The Cargo row describes hosted mode as a per-patch sparse registry for crates.io direct dependencies only, and says shapes it can't redirect are refused. A crate whose lock entry has nosourcebecause a[patch.crates-io]path override resolved it isn't a crates.io dependency. The existing path/git-declaration refusal (redirect_cargo_toml_dep_unrewritable) is the closest documented behaviour. The defect stands either way: the scan reportsredirected: 1with no warnings, and the result failscargo fetch --lockedand drops the user's override.Generated by Claude Code
cargo: run 5 ledger (#480)
mikolalysenko commentedon Oct 1, 2026
[agent] Triaged as
priority:p2(Cargo). Confirmed on main6e7ef74: theNone =>arm incrates/socket-patch-core/src/formats/cargo/hosted.rs(~L162) inserts asource =line into a sourceless lock block, and the manifest check inpatch/redirect/mod.rsonly refusespath/giton the dependency itself, never a root[patch.crates-io]override of the same crate. No duplicate or existing fix PR found; not clustered with the other open Cargo issues (#386, #417, #455 hit different code paths).Generated by Claude Code
mikolalysenko commentedon Oct 1, 2026
[agent] Cargo bug-hunt run 6, main
61cfb9b. The URL spelling of the override fails in the same way.With
[patch."https://github.com/rust-lang/crates.io-index"] cfg-if = { path = "local/cfg-if" }in place of[patch.crates-io], the lock again recordscfg-if 1.0.4with no source.scan --mode hosted --jsonstill reportsredirected: 1,warnings: [], pinscfg-if = { version = "1.0.4", registry = "socket-patch-<uuid>" }, and insertssource = "sparse+…/patch-registry/…"into the sourceless block. A fresh checkout then fails:Reproduced 2/2 on cargo 1.93.1 and 1/1 on 1.97.0, using the
e2e_redirect_cargo_shapes.rsharness with a post-baseline override shape. A fix that only looks at[patch.crates-io]would miss this spelling.For comparison, a legacy
[replace] "cfg-if:1.0.4" = { path = … }is correctly refused:redirect_cargo_lock_pkg_ambiguous, with nothing rewritten.The agent-mode twin of this override (the unused registry copy gets patched and VEX attests it) is #506.
Generated by Claude Code
mikolalysenko commentedon Oct 2, 2026
[agent] Cargo bug-hunt run 7, main
61cfb9b. Two more override shapes fail the same way, and one of them goes through a different code arm.Both shapes use the
tests/e2e_redirect_cargo_shapes.rsharness, with a local hook (not committed) that adds the override after the baseline build and relocks.scan --mode hosted --jsonreportsredirected: 1withwarnings: []. A fresh checkout then failscargo fetch --locked:.cargo/config.toml:[patch.crates-io] cfg-if = { path = "local/cfg-if" }, with nothing inCargo.toml. The lock block has no source, so this is the sameNone =>arm as the original report. It also means a fix that only checks the root manifest's[patch]tables isn't enough: cargo merges[patch]from the whole config chain.[patch.crates-io] cfg-if = { git = "file:///…/gitfork" }. Here the lock block has a source (source = "git+file:///…/gitfork#<sha>"), and theSome(source) =>arm incrates/socket-patch-core/src/formats/cargo/hosted.rs:146overwrites it with the sparse patch-registry URL. A fix that only refuses sourceless lock blocks won't catch this. The check probably needs to be "the lock source is crates.io" (registry+https://github.com/rust-lang/crates.io-indexorsparse+https://index.crates.io/).Control: a git direct dependency (
cfg-if = { git = … }under[dependencies]) is refused correctly withredirect_cargo_toml_dep_unrewritable, and nothing is rewritten.[patch][patch]Generated by Claude Code
mikolalysenko commentedon Oct 9, 2026
v5 release blocker (P1). Hosted Cargo patching must not silently replace a user path/git override with the public registry package.
This follows the maintainer's release scope: one normally completing CLI instance, prioritizing valid-lockfile patch/install behavior, compatibility, and actionable CLI UX.
mikolalysenko commentedon Oct 9, 2026
[agent] Claiming for v5 blocker burn-down (shared root cause: hosted cargo rewriter ignores root [patch.*] overrides / sourceless lock blocks). Branch: agent/v5-cargo-patch-override. Claim-ID: 2026-10-09T16:41:36Z-eca7f8
Refuse hosted cargo redirect over [patch]
Reuse the lock parse for the override check
Merge main into agent/v5-cargo-patch-override