Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions crates/uv-configuration/src/crc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use std::sync::LazyLock;

use uv_static::EnvVars;

#[derive(Debug)]
pub enum CRCMode {
/// Fail on CRC mismatch.
Enforce,
/// Warn on CRC mismatch, but continue.
Lax,
Comment on lines +8 to +10
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious why not "Error" and "Warn" here which feel more canonical to me?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(which would also be "Ignore" instead of "None", I think)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we'd then want a different name? UV_CRC_CHECK = error | warn | ignore? I feel less strongly about that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to do the change once I'm back to personal laptop

/// Skip CRC checks.
None,
}

/// Lazily initialize CRC mode from `UV_CRC_MODE`.
pub static CURRENT_CRC_MODE: LazyLock<CRCMode> =
LazyLock::new(|| match std::env::var(EnvVars::UV_CRC_MODE).as_deref() {
Ok("enforce") => CRCMode::Enforce,
Ok("lax") => CRCMode::Lax,
_ => CRCMode::None,
});
2 changes: 2 additions & 0 deletions crates/uv-configuration/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub use build_options::*;
pub use concurrency::*;
pub use config_settings::*;
pub use constraints::*;
pub use crc::*;
pub use dependency_groups::*;
pub use dry_run::*;
pub use editable::*;
Expand All @@ -28,6 +29,7 @@ mod build_options;
mod concurrency;
mod config_settings;
mod constraints;
mod crc;
mod dependency_groups;
mod dry_run;
mod editable;
Expand Down
1 change: 1 addition & 0 deletions crates/uv-extract/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ workspace = true
uv-configuration = { workspace = true }
uv-distribution-filename = { workspace = true }
uv-pypi-types = { workspace = true }
uv-warnings = { workspace = true }

astral-tokio-tar = { workspace = true }
async-compression = { workspace = true, features = ["bzip2", "gzip", "zstd", "xz"] }
Expand Down
33 changes: 22 additions & 11 deletions crates/uv-extract/src/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ use rustc_hash::FxHashSet;
use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
use tracing::warn;

use uv_configuration::{CRCMode, CURRENT_CRC_MODE};
use uv_distribution_filename::SourceDistExtension;
use uv_warnings::warn_user;

use crate::Error;

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

// Validate the CRC of any file we unpack
// (It would be nice if async_zip made it harder to Not do this...)
let reader = reader.into_inner();
let computed = reader.compute_hash();
let expected = reader.entry().crc32();
if computed != expected {
return Err(Error::BadCrc32 {
path: relpath,
computed,
expected,
});
if matches!(*CURRENT_CRC_MODE, CRCMode::Enforce | CRCMode::Lax) {
// Validate the CRC of any file we unpack
// (It would be nice if async_zip made it harder to Not do this...)
let reader = reader.into_inner();
let computed = reader.compute_hash();
let expected = reader.entry().crc32();

if computed != expected {
if let CRCMode::Enforce = *CURRENT_CRC_MODE {
return Err(Error::BadCrc32 {
path: relpath,
computed,
expected,
});
}
warn_user!(
"Bad CRC (got {computed:08x}, expected {expected:08x}) for file: {}",
relpath.display()
);
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions crates/uv-metadata/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ license.workspace = true
doctest = false

[dependencies]
uv-configuration = { workspace = true }
uv-distribution-filename = { workspace = true }
uv-normalize = { workspace = true }
uv-pypi-types = { workspace = true }
uv-warnings = { workspace = true }

async_zip = { workspace = true }
fs-err = { workspace = true }
Expand Down
35 changes: 23 additions & 12 deletions crates/uv-metadata/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ use std::path::Path;
use thiserror::Error;
use tokio::io::AsyncReadExt;
use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
use zip::ZipArchive;

use uv_configuration::{CRCMode, CURRENT_CRC_MODE};
use uv_distribution_filename::WheelFilename;
use uv_normalize::{DistInfoName, InvalidNameError};
use uv_pypi_types::ResolutionMetadata;
use zip::ZipArchive;
use uv_warnings::warn_user;

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

// Validate the CRC of any file we unpack
// (It would be nice if async_zip made it harder to Not do this...)
let reader = reader.into_inner();
let computed = reader.compute_hash();
let expected = reader.entry().crc32();
if computed != expected {
return Err(Error::BadCrc32 {
path,
computed,
expected,
});
if matches!(*CURRENT_CRC_MODE, CRCMode::Enforce | CRCMode::Lax) {
// Validate the CRC of any file we unpack
// (It would be nice if async_zip made it harder to Not do this...)
let reader = reader.into_inner();
let computed = reader.compute_hash();
let expected = reader.entry().crc32();

if computed != expected {
if let CRCMode::Enforce = *CURRENT_CRC_MODE {
return Err(Error::BadCrc32 {
path,
computed,
expected,
});
}
warn_user!(
"Bad CRC (got {computed:08x}, expected {expected:08x}) for file: {path}"
);
}
}

let metadata = ResolutionMetadata::parse_metadata(&contents)
Expand Down
5 changes: 5 additions & 0 deletions crates/uv-static/src/env_vars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,11 @@ impl EnvVars {
/// Specifies the directory for storing managed Python installations.
pub const UV_PYTHON_INSTALL_DIR: &'static str = "UV_PYTHON_INSTALL_DIR";

/// Specifies how CRC validation is performed during unzipping a download stream.
///
/// Possible values are `enforce`, `lax`, and `none` (default).
pub const UV_CRC_MODE: &'static str = "UV_CRC_MODE";

/// Managed Python installations are downloaded from the Astral
/// [`python-build-standalone`](https://github.com/astral-sh/python-build-standalone) project.
///
Expand Down
51 changes: 42 additions & 9 deletions crates/uv/tests/it/pip_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8913,27 +8913,60 @@ fn missing_subdirectory_url() -> Result<()> {
// This wheel was uploaded with a bad crc32 and we weren't detecting that
// (Could be replaced with a checked-in hand-crafted corrupt wheel?)
#[test]
fn bad_crc32() -> Result<()> {
let context = TestContext::new("3.11");
let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.touch()?;
fn bad_crc32() {
let context = TestContext::new("3.11").with_filtered_counts();

uv_snapshot!(context.pip_install()
uv_snapshot!(context.filters(), context.pip_install()
.env(EnvVars::UV_CRC_MODE, "enforce")
.arg("--python-platform").arg("linux")
.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"
.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###"
success: false
exit_code: 1
----- stdout -----

----- stderr -----
Resolved 7 packages in [TIME]
Resolved [N] packages in [TIME]
× 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`
├─▶ Failed to extract archive: osqp-1.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
╰─▶ Bad CRC (got ca5f1131, expected d5c95dfa) for file: osqp/ext_builtin.cpython-311-x86_64-linux-gnu.so
"
"###
);

Ok(())
uv_snapshot!(context.filters(), context.pip_install()
.env(EnvVars::UV_CRC_MODE, "lax")
.arg("--python-platform").arg("linux")
.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###"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Resolved [N] packages in [TIME]
warning: Bad CRC (got ca5f1131, expected d5c95dfa) for file: osqp/ext_builtin.cpython-311-x86_64-linux-gnu.so
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ jinja2==3.1.3
+ joblib==1.3.2
+ markupsafe==2.1.5
+ numpy==1.26.4
+ 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)
+ scipy==1.12.0
+ setuptools==69.2.0
"###
);

uv_snapshot!(context.filters(), context.pip_install()
.env(EnvVars::UV_CRC_MODE, "none")
.arg("--python-platform").arg("linux")
.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###"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Audited [N] packages in [TIME]
"###
);
}

#[test]
Expand Down
6 changes: 6 additions & 0 deletions docs/configuration/environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ local `uv.toml` file to use as the configuration file.
Equivalent to the `--constraint` command-line argument. If set, uv will use this
file as the constraints file. Uses space-separated list of files.

### `UV_CRC_MODE`

Specifies how CRC validation is performed during unzipping a download stream.

Possible values are `enforce`, `lax`, and `none` (default).

### `UV_CUSTOM_COMPILE_COMMAND`

Equivalent to the `--custom-compile-command` command-line argument.
Expand Down
Loading