Skip to content

Commit fd5e12f

Browse files
mikolalysenkoclaude
andcommitted
Refuse hosted cargo redirect over [patch]
A crate the user overrides with a [patch.crates-io] (or index-URL) path, git or registry entry was still pinned to the Socket registry by scan --mode hosted: the user's fork was silently replaced with the patched crates.io bytes, a source was spliced into the sourceless lock block, and the stale [patch] entry broke every cargo --locked build while the scan reported redirected: 1 with no warnings. The rewriter now refuses such a crate with redirect_cargo_dep_overridden and rewrites nothing. With a lock, a git / other-registry source always refuses, and a sourceless block refuses when the root Cargo.toml or the project .cargo/config* holds a [patch] entry for it (a sourceless block without one is the vendored-takeover revert shape, still pinned). With no lock, the override tables answer directly. Fixes #480 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 85d021a commit fd5e12f

4 files changed

Lines changed: 393 additions & 2 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

‎crates/socket-patch-cli/tests/e2e_redirect_cargo_shapes.rs‎

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
//! * `lockless_other_dependencies` — the same project with no committed
2222
//! `Cargo.lock`: with no resolved graph to read, a crate declared beside
2323
//! any other dependency is refused just as loudly.
24+
//! * `user_patch_same_crate` / `config_patch_same_crate` — the user
25+
//! overrides the patched crate with a `[patch]` path entry (root manifest
26+
//! or `.cargo/config.toml`): hosted mode refuses it rather than dropping
27+
//! the override and breaking `--locked` (#480).
2428
//!
2529
//! Every shape runs the same chain against the real cargo: a baseline build
2630
//! with a private CARGO_HOME (network to crates.io for fixture setup only),
@@ -109,6 +113,11 @@ struct Shape {
109113
/// A shape hosted mode must REFUSE: the rewriter warning code every
110114
/// patch is skipped with. The scan must leave every file untouched.
111115
refused: Option<&'static str>,
116+
/// Extra crates.io dependencies fetched into the CARGO_HOME by a helper
117+
/// project before the scan: a patched crate the project itself resolves
118+
/// elsewhere (a `[patch]` override) still needs its registry copy for
119+
/// the served `.crate`, exactly as on a machine that built it before.
120+
prefetch: &'static str,
112121
}
113122

114123
fn run_socket(cwd: &Path, args: &[&str], cargo_home: &Path) -> (i32, String, String) {
@@ -477,6 +486,19 @@ async fn run_shape(shape: Shape) -> Option<()> {
477486
if shape.lockless {
478487
std::fs::remove_file(proj.join("Cargo.lock")).unwrap();
479488
}
489+
if !shape.prefetch.is_empty() {
490+
let helper = tmp.path().join("prefetch");
491+
std::fs::create_dir_all(helper.join("src")).unwrap();
492+
std::fs::write(helper.join("Cargo.toml"), helper_manifest(shape.prefetch)).unwrap();
493+
std::fs::write(helper.join("src/main.rs"), "fn main() {}\n").unwrap();
494+
let fetched = cargo(&helper, &["build", "-q"], &home);
495+
assert!(
496+
fetched.status.success(),
497+
"{}: the prefetch helper must build:\n{}",
498+
shape.tag,
499+
stderr(&fetched)
500+
);
501+
}
480502
let before = snapshot(&proj);
481503

482504
let mut served = Vec::new();
@@ -787,6 +809,10 @@ fn pin_patched_versions(proj: &Path, home: &Path, patches: &[Patch]) {
787809
}
788810
}
789811

812+
fn helper_manifest(deps: &str) -> String {
813+
format!("[package]\nname = \"prefetch\"\nversion = \"0.1.0\"\nedition = \"2018\"\n\n[dependencies]\n{deps}")
814+
}
815+
790816
fn consumer_manifest(deps: &str) -> String {
791817
format!("[package]\nname = \"consumer\"\nversion = \"0.1.0\"\nedition = \"2018\"\n\n[dependencies]\n{deps}")
792818
}
@@ -814,6 +840,7 @@ async fn cargo_hosted_multi_version_pins_each_declaration_and_removes_cleanly()
814840
crlf: false,
815841
lockless: false,
816842
refused: None,
843+
prefetch: "",
817844
};
818845
let _ = run_shape(shape).await;
819846
}
@@ -837,6 +864,7 @@ async fn cargo_hosted_legacy_config_is_restored_byte_for_byte() {
837864
crlf: false,
838865
lockless: false,
839866
refused: None,
867+
prefetch: "",
840868
};
841869
let _ = run_shape(shape).await;
842870
}
@@ -875,6 +903,7 @@ async fn cargo_hosted_config_trailing_bytes_are_restored() {
875903
crlf: false,
876904
lockless: false,
877905
refused: None,
906+
prefetch: "",
878907
};
879908
if run_shape(shape).await.is_none() {
880909
return;
@@ -908,6 +937,7 @@ async fn cargo_hosted_same_line_in_two_sections_removes_cleanly() {
908937
crlf: false,
909938
lockless: false,
910939
refused: None,
940+
prefetch: "",
911941
};
912942
let _ = run_shape(shape).await;
913943
}
@@ -947,6 +977,7 @@ async fn cargo_hosted_workspace_member_declaration_is_pinned() {
947977
crlf: false,
948978
lockless: false,
949979
refused: None,
980+
prefetch: "",
950981
};
951982
let _ = run_shape(shape).await;
952983
}
@@ -969,6 +1000,7 @@ async fn cargo_hosted_crlf_project_keeps_its_line_endings() {
9691000
crlf: true,
9701001
lockless: false,
9711002
refused: None,
1003+
prefetch: "",
9721004
};
9731005
let _ = run_shape(shape).await;
9741006
}
@@ -994,6 +1026,7 @@ async fn cargo_hosted_refuses_a_crate_another_crate_depends_on() {
9941026
crlf: false,
9951027
lockless: false,
9961028
refused: Some("redirect_cargo_transitive_dependents"),
1029+
prefetch: "",
9971030
};
9981031
let _ = run_shape(shape).await;
9991032
}
@@ -1020,6 +1053,82 @@ async fn cargo_hosted_refuses_a_lockless_project_with_other_dependencies() {
10201053
crlf: false,
10211054
lockless: true,
10221055
refused: Some("redirect_cargo_lockless_dependents"),
1056+
prefetch: "",
1057+
};
1058+
let _ = run_shape(shape).await;
1059+
}
1060+
1061+
fn local_crate(name: &str, version: &str) -> String {
1062+
format!("[package]\nname = \"{name}\"\nversion = \"{version}\"\nedition = \"2018\"\n")
1063+
}
1064+
1065+
/// #480: the user overrides the patched crate with a root
1066+
/// `[patch.crates-io]` path entry. Pinning the declaration to the Socket
1067+
/// registry would silently drop the override (cargo: "patch … was not used
1068+
/// in the crate graph") and, with a lock, break `cargo fetch --locked`, so
1069+
/// hosted mode refuses it and rewrites nothing. The project has no
1070+
/// committed `Cargo.lock` (a library): the crawl then walks the whole
1071+
/// registry cache, finds the crates.io copy another project fetched, and
1072+
/// hands the crate to the rewriter — which must read the override table.
1073+
/// (With a lock the rewriter refuses from the sourceless lock block; the
1074+
/// unit tests cover that, as the lock-scoped crawl never discovers it.)
1075+
#[tokio::test(flavor = "multi_thread")]
1076+
async fn cargo_hosted_refuses_a_crate_the_root_manifest_patch_overrides() {
1077+
let shape = Shape {
1078+
tag: "user-patch-same-crate",
1079+
files: vec![
1080+
(
1081+
"Cargo.toml",
1082+
format!(
1083+
"{}\n[patch.crates-io]\ncfg-if = {{ path = \"cfg-if-local\" }}\n",
1084+
consumer_manifest("cfg-if = \"1.0.4\"\n")
1085+
),
1086+
),
1087+
("src/main.rs", "fn main() {}\n".to_string()),
1088+
("cfg-if-local/Cargo.toml", local_crate("cfg-if", "1.0.4")),
1089+
(
1090+
"cfg-if-local/src/lib.rs",
1091+
"pub fn user_fork() {}\n".to_string(),
1092+
),
1093+
],
1094+
patches: vec![CFG_IF_1],
1095+
oracle: Vec::new(),
1096+
crlf: false,
1097+
lockless: true,
1098+
refused: Some("redirect_cargo_dep_overridden"),
1099+
prefetch: "cfg-if = \"=1.0.4\"\n",
1100+
};
1101+
let _ = run_shape(shape).await;
1102+
}
1103+
1104+
/// #480: the same override in the project `.cargo/config.toml` under the
1105+
/// index-URL spelling (cargo merges `[patch]` from the config chain): the
1106+
/// manifest says nothing, and the redirect is still refused.
1107+
#[tokio::test(flavor = "multi_thread")]
1108+
async fn cargo_hosted_refuses_a_crate_a_config_patch_overrides() {
1109+
let shape = Shape {
1110+
tag: "config-patch-same-crate",
1111+
files: vec![
1112+
("Cargo.toml", consumer_manifest("cfg-if = \"1.0.4\"\n")),
1113+
(
1114+
".cargo/config.toml",
1115+
"[patch.\"https://github.com/rust-lang/crates.io-index\"]\n\
1116+
cfg-if = { path = \"local/cfg-if\" }\n"
1117+
.to_string(),
1118+
),
1119+
("src/main.rs", "fn main() {}\n".to_string()),
1120+
("local/cfg-if/Cargo.toml", local_crate("cfg-if", "1.0.4")),
1121+
(
1122+
"local/cfg-if/src/lib.rs",
1123+
"pub fn user_fork() {}\n".to_string(),
1124+
),
1125+
],
1126+
patches: vec![CFG_IF_1],
1127+
oracle: Vec::new(),
1128+
crlf: false,
1129+
lockless: true,
1130+
refused: Some("redirect_cargo_dep_overridden"),
1131+
prefetch: "cfg-if = \"=1.0.4\"\n",
10231132
};
10241133
let _ = run_shape(shape).await;
10251134
}

0 commit comments

Comments
 (0)