From bae533962dbd95fea323f9f7b19e5727c0e43983 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Tue, 14 Jul 2026 07:11:54 -0500 Subject: [PATCH] Reject distribution metadata in wheel data --- crates/uv-install-wheel/src/install.rs | 25 ++++ crates/uv/tests/pip_install/pip_install.rs | 138 +++++++++++++++++++++ 2 files changed, 163 insertions(+) diff --git a/crates/uv-install-wheel/src/install.rs b/crates/uv-install-wheel/src/install.rs index 6a4ee177d19..96b629f9f5e 100644 --- a/crates/uv-install-wheel/src/install.rs +++ b/crates/uv-install-wheel/src/install.rs @@ -6,6 +6,7 @@ use std::str::FromStr; use fs_err::File; use tracing::{instrument, trace}; +use walkdir::WalkDir; use uv_distribution_filename::WheelFilename; use uv_pep440::Version; @@ -82,6 +83,30 @@ pub fn install_wheel( } } + // Validate `.data` before linking the wheel into site-packages. Distribution metadata in a + // data scheme could otherwise replace the metadata that was just validated or inject another + // installed distribution. + let data_dir = wheel.join(format!("{dist_info_prefix}.data")); + if data_dir.is_dir() { + for entry in WalkDir::new(&data_dir).min_depth(1) { + let entry = entry?; + if entry + .path() + .extension() + .and_then(|extension| extension.to_str()) + .is_some_and(|extension| { + extension.eq_ignore_ascii_case("dist-info") + || extension.eq_ignore_ascii_case("egg-info") + }) + { + return Err(Error::InvalidWheel(format!( + "Wheel data directory must not contain distribution metadata: {}", + entry.file_name().display() + ))); + } + } + } + // We're going step by step though // https://packaging.python.org/en/latest/specifications/binary-distribution-format/#installing-a-wheel-distribution-1-0-py32-none-any-whl // > 1.a Parse distribution-1.0.dist-info/WHEEL. diff --git a/crates/uv/tests/pip_install/pip_install.rs b/crates/uv/tests/pip_install/pip_install.rs index 118570ccace..c7bb033f679 100644 --- a/crates/uv/tests/pip_install/pip_install.rs +++ b/crates/uv/tests/pip_install/pip_install.rs @@ -97,6 +97,144 @@ fn write_many_files_wheel(path: &Path, source_files: usize) -> Result<()> { Ok(()) } +fn write_data_metadata_wheel( + path: &Path, + name: &str, + data_metadata: &str, + metadata_file: bool, +) -> Result<()> { + let mut writer = ZipFileWriter::new(Vec::new()); + let metadata = formatdoc! {" + Metadata-Version: 2.1 + Name: {name} + Version: 1.0.0 + "}; + let wheel = indoc! {" + Wheel-Version: 1.0 + Generator: uv-test + Root-Is-Purelib: true + Tag: py3-none-any + "}; + let data_metadata = if metadata_file { + format!("{name}-1.0.0.data/purelib/{data_metadata}") + } else { + format!("{name}-1.0.0.data/purelib/{data_metadata}/METADATA") + }; + let entries = [ + (format!("{name}/__init__.py"), "VALUE = 1\n".to_string()), + (format!("{name}-1.0.0.dist-info/METADATA"), metadata.clone()), + (format!("{name}-1.0.0.dist-info/WHEEL"), wheel.to_string()), + ( + data_metadata, + "Name: injected\nVersion: 9.9.9\n".to_string(), + ), + ]; + let mut record = String::new(); + for (entry_name, contents) in entries { + let entry = ZipEntryBuilder::new(entry_name.clone().into(), Compression::Stored); + block_on(writer.write_entry_whole(entry, contents.as_bytes()))?; + writeln!(record, "{entry_name},,")?; + } + writeln!(record, "{name}-1.0.0.dist-info/RECORD,,")?; + let record_name = format!("{name}-1.0.0.dist-info/RECORD"); + let entry = ZipEntryBuilder::new(record_name.into(), Compression::Stored); + block_on(writer.write_entry_whole(entry, record.as_bytes()))?; + fs_err::write(path, block_on(writer.close())?)?; + Ok(()) +} + +#[test] +fn reject_wheel_data_distribution_metadata() -> Result<()> { + let context = uv_test::test_context!("3.12"); + + let overwrite = context.temp_dir.join("overwrite-1.0.0-py3-none-any.whl"); + write_data_metadata_wheel(&overwrite, "overwrite", "overwrite-1.0.0.dist-info", false)?; + uv_snapshot!(context.filters(), context.pip_install().arg(&overwrite), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + error: Failed to install: overwrite-1.0.0-py3-none-any.whl (overwrite==1.0.0 (from file://[TEMP_DIR]/overwrite-1.0.0-py3-none-any.whl)) + Caused by: The wheel is invalid: Wheel data directory must not contain distribution metadata: overwrite-1.0.0.dist-info + "); + assert!( + !context + .site_packages() + .join("overwrite-1.0.0.dist-info") + .exists() + ); + + let inject = context.temp_dir.join("inject-1.0.0-py3-none-any.whl"); + write_data_metadata_wheel(&inject, "inject", "phantom-9.9.9.DIST-INFO", false)?; + uv_snapshot!(context.filters(), context.pip_install().arg(&inject), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + error: Failed to install: inject-1.0.0-py3-none-any.whl (inject==1.0.0 (from file://[TEMP_DIR]/inject-1.0.0-py3-none-any.whl)) + Caused by: The wheel is invalid: Wheel data directory must not contain distribution metadata: phantom-9.9.9.DIST-INFO + "); + assert!( + !context + .site_packages() + .join("phantom-9.9.9.DIST-INFO") + .exists() + ); + + let egg_info = context.temp_dir.join("egg-1.0.0-py3-none-any.whl"); + write_data_metadata_wheel(&egg_info, "egg", "phantom-9.9.9.EGG-INFO", false)?; + uv_snapshot!(context.filters(), context.pip_install().arg(&egg_info), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + error: Failed to install: egg-1.0.0-py3-none-any.whl (egg==1.0.0 (from file://[TEMP_DIR]/egg-1.0.0-py3-none-any.whl)) + Caused by: The wheel is invalid: Wheel data directory must not contain distribution metadata: phantom-9.9.9.EGG-INFO + "); + assert!( + !context + .site_packages() + .join("phantom-9.9.9.EGG-INFO") + .exists() + ); + + let egg_info_file = context.temp_dir.join("eggfile-1.0.0-py3-none-any.whl"); + write_data_metadata_wheel( + &egg_info_file, + "eggfile", + "phantom-file-9.9.9.egg-info", + true, + )?; + uv_snapshot!(context.filters(), context.pip_install().arg(&egg_info_file), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + error: Failed to install: eggfile-1.0.0-py3-none-any.whl (eggfile==1.0.0 (from file://[TEMP_DIR]/eggfile-1.0.0-py3-none-any.whl)) + Caused by: The wheel is invalid: Wheel data directory must not contain distribution metadata: phantom-file-9.9.9.egg-info + "); + assert!( + !context + .site_packages() + .join("phantom-file-9.9.9.egg-info") + .exists() + ); + + Ok(()) +} + #[test] fn missing_requirements_txt() { let context = uv_test::test_context!("3.12");