Skip to content

Commit 81b274c

Browse files
committed
Add cargo config boundary check to VEX
When a .cargo/config.toml above the git root replaces crates.io with a directory source, VEX now refuses to verify cargo patches rather than attesting registry copies that cargo won't build. This matches the gate that apply already enforces.
1 parent 8e86006 commit 81b274c

1 file changed

Lines changed: 70 additions & 3 deletions

File tree

  • crates/socket-patch-cli/src/commands

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

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use std::path::{Path, PathBuf};
2121

2222
use clap::Args;
2323
use socket_patch_core::api::client::ApiClient;
24+
use socket_patch_core::crawlers::Ecosystem;
2425
use socket_patch_core::manifest::schema::{PatchManifest, PatchRecord};
2526
use socket_patch_core::telemetry::{track_vex_failed, track_vex_generated, TelemetryAuth};
2627
use socket_patch_core::vex::verify::{npm_manifest_after_hash, DEPENDENCY_NOT_INSTALLED};
@@ -327,6 +328,32 @@ impl VexGenError {
327328
}
328329
}
329330

331+
/// Check whether a workspace VEX run should refuse Cargo patches: a
332+
/// `.cargo/config` / `config.toml` sits above the repository root (stat
333+
/// only; it is never read), and cargo merges it into the build, so it may
334+
/// replace crates.io with another source while VEX verifies the shared
335+
/// registry cache. Returns `None` for a global run, a run without Cargo
336+
/// patches or a project that is no Cargo project.
337+
fn cargo_config_outside_root_vex(common: &GlobalArgs, manifest: &PatchManifest) -> Option<String> {
338+
let cwd = &common.cwd;
339+
if common.is_global()
340+
|| !common.ecosystem_selected(Ecosystem::Cargo)
341+
|| !manifest
342+
.patches
343+
.keys()
344+
.any(|p| Ecosystem::from_purl(p) == Some(Ecosystem::Cargo))
345+
|| !(cwd.join("Cargo.toml").is_file() || cwd.join("Cargo.lock").is_file())
346+
{
347+
return None;
348+
}
349+
socket_patch_core::vendor::cargo_config::config_above_repo(cwd).map(|why| {
350+
format!(
351+
"{why}: it may replace crates.io with another source, so the registry copy VEX \
352+
would verify may not be what cargo builds"
353+
)
354+
})
355+
}
356+
330357
pub async fn run(args: VexArgs) -> i32 {
331358
apply_env_toggles(&args.common);
332359

@@ -682,6 +709,20 @@ async fn generate_vex(
682709
Err(reason) => return Err(fail(common, params, "product_undetected", reason).await),
683710
};
684711

712+
// A cargo config above the repository root is never read in a
713+
// workspace run; it may move crates.io elsewhere, so Cargo patches
714+
// are refused instead of verifying the registry cache blind (the same
715+
// gate `apply` runs).
716+
let cargo_config_refused = cargo_config_outside_root_vex(common, manifest);
717+
if let Some(ref why) = cargo_config_refused {
718+
note_warning(warnings, common, "cargo_config_outside_root", why.clone());
719+
}
720+
let cargo_refused = |purl: &str| {
721+
cargo_config_refused.is_some()
722+
&& purl.starts_with("pkg:cargo/")
723+
&& manifest.patches.contains_key(purl)
724+
};
725+
685726
// The help text promises "PURL/identifier", so an arbitrary string is
686727
// accepted — but the OpenVEX spec types the product `@id` as an IRI, and
687728
// strict consumers (vexctl et al.) may reject or mis-key a bare name.
@@ -742,16 +783,20 @@ async fn generate_vex(
742783
applied: manifest
743784
.patches
744785
.keys()
745-
.filter(|purl| !mirror_refused(purl))
786+
.filter(|purl| !mirror_refused(purl) && !cargo_refused(purl))
746787
.cloned()
747788
.collect(),
748789
failed: manifest
749790
.patches
750791
.keys()
751-
.filter(|purl| mirror_refused(purl))
792+
.filter(|purl| mirror_refused(purl) || cargo_refused(purl))
752793
.map(|purl| FailedPatch {
753794
purl: purl.clone(),
754-
reason: "mirror_overrides_source".into(),
795+
reason: if mirror_refused(purl) {
796+
"mirror_overrides_source".into()
797+
} else {
798+
"cargo_config_outside_root".into()
799+
},
755800
})
756801
.collect(),
757802
vendored,
@@ -895,6 +940,24 @@ async fn generate_vex(
895940
outcome
896941
};
897942

943+
// A cargo config above the repository root moves any Cargo patches from
944+
// applied to failed: the crawler inspected the registry cache, but cargo
945+
// may build from elsewhere (a directory source). The same gate `apply`
946+
// runs.
947+
if cargo_config_refused.is_some() {
948+
let cargo_purls: Vec<String> = outcome
949+
.applied
950+
.iter()
951+
.filter(|purl| purl.starts_with("pkg:cargo/"))
952+
.cloned()
953+
.collect();
954+
outcome.applied.retain(|purl| !purl.starts_with("pkg:cargo/"));
955+
outcome.failed.extend(cargo_purls.into_iter().map(|purl| FailedPatch {
956+
purl,
957+
reason: "cargo_config_outside_root".into(),
958+
}));
959+
}
960+
898961
// In-run `scan --mode hosted --vex`: the bytes of deps THAT RUN confirmed
899962
// redirected live on the patch server until the next install, so their
900963
// verification against the local tree would spuriously fail
@@ -2138,6 +2201,10 @@ fn omission_phrase(reason: &str) -> &'static str {
21382201
"the lockfiles wire this package to different patches, so which one the build \
21392202
installs cannot be determined"
21402203
}
2204+
"cargo_config_outside_root" => {
2205+
"a cargo config above the repository root may replace crates.io with another \
2206+
source, so the registry copy cannot be verified"
2207+
}
21412208
_ => "the patch could not be verified",
21422209
}
21432210
}

0 commit comments

Comments
 (0)