Temporary Files - Standard-Library Limitations
The Rust standard library provides std::env::temp_dir() -> PathBuf to query the operating system's temporary directory (/tmp or $TMPDIR on Unix, %TEMP% on Windows), but intentionally does not include a built-in temporary-file generator or auto-deleting TempFile / TempDir type in std.
Why Naive Temporary File Creation Is Unsafe
In a multi-user or multi-process system, constructing a predictable path like env::temp_dir().join("my_app_temp.txt") and calling File::create(&path) has three severe flaws:
- Symlink / TOCTOU Attack (
File::create): If an attacker creates a symbolic link/tmp/my_app_temp.txt -> /home/user/.ssh/authorized_keysbefore your program runs,File::createfollows the symlink and overwrites the target file! - Name Collisions Across Concurrent Processes or Tests: Two threads or test binaries running simultaneously overwrite each other's
/tmp/my_app_temp.txt. - Leaked Disk Files on Early Return or Panic: Without an RAII guard whose
Dropimplementation deletes the file, any early?error or panic leaves stale files in/tmp.
Unsafe Naive Pattern:
let p = env::temp_dir().join("fixed_name.tmp");
File::create(&p)?; // ❌ Follows symlinks, collides across processes, leaks on panic!
Safe Standard-Library Pattern (`File::create_new` + RAII Drop Guard):
1. Generate unique name (`PID` + atomic counter + timestamp)
2. Open with `File::create_new(&p)` (`O_CREAT | O_EXCL` -> NEVER follows symlinks,
retries if `ErrorKind::AlreadyExists`!)
3. Wrap in a `ScopedTempFile` struct whose `Drop` calls `let _ = fs::remove_file(&self.path);`!
Building a Secure, Self-Deleting ScopedTempFile in Pure std
By combining File::create_new (which uses OS O_CREAT | O_EXCL to guarantee the path did not previously exist as a file or symlink), an atomic counter + process ID, and a Drop implementation that removes the file when the guard leaves scope, you get collision-free, auto-cleaning temporary files in pure std (or use the ecosystem tempfile crate in production Cargo projects):
use std::fs::{self, File};
use std::io::{self, ErrorKind, Write};
use std::path::{Path, PathBuf};
use std::process;
use std::sync::atomic::{AtomicU64, Ordering};
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
pub struct ScopedTempFile {
file: Option<File>,
path: PathBuf,
}
impl ScopedTempFile {
pub fn new(prefix: &str) -> io::Result<Self> {
let base_dir = std::env::temp_dir();
let pid = process::id();
loop {
let seq = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
let candidate = base_dir.join(format!("{prefix}_{pid}_{seq}.tmp"));
// `File::create_new` uses `O_CREAT | O_EXCL`: it NEVER follows symlinks
// and returns `AlreadyExists` if another process took the name:
match File::create_new(&candidate) {
Ok(file) => {
return Ok(Self {
file: Some(file),
path: candidate,
});
}
Err(e) if e.kind() == ErrorKind::AlreadyExists => continue,
Err(e) => return Err(e),
}
}
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn file_mut(&mut self) -> &mut File {
self.file.as_mut().unwrap()
}
}
impl Drop for ScopedTempFile {
fn drop(&mut self) {
// Close the `File` handle first (required on Windows before deleting!),
// then remove the temporary file from disk:
drop(self.file.take());
let _ = fs::remove_file(&self.path);
}
}
fn main() -> io::Result<()> {
let saved_path: PathBuf;
{
let mut tmp = ScopedTempFile::new("rust_ch50")?;
saved_path = tmp.path().to_path_buf();
tmp.file_mut().write_all(b"auto-cleaned scratch data\n")?;
println!(
"inside scope -> exists: {}, content: {:?}",
fs::exists(&saved_path)?,
fs::read_to_string(&saved_path)?.trim()
);
} // `tmp` drops here, closing and deleting `saved_path` automatically!
println!("after scope -> exists: {}", fs::exists(&saved_path)?);
Ok(())
}
inside scope -> exists: true, content: "auto-cleaned scratch data"
after scope -> exists: false