Skip to content

Commit c25217e

Browse files
committed
Add CRC mode to make CRC checks opt-in by default
1 parent c4fd34f commit c25217e

File tree

10 files changed

+123
-32
lines changed

10 files changed

+123
-32
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/uv-distribution-filename/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ uv-normalize = { workspace = true }
2121
uv-pep440 = { workspace = true }
2222
uv-platform-tags = { workspace = true }
2323
uv-small-str = { workspace = true }
24+
uv-static = { workspace = true }
2425

2526
memchr = { workspace = true }
2627
rkyv = { workspace = true, features = ["smallvec-1"] }
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
use std::sync::LazyLock;
2+
3+
use uv_static::EnvVars;
4+
5+
#[derive(Debug)]
6+
pub enum CRCMode {
7+
/// Fail on CRC mismatch.
8+
Enforce,
9+
/// Warn on CRC mismatch, but continue.
10+
Lax,
11+
/// Skip CRC checks.
12+
None,
13+
}
14+
15+
/// Lazily initialize CRC mode from `UV_CRC_MODE`.
16+
pub static CURRENT_CRC_MODE: LazyLock<CRCMode> =
17+
LazyLock::new(|| match std::env::var(EnvVars::UV_CRC_MODE).as_deref() {
18+
Ok("enforce") => CRCMode::Enforce,
19+
Ok("lax") => CRCMode::Lax,
20+
_ => CRCMode::None,
21+
});

crates/uv-distribution-filename/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ use uv_normalize::PackageName;
44
use uv_pep440::Version;
55

66
pub use build_tag::{BuildTag, BuildTagError};
7+
pub use crc::{CRCMode, CURRENT_CRC_MODE};
78
pub use egg::{EggInfoFilename, EggInfoFilenameError};
89
pub use extension::{DistExtension, ExtensionError, SourceDistExtension};
910
pub use source_dist::{SourceDistFilename, SourceDistFilenameError};
1011
pub use wheel::{WheelFilename, WheelFilenameError};
1112

1213
mod build_tag;
14+
mod crc;
1315
mod egg;
1416
mod extension;
1517
mod source_dist;

crates/uv-extract/src/stream.rs

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
77
use tracing::warn;
88

99
use uv_distribution_filename::SourceDistExtension;
10+
use uv_distribution_filename::{CRCMode, CURRENT_CRC_MODE};
1011

1112
use crate::Error;
1213

@@ -86,17 +87,26 @@ pub async fn unzip<R: tokio::io::AsyncRead + Unpin>(
8687
let mut reader = entry.reader_mut().compat();
8788
tokio::io::copy(&mut reader, &mut writer).await?;
8889

89-
// Validate the CRC of any file we unpack
90-
// (It would be nice if async_zip made it harder to Not do this...)
91-
let reader = reader.into_inner();
92-
let computed = reader.compute_hash();
93-
let expected = reader.entry().crc32();
94-
if computed != expected {
95-
return Err(Error::BadCrc32 {
96-
path: relpath,
97-
computed,
98-
expected,
99-
});
90+
if matches!(*CURRENT_CRC_MODE, CRCMode::Enforce | CRCMode::Lax) {
91+
// Validate the CRC of any file we unpack
92+
// (It would be nice if async_zip made it harder to Not do this...)
93+
let reader = reader.into_inner();
94+
let computed = reader.compute_hash();
95+
let expected = reader.entry().crc32();
96+
97+
if computed != expected {
98+
if let CRCMode::Enforce = *CURRENT_CRC_MODE {
99+
return Err(Error::BadCrc32 {
100+
path: relpath,
101+
computed,
102+
expected,
103+
});
104+
}
105+
warn!(
106+
"Bad CRC (got {computed:08x}, expected {expected:08x}) for file: {}",
107+
relpath.display()
108+
);
109+
}
100110
}
101111
}
102112

crates/uv-metadata/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ futures = { workspace = true }
2323
thiserror = { workspace = true }
2424
tokio = { workspace = true }
2525
tokio-util = { workspace = true }
26+
tracing = { workspace = true }
2627
zip = { workspace = true }
2728

2829
[lints]

crates/uv-metadata/src/lib.rs

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@ use std::path::Path;
99
use thiserror::Error;
1010
use tokio::io::AsyncReadExt;
1111
use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
12+
use tracing::warn;
13+
use zip::ZipArchive;
14+
1215
use uv_distribution_filename::WheelFilename;
16+
use uv_distribution_filename::{CRCMode, CURRENT_CRC_MODE};
1317
use uv_normalize::{DistInfoName, InvalidNameError};
1418
use uv_pypi_types::ResolutionMetadata;
15-
use zip::ZipArchive;
1619

1720
/// The caller is responsible for attaching the path or url we failed to read.
1821
#[derive(Debug, Error)]
@@ -252,17 +255,23 @@ pub async fn read_metadata_async_stream<R: futures::AsyncRead + Unpin>(
252255
let mut contents = Vec::new();
253256
reader.read_to_end(&mut contents).await.unwrap();
254257

255-
// Validate the CRC of any file we unpack
256-
// (It would be nice if async_zip made it harder to Not do this...)
257-
let reader = reader.into_inner();
258-
let computed = reader.compute_hash();
259-
let expected = reader.entry().crc32();
260-
if computed != expected {
261-
return Err(Error::BadCrc32 {
262-
path,
263-
computed,
264-
expected,
265-
});
258+
if matches!(*CURRENT_CRC_MODE, CRCMode::Enforce | CRCMode::Lax) {
259+
// Validate the CRC of any file we unpack
260+
// (It would be nice if async_zip made it harder to Not do this...)
261+
let reader = reader.into_inner();
262+
let computed = reader.compute_hash();
263+
let expected = reader.entry().crc32();
264+
265+
if computed != expected {
266+
if let CRCMode::Enforce = *CURRENT_CRC_MODE {
267+
return Err(Error::BadCrc32 {
268+
path,
269+
computed,
270+
expected,
271+
});
272+
}
273+
warn!("Bad CRC (got {computed:08x}, expected {expected:08x}) for file: {path}");
274+
}
266275
}
267276

268277
let metadata = ResolutionMetadata::parse_metadata(&contents)

crates/uv-static/src/env_vars.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,11 @@ impl EnvVars {
257257
/// Specifies the directory for storing managed Python installations.
258258
pub const UV_PYTHON_INSTALL_DIR: &'static str = "UV_PYTHON_INSTALL_DIR";
259259

260+
/// Specifies how CRC validation is performed during unzipping a download stream.
261+
///
262+
/// Possible values are `enforce`, `lax`, and `none` (default).
263+
pub const UV_CRC_MODE: &'static str = "UV_CRC_MODE";
264+
260265
/// Managed Python installations are downloaded from the Astral
261266
/// [`python-build-standalone`](https://github.com/astral-sh/python-build-standalone) project.
262267
///

crates/uv/tests/it/pip_install.rs

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8913,27 +8913,61 @@ fn missing_subdirectory_url() -> Result<()> {
89138913
// This wheel was uploaded with a bad crc32 and we weren't detecting that
89148914
// (Could be replaced with a checked-in hand-crafted corrupt wheel?)
89158915
#[test]
8916-
fn bad_crc32() -> Result<()> {
8917-
let context = TestContext::new("3.11");
8918-
let requirements_txt = context.temp_dir.child("requirements.txt");
8919-
requirements_txt.touch()?;
8916+
fn bad_crc32() {
8917+
let context = TestContext::new("3.11").with_filtered_counts();
89208918

8921-
uv_snapshot!(context.pip_install()
8919+
uv_snapshot!(context.filters(), context.pip_install()
8920+
.env(EnvVars::UV_CRC_MODE, "enforce")
89228921
.arg("--python-platform").arg("linux")
8923-
.arg("osqp @ https://files.pythonhosted.org/packages/00/04/5959347582ab970e9b922f27585d34f7c794ed01125dac26fb4e7dd80205/osqp-1.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"), @r"
8922+
.arg("osqp @ https://files.pythonhosted.org/packages/00/04/5959347582ab970e9b922f27585d34f7c794ed01125dac26fb4e7dd80205/osqp-1.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"), @r###"
89248923
success: false
89258924
exit_code: 1
89268925
----- stdout -----
89278926
89288927
----- stderr -----
8929-
Resolved 7 packages in [TIME]
8928+
Resolved [N] packages in [TIME]
89308929
× Failed to download `osqp @ https://files.pythonhosted.org/packages/00/04/5959347582ab970e9b922f27585d34f7c794ed01125dac26fb4e7dd80205/osqp-1.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl`
89318930
├─▶ Failed to extract archive: osqp-1.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
89328931
╰─▶ Bad CRC (got ca5f1131, expected d5c95dfa) for file: osqp/ext_builtin.cpython-311-x86_64-linux-gnu.so
8933-
"
8932+
"###
89348933
);
89358934

8936-
Ok(())
8935+
// When using --verbose, we should see
8936+
// WARN Bad CRC (got ca5f1131, expected d5c95dfa) for file: osqp/ext_builtin.cpython-311-x86_64-linux-gnu.so
8937+
uv_snapshot!(context.filters(), context.pip_install()
8938+
.env(EnvVars::UV_CRC_MODE, "lax")
8939+
.arg("--python-platform").arg("linux")
8940+
.arg("osqp @ https://files.pythonhosted.org/packages/00/04/5959347582ab970e9b922f27585d34f7c794ed01125dac26fb4e7dd80205/osqp-1.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"), @r###"
8941+
success: true
8942+
exit_code: 0
8943+
----- stdout -----
8944+
8945+
----- stderr -----
8946+
Resolved [N] packages in [TIME]
8947+
Prepared [N] packages in [TIME]
8948+
Installed [N] packages in [TIME]
8949+
+ jinja2==3.1.3
8950+
+ joblib==1.3.2
8951+
+ markupsafe==2.1.5
8952+
+ numpy==1.26.4
8953+
+ osqp==1.0.2 (from https://files.pythonhosted.org/packages/00/04/5959347582ab970e9b922f27585d34f7c794ed01125dac26fb4e7dd80205/osqp-1.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl)
8954+
+ scipy==1.12.0
8955+
+ setuptools==69.2.0
8956+
"###
8957+
);
8958+
8959+
uv_snapshot!(context.filters(), context.pip_install()
8960+
.env(EnvVars::UV_CRC_MODE, "none")
8961+
.arg("--python-platform").arg("linux")
8962+
.arg("osqp @ https://files.pythonhosted.org/packages/00/04/5959347582ab970e9b922f27585d34f7c794ed01125dac26fb4e7dd80205/osqp-1.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"), @r###"
8963+
success: true
8964+
exit_code: 0
8965+
----- stdout -----
8966+
8967+
----- stderr -----
8968+
Audited [N] packages in [TIME]
8969+
"###
8970+
);
89378971
}
89388972

89398973
#[test]

docs/configuration/environment.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ local `uv.toml` file to use as the configuration file.
5151
Equivalent to the `--constraint` command-line argument. If set, uv will use this
5252
file as the constraints file. Uses space-separated list of files.
5353

54+
### `UV_CRC_MODE`
55+
56+
Specifies how CRC validation is performed during unzipping a download stream.
57+
58+
Possible values are `enforce`, `lax`, and `none` (default).
59+
5460
### `UV_CUSTOM_COMPILE_COMMAND`
5561

5662
Equivalent to the `--custom-compile-command` command-line argument.

0 commit comments

Comments
 (0)