From 8edfdbed77a5fe80dc50c029a53e9627cff70017 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 17 Jul 2024 15:44:26 -0400 Subject: [PATCH] Make entrypoint writes atomic to avoid overwriting symlinks (#5165) ## Summary It turns out that if `path` is a symlink, `File::create(path)?.write_all(content.as_ref())?` will overwrite the _target_ file. That means an entrypoint named `python` would actually overwrite the user's source Python executable, which is symlinked into the virtual environment. This PR replaces that code with our atomic write method. Closes https://github.com/astral-sh/uv/issues/5152. ## Test Plan I ran through the test plan `https://github.com/astral-sh/uv/issues/5152`, but used an executable named `bar` linked to `foo.txt` instead... --- crates/install-wheel-rs/src/wheel.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/crates/install-wheel-rs/src/wheel.rs b/crates/install-wheel-rs/src/wheel.rs index ee6ab1d5f584..2ef48cab53c3 100644 --- a/crates/install-wheel-rs/src/wheel.rs +++ b/crates/install-wheel-rs/src/wheel.rs @@ -474,8 +474,19 @@ fn install_script( let start = format_shebang(&layout.sys_executable, &layout.os_name) .as_bytes() .to_vec(); - let mut target = File::create(&script_absolute)?; + + let mut target = tempfile::NamedTempFile::new_in(&layout.scheme.scripts)?; let size_and_encoded_hash = copy_and_hash(&mut start.chain(script), &mut target)?; + target.persist(&script_absolute).map_err(|err| { + io::Error::new( + io::ErrorKind::Other, + format!( + "Failed to persist temporary file to {}: {}", + path.user_display(), + err.error + ), + ) + })?; fs::remove_file(&path)?; Some(size_and_encoded_hash) } else { @@ -604,7 +615,8 @@ pub(crate) fn write_file_recorded( relative_path.display() ); - File::create(site_packages.join(relative_path))?.write_all(content.as_ref())?; + uv_fs::write_atomic_sync(site_packages.join(relative_path), content.as_ref())?; + let hash = Sha256::new().chain_update(content.as_ref()).finalize(); let encoded_hash = format!("sha256={}", BASE64URL_NOPAD.encode(&hash)); record.push(RecordEntry {