From 374e8c0bf7c7a22d062d794026372f3ee3f372ef Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Sat, 15 Aug 2026 19:35:12 -0600 Subject: [PATCH] fix: prevent tmp-overwriting --- src/function/mod.rs | 42 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/src/function/mod.rs b/src/function/mod.rs index 005e9ac..ce6f9ae 100644 --- a/src/function/mod.rs +++ b/src/function/mod.rs @@ -32,7 +32,7 @@ use skill::SKILL_FUNCTION_PREFIX; use std::ffi::OsStr; use std::fs::File; use std::io::{Read, Write}; -use std::sync::atomic::Ordering; +use std::sync::atomic::{AtomicU64, Ordering}; use std::{collections::VecDeque, thread}; use std::{ collections::{HashMap, HashSet}, @@ -159,7 +159,12 @@ pub(crate) fn write_file_atomic( .file_name() .and_then(OsStr::to_str) .ok_or_else(|| anyhow!("Unable to extract file name from path: {}", path.display()))?; - let tmp = path.with_file_name(format!(".{file_name}.tmp.{}", std::process::id())); + static TMP_COUNTER: AtomicU64 = AtomicU64::new(0); + let tmp = path.with_file_name(format!( + ".{file_name}.tmp.{}.{}", + std::process::id(), + TMP_COUNTER.fetch_add(1, Ordering::Relaxed) + )); fs::write(&tmp, content)?; #[cfg(unix)] @@ -2385,6 +2390,39 @@ mod tests { fs::remove_dir_all(&dir).unwrap(); } + #[test] + fn write_file_atomic_concurrent_writers_to_same_target() { + let dir = temp_file("-atomic-concurrent-", ""); + fs::create_dir_all(&dir).unwrap(); + let path = dir.join("shim"); + + let contents: Vec = (0..8) + .map(|i| format!("#!/bin/sh\necho writer-{i}\n")) + .collect(); + thread::scope(|scope| { + for content in &contents { + scope.spawn(|| { + for _ in 0..50 { + write_file_atomic(&path, content, Some(0o755)).unwrap(); + } + }); + } + }); + + let final_content = fs::read_to_string(&path).unwrap(); + assert!( + contents.contains(&final_content), + "final content must be one writer's complete content, got: {final_content:?}" + ); + assert_eq!( + fs::read_dir(&dir).unwrap().count(), + 1, + "no tmp files left behind" + ); + + fs::remove_dir_all(&dir).unwrap(); + } + #[test] fn bin_entry_stem_strips_run_prefix_and_extension() { assert_eq!(bin_entry_stem("fs_grep"), "fs_grep");