From 8cd9169f9e167a16ef4e46bdf3cd7b36e1f4724a Mon Sep 17 00:00:00 2001 From: snoopuppy582 Date: Fri, 17 Jul 2026 17:06:24 +0900 Subject: [PATCH 1/2] Support checksummed local registries for git sources --- src/compiler/fingerprint/mod.rs | 21 ++++- src/sources/config.rs | 10 ++- src/sources/registry/local.rs | 23 +++-- src/sources/registry/mod.rs | 59 ++++++++---- src/sources/replaced.rs | 23 ++++- src/workspace/summary.rs | 4 + tests/testsuite/local_registry.rs | 144 +++++++++++++++++++++++++++++- 7 files changed, 255 insertions(+), 29 deletions(-) diff --git a/src/compiler/fingerprint/mod.rs b/src/compiler/fingerprint/mod.rs index a5d75aee4cc..5438acf0fa7 100644 --- a/src/compiler/fingerprint/mod.rs +++ b/src/compiler/fingerprint/mod.rs @@ -1600,10 +1600,27 @@ fn calculate_normal( } else { let dep_info = dep_info_loc(build_runner, unit); let dep_info = dep_info.strip_prefix(&build_root).unwrap().to_path_buf(); - vec![LocalFingerprint::CheckDepInfo { + let mut local = vec![LocalFingerprint::CheckDepInfo { dep_info, checksum: build_runner.bcx.gctx.cli_unstable().checksum_freshness, - }] + }]; + let source_id = unit.pkg.package_id().source_id(); + let sources = build_runner.bcx.packages.sources(); + let source = sources + .get(source_id) + .ok_or_else(|| internal("missing package source"))?; + // Git and registry files are omitted from dep-info. A replacement can + // keep the same source path while its checksumless origin is updated. + if source.is_replaced() && !source.supports_checksums() { + let fingerprint = source.fingerprint(&unit.pkg).with_context(|| { + format!( + "failed to determine replacement source fingerprint for {}", + unit.pkg + ) + })?; + local.push(LocalFingerprint::Precalculated(fingerprint)); + } + local }; // Figure out what the outputs of our unit is, and we'll be storing them diff --git a/src/sources/config.rs b/src/sources/config.rs index d6daf723701..ca9b2aac567 100644 --- a/src/sources/config.rs +++ b/src/sources/config.rs @@ -193,7 +193,8 @@ impl<'gctx> SourceConfigMap<'gctx> { let new_src = self.load_overlaid(new_id)?; let old_src = id.load(self.gctx)?; - if !new_src.supports_checksums() && old_src.supports_checksums() { + let old_src_supports_checksums = old_src.supports_checksums(); + if !new_src.supports_checksums() && old_src_supports_checksums { bail!( "\ cannot replace `{orig}` with `{name}`, the source `{orig}` supports \ @@ -219,7 +220,12 @@ restore the source replacement configuration to continue the build ); } - Ok(Box::new(ReplacedSource::new(id, new_id, new_src))) + Ok(Box::new(ReplacedSource::new( + id, + new_id, + old_src_supports_checksums, + new_src, + ))) } /// Gets the [`Source`] for a given [`SourceId`] without performing any source replacement. diff --git a/src/sources/registry/local.rs b/src/sources/registry/local.rs index 1b244c07924..7415c1e547a 100644 --- a/src/sources/registry/local.rs +++ b/src/sources/registry/local.rs @@ -1,12 +1,14 @@ //! Access to a registry on the local filesystem. See [`LocalRegistry`] for more. -use crate::sources::registry::{LoadResponse, MaybeLock, RegistryConfig, RegistryData}; +use crate::sources::registry::{ + LoadResponse, LockMetadata, MaybeLock, PACKAGE_SOURCE_LOCK, RegistryConfig, RegistryData, +}; use crate::util::errors::CargoResult; use crate::util::{Filesystem, GlobalContext}; use crate::workspace::PackageId; use cargo_util::{Sha256, paths}; use std::cell::Cell; -use std::fs::File; +use std::fs::{self, File}; use std::io::SeekFrom; use std::io::{self, prelude::*}; use std::path::Path; @@ -178,10 +180,21 @@ impl<'gctx> RegistryData for LocalRegistry<'gctx> { let path = self.root.join(&pkg.tarball_name()).into_path_unlocked(); let mut crate_file = paths::open(&path)?; - // If we've already got an unpacked version of this crate, then skip the - // checksum below as it is in theory already verified. + // If this exact archive has already been unpacked, then its checksum was + // verified before the source lock was written. let dst = path.file_stem().unwrap(); - if self.src_path.join(dst).into_path_unlocked().exists() { + let source_lock = self + .src_path + .join(dst) + .into_path_unlocked() + .join(PACKAGE_SOURCE_LOCK); + let checksum_is_verified = fs::read_to_string(source_lock) + .ok() + .and_then(|contents| serde_json::from_str::(&contents).ok()) + .map_or(false, |metadata| { + metadata.is_valid_for_checksum(checksum, true) + }); + if checksum_is_verified { return Ok(MaybeLock::Ready(crate_file)); } diff --git a/src/sources/registry/mod.rs b/src/sources/registry/mod.rs index e43e61b8d98..b3006f7276c 100644 --- a/src/sources/registry/mod.rs +++ b/src/sources/registry/mod.rs @@ -195,7 +195,6 @@ use anyhow::Context as _; use cargo_util::paths; use cargo_util_terminal::report::Level; use flate2::read::GzDecoder; -use futures::FutureExt as _; use serde::Deserialize; use serde::Serialize; use tar::{Archive, EntryType}; @@ -211,7 +210,7 @@ use crate::util::{CargoResult, Filesystem, GlobalContext, LimitErrorReader, rest use crate::util::{VersionExt, hex}; use crate::workspace::dependency::Dependency; use crate::workspace::global_cache_tracker; -use crate::workspace::{Package, PackageId, SourceId}; +use crate::workspace::{Package, PackageId, SourceId, SourceKind}; pub use cargo_util_schemas::index::RegistryConfig; @@ -233,6 +232,19 @@ pub const CRATES_IO_DOMAIN: &str = "crates.io"; struct LockMetadata { /// The version of `.cargo-ok` file v: u32, + /// The checksum of the archive that was unpacked. + #[serde(default, skip_serializing_if = "Option::is_none")] + checksum: Option, +} + +impl LockMetadata { + fn is_valid_for_checksum(&self, checksum: &str, checksum_required: bool) -> bool { + self.v == 1 + && match self.checksum.as_deref() { + Some(unpacked_checksum) => unpacked_checksum == checksum, + None => !checksum_required, + } + } } /// A [`Source`] implementation for a local or a remote registry. @@ -551,7 +563,12 @@ impl<'gctx> RegistrySource<'gctx> { /// `.cargo-ok` file is found. /// /// [CVE-2022-36113]: https://blog.rust-lang.org/2022/09/14/cargo-cves.html#arbitrary-file-corruption-cve-2022-36113 - fn unpack_package(&self, pkg: PackageId, tarball: &File) -> CargoResult { + fn unpack_package( + &self, + pkg: PackageId, + tarball: &File, + checksum: &str, + ) -> CargoResult { let package_dir = format!("{}-{}", pkg.name(), pkg.version()); let dst = self.src_path.join(&package_dir); let path = dst.join(PACKAGE_SOURCE_LOCK); @@ -559,9 +576,10 @@ impl<'gctx> RegistrySource<'gctx> { .gctx .assert_package_cache_locked(CacheLockMode::DownloadExclusive, &path); let unpack_dir = path.parent().unwrap(); + let checksum_required = matches!(self.source_id.kind(), SourceKind::LocalRegistry); match fs::read_to_string(path) { Ok(ok) => match serde_json::from_str::(&ok) { - Ok(lock_meta) if lock_meta.v == 1 => { + Ok(lock_meta) if lock_meta.is_valid_for_checksum(checksum, checksum_required) => { self.gctx .deferred_global_last_use()? .mark_registry_src_used(global_cache_tracker::RegistrySrc { @@ -571,6 +589,10 @@ impl<'gctx> RegistrySource<'gctx> { }); return Ok(unpack_dir.to_path_buf()); } + Ok(lock_meta) if lock_meta.v == 1 => { + tracing::debug!("archive checksum metadata missing or changed, clearing cache"); + paths::remove_dir_all(dst.as_path_unlocked())?; + } _ => { if ok == "ok" { tracing::debug!("old `ok` content found, clearing cache"); @@ -598,7 +620,10 @@ impl<'gctx> RegistrySource<'gctx> { .open(&path) .with_context(|| format!("failed to open `{}`", path.display()))?; - let lock_meta = LockMetadata { v: 1 }; + let lock_meta = LockMetadata { + v: 1, + checksum: checksum_required.then(|| checksum.to_owned()), + }; write!(ok, "{}", serde_json::to_string(&lock_meta).unwrap())?; self.gctx @@ -645,9 +670,14 @@ impl<'gctx> RegistrySource<'gctx> { /// should only be called after doing integrity check. That is to say, /// you need to call either [`RegistryData::download`] or /// [`RegistryData::finish_download`] before calling this method. - async fn get_pkg(&self, package: PackageId, path: &File) -> CargoResult { + async fn get_pkg( + &self, + package: PackageId, + path: &File, + checksum: &str, + ) -> CargoResult { let path = self - .unpack_package(package, path) + .unpack_package(package, path, checksum) .with_context(|| format!("failed to unpack package `{}`", package))?; let src = PathSource::new(&path, self.source_id, self.gctx); src.load()?; @@ -658,15 +688,9 @@ impl<'gctx> RegistrySource<'gctx> { // After we've loaded the package configure its summary's `checksum` // field with the checksum we know for this `PackageId`. - let cksum = self - .index - .hash(package, &*self.ops) - .now_or_never() - .expect("a downloaded dep now pending!?") - .expect("summary not found"); pkg.manifest_mut() .summary_mut() - .set_checksum(cksum.to_string()); + .set_checksum(checksum.to_owned()); Ok(pkg) } @@ -862,7 +886,10 @@ impl<'gctx> Source for RegistrySource<'gctx> { async fn download(&self, package: PackageId) -> CargoResult { let hash = self.index.hash(package, &*self.ops).await?; match self.ops.download(package, &hash).await? { - MaybeLock::Ready(file) => self.get_pkg(package, &file).await.map(MaybePackage::Ready), + MaybeLock::Ready(file) => self + .get_pkg(package, &file, &hash) + .await + .map(MaybePackage::Ready), MaybeLock::Download { url, descriptor, @@ -878,7 +905,7 @@ impl<'gctx> Source for RegistrySource<'gctx> { async fn finish_download(&self, package: PackageId, data: Vec) -> CargoResult { let hash = self.index.hash(package, &*self.ops).await?; let file = self.ops.finish_download(package, &hash, &data).await?; - self.get_pkg(package, &file).await + self.get_pkg(package, &file, &hash).await } fn fingerprint(&self, pkg: &Package) -> CargoResult { diff --git a/src/sources/replaced.rs b/src/sources/replaced.rs index 14e891a3574..503985d4236 100644 --- a/src/sources/replaced.rs +++ b/src/sources/replaced.rs @@ -16,6 +16,8 @@ pub struct ReplacedSource<'gctx> { to_replace: SourceId, /// The identifier of the new replacement source. replace_with: SourceId, + /// Whether the original source supports checksums. + to_replace_supports_checksums: bool, inner: Box, } @@ -26,11 +28,13 @@ impl<'gctx> ReplacedSource<'gctx> { pub fn new( to_replace: SourceId, replace_with: SourceId, + to_replace_supports_checksums: bool, src: Box, ) -> ReplacedSource<'gctx> { ReplacedSource { to_replace, replace_with, + to_replace_supports_checksums, inner: src, } } @@ -55,7 +59,7 @@ impl<'gctx> Source for ReplacedSource<'gctx> { } fn supports_checksums(&self) -> bool { - self.inner.supports_checksums() + self.to_replace_supports_checksums } fn requires_precise(&self) -> bool { @@ -69,11 +73,20 @@ impl<'gctx> Source for ReplacedSource<'gctx> { f: &mut dyn FnMut(IndexSummary), ) -> CargoResult<()> { let (replace_with, to_replace) = (self.replace_with, self.to_replace); + let supports_checksums = self.to_replace_supports_checksums; let dep = dep.clone().map_source(to_replace, replace_with); self.inner .query(&dep, kind, &mut |summary| { - f(summary.map_summary(|s| s.map_source(replace_with, to_replace))) + f(summary.map_summary(|s| { + let mut s = s.map_source(replace_with, to_replace); + // The lockfile describes the original source, not the + // stronger guarantees offered by its replacement. + if !supports_checksums { + s.clear_checksum(); + } + s + })) }) .await .map_err(|e| { @@ -132,7 +145,11 @@ impl<'gctx> Source for ReplacedSource<'gctx> { } fn fingerprint(&self, id: &Package) -> CargoResult { - self.inner.fingerprint(id) + let replacement = self.inner.fingerprint(id)?; + match id.package_id().source_id().precise_git_fragment() { + Some(precise) => Ok(format!("{precise} {replacement}")), + None => Ok(replacement), + } } fn verify(&self, id: PackageId) -> CargoResult<()> { diff --git a/src/workspace/summary.rs b/src/workspace/summary.rs index 1cf96a88d99..0ecfa00a914 100644 --- a/src/workspace/summary.rs +++ b/src/workspace/summary.rs @@ -140,6 +140,10 @@ impl Summary { Arc::make_mut(&mut self.inner).checksum = Some(cksum); } + pub fn clear_checksum(&mut self) { + Arc::make_mut(&mut self.inner).checksum = None; + } + pub fn set_pubtime(&mut self, pubtime: jiff::Timestamp) { Arc::make_mut(&mut self.inner).pubtime = Some(pubtime); } diff --git a/tests/testsuite/local_registry.rs b/tests/testsuite/local_registry.rs index 100e58eff5b..2eaae95c377 100644 --- a/tests/testsuite/local_registry.rs +++ b/tests/testsuite/local_registry.rs @@ -5,7 +5,7 @@ use std::fs; use crate::prelude::*; use cargo_test_support::paths; use cargo_test_support::registry::{Package, registry_path}; -use cargo_test_support::{basic_manifest, project, str, t}; +use cargo_test_support::{basic_manifest, git, project, str, t}; fn setup() { let root = paths::root(); @@ -500,6 +500,148 @@ unable to verify that `bar v0.0.1` is the same as when the lockfile was generate .run(); } +#[cargo_test] +fn git_dependency_can_be_replaced_with_checksummed_local_registry() { + let git_project = git::new("dep", |project| { + project + .file("Cargo.toml", &basic_manifest("bar", "0.0.1")) + .file("src/lib.rs", "pub fn bar() {}") + }); + let p = project() + .file( + "Cargo.toml", + &format!( + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + bar = {{ git = '{}' }} + "#, + git_project.url() + ), + ) + .file( + "src/lib.rs", + "extern crate bar; pub fn foo() { bar::bar(); }", + ) + .build(); + + p.cargo("generate-lockfile").run(); + + Package::new("bar", "0.0.1") + .local(true) + .file("src/lib.rs", "pub fn bar() {}") + .publish(); + t!(fs::create_dir_all(paths::root().join(".cargo"))); + t!(fs::write( + paths::root().join(".cargo/config.toml"), + format!( + r#" + [source.git] + git = '{}' + replace-with = 'local' + + [source.local] + local-registry = 'registry' + "#, + git_project.url() + ) + )); + + p.cargo("build").run(); + + let lockfile = t!(fs::read_to_string(p.root().join("Cargo.lock"))); + assert!(!lockfile.contains("checksum")); + + t!(fs::remove_file(paths::root().join(".cargo/config.toml"))); + p.cargo("build").run(); +} + +#[cargo_test] +fn updated_git_dependency_refreshes_local_registry_source_cache() { + let (git_project, repo) = git::new_repo("dep", |project| { + project + .file("Cargo.toml", &basic_manifest("bar", "0.0.1")) + .file("src/lib.rs", "pub fn bar() {}") + }); + let p = project() + .file( + "Cargo.toml", + &format!( + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + bar = {{ git = '{}' }} + "#, + git_project.url() + ), + ) + .file( + "src/lib.rs", + "extern crate bar; pub fn foo() { bar::bar(); }", + ) + .build(); + let config = format!( + r#" + [source.git] + git = '{}' + replace-with = 'local' + + [source.local] + local-registry = 'registry' + "#, + git_project.url() + ); + let config_path = paths::root().join(".cargo/config.toml"); + + p.cargo("generate-lockfile").run(); + Package::new("bar", "0.0.1") + .local(true) + .file("src/lib.rs", "pub fn bar() {}") + .publish(); + t!(fs::create_dir_all(config_path.parent().unwrap())); + t!(fs::write(&config_path, &config)); + p.cargo("build").run(); + + git_project.change_file( + "src/lib.rs", + "pub fn bar() {} pub fn from_updated_commit() {}", + ); + git::add(&repo); + git::commit(&repo); + p.change_file( + "src/lib.rs", + "extern crate bar; pub fn foo() { bar::from_updated_commit(); }", + ); + + t!(fs::remove_file(&config_path)); + p.cargo("update -p bar").run(); + + registry_path().join("index").join("3").rm_rf(); + Package::new("bar", "0.0.1") + .local(true) + .file( + "src/lib.rs", + "pub fn bar() {} pub fn from_updated_commit() {}", + ) + .publish(); + t!(fs::write(&config_path, &config)); + + p.cargo("build").run(); + + let lockfile = t!(fs::read_to_string(p.root().join("Cargo.lock"))); + assert!(!lockfile.contains("checksum")); +} + #[cargo_test] fn crates_io_registry_url_is_optional() { let root = paths::root(); From f779256fc68e155278b0e1704c14acc1fb8d4eb4 Mon Sep 17 00:00:00 2001 From: snoopuppy582 Date: Fri, 17 Jul 2026 17:22:35 +0900 Subject: [PATCH 2/2] Avoid matching checksum text in git URLs --- tests/testsuite/local_registry.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/testsuite/local_registry.rs b/tests/testsuite/local_registry.rs index 2eaae95c377..d12201857da 100644 --- a/tests/testsuite/local_registry.rs +++ b/tests/testsuite/local_registry.rs @@ -555,7 +555,11 @@ fn git_dependency_can_be_replaced_with_checksummed_local_registry() { p.cargo("build").run(); let lockfile = t!(fs::read_to_string(p.root().join("Cargo.lock"))); - assert!(!lockfile.contains("checksum")); + assert!( + !lockfile + .lines() + .any(|line| line.trim_start().starts_with("checksum = ")) + ); t!(fs::remove_file(paths::root().join(".cargo/config.toml"))); p.cargo("build").run(); @@ -639,7 +643,11 @@ fn updated_git_dependency_refreshes_local_registry_source_cache() { p.cargo("build").run(); let lockfile = t!(fs::read_to_string(p.root().join("Cargo.lock"))); - assert!(!lockfile.contains("checksum")); + assert!( + !lockfile + .lines() + .any(|line| line.trim_start().starts_with("checksum = ")) + ); } #[cargo_test]