Skip to content

Commit 03007d9

Browse files
committed
feat: re-seed from system randomness on collision
Re-seed thread-local RNG from system randomness if we run into a temporary file-name collision. This should address the concerns about using a predictable RNG without hurting performance in the common case where nobody is trying to predict our filenames. I'm only re-seeding once because if we _still_ fail to create a temporary file, the collision was likely due to too many temporary files instead of an attacker predicting our random temporary file names. I've also reduced the number of tries from 2^31 to 2^16. If it takes more than that to create a temporary file, something else is wrong. Pausing for a long time is usually worse than just failing. fixes #178
1 parent 16209da commit 03007d9

File tree

4 files changed

+44
-44
lines changed

4 files changed

+44
-44
lines changed

Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ fastrand = "2.1.1"
2222
# Not available in stdlib until 1.70, but we support 1.63 to support Debian stable.
2323
once_cell = { version = "1.19.0", default-features = false, features = ["std"] }
2424

25+
[target.'cfg(any(unix, windows, target_os = "wasi"))'.dependencies]
26+
getrandom = { version = "0.2.15", default-features = false }
27+
2528
[target.'cfg(any(unix, target_os = "wasi"))'.dependencies]
2629
rustix = { version = "0.38.39", features = ["fs"] }
2730

src/lib.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
//! `tempfile` doesn't rely on file paths so this isn't an issue. However, `NamedTempFile` does
3030
//! rely on file paths for _some_ operations. See the security documentation on
3131
//! the `NamedTempFile` type for more information.
32-
//!
32+
//!
3333
//! The OWASP Foundation provides a resource on vulnerabilities concerning insecure
3434
//! temporary files: https://owasp.org/www-community/vulnerabilities/Insecure_Temporary_File
3535
//!
@@ -150,7 +150,7 @@
150150
#[cfg(doctest)]
151151
doc_comment::doctest!("../README.md");
152152

153-
const NUM_RETRIES: u32 = 1 << 31;
153+
const NUM_RETRIES: u32 = 65536;
154154
const NUM_RAND_CHARS: usize = 6;
155155

156156
use std::ffi::OsStr;

src/util.rs

+15-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,21 @@ pub fn create_helper<R>(
3232
1
3333
};
3434

35-
for _ in 0..num_retries {
35+
for i in 0..num_retries {
36+
// If we fail to create the file the first time, re-seed from system randomness in case an
37+
// attacker is predicting our randomness (fastrand is predictable). If re-seeding doesn't help, either:
38+
//
39+
// 1. We have lots of temporary files, possibly created by an attacker but not necessarily.
40+
// Re-seeding the randomness won't help here.
41+
// 2. We're failing to create random files for some other reason. This shouldn't be the case
42+
// given that we're checking error kinds, but it could happen.
43+
#[cfg(any(windows, unix, target_os = "redox", target_os = "wasi"))]
44+
if i == 1 {
45+
let mut seed = [0u8; 8];
46+
if getrandom::getrandom(&mut seed).is_ok() {
47+
fastrand::seed(u64::from_ne_bytes(seed));
48+
}
49+
}
3650
let path = base.join(tmpname(prefix, suffix, random_len));
3751
return match f(path) {
3852
Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists && num_retries > 1 => continue,

tests/namedtempfile.rs

+24-41
Original file line numberDiff line numberDiff line change
@@ -421,49 +421,32 @@ fn test_make_uds() {
421421
#[cfg(unix)]
422422
#[test]
423423
fn test_make_uds_conflict() {
424+
use std::io::ErrorKind;
424425
use std::os::unix::net::UnixListener;
425-
use std::sync::atomic::{AtomicUsize, Ordering};
426-
use std::sync::Arc;
427-
428-
// Check that retries happen correctly by racing N different threads.
429-
430-
const NTHREADS: usize = 20;
431-
432-
// The number of times our callback was called.
433-
let tries = Arc::new(AtomicUsize::new(0));
434-
435-
let mut threads = Vec::with_capacity(NTHREADS);
436-
437-
for _ in 0..NTHREADS {
438-
let tries = tries.clone();
439-
threads.push(std::thread::spawn(move || {
440-
// Ensure that every thread uses the same seed so we are guaranteed
441-
// to retry. Note that fastrand seeds are thread-local.
442-
fastrand::seed(42);
443-
444-
Builder::new()
445-
.prefix("tmp")
446-
.suffix(".sock")
447-
.rand_bytes(12)
448-
.make(|path| {
449-
tries.fetch_add(1, Ordering::Relaxed);
450-
UnixListener::bind(path)
451-
})
452-
}));
453-
}
454426

455-
// Join all threads, but don't drop the temp file yet. Otherwise, we won't
456-
// get a deterministic number of `tries`.
457-
let sockets: Vec<_> = threads
458-
.into_iter()
459-
.map(|thread| thread.join().unwrap().unwrap())
460-
.collect();
461-
462-
// Number of tries is exactly equal to (n*(n+1))/2.
463-
assert_eq!(
464-
tries.load(Ordering::Relaxed),
465-
(NTHREADS * (NTHREADS + 1)) / 2
466-
);
427+
let sockets = std::iter::repeat_with(|| {
428+
Builder::new()
429+
.prefix("tmp")
430+
.suffix(".sock")
431+
.rand_bytes(1)
432+
.make(|path| UnixListener::bind(path))
433+
})
434+
.take_while(|r| match r {
435+
Ok(_) => true,
436+
Err(e) if matches!(e.kind(), ErrorKind::AddrInUse | ErrorKind::AlreadyExists) => false,
437+
Err(e) => panic!("unexpected error {e}"),
438+
})
439+
.collect::<Result<Vec<_>, _>>()
440+
.unwrap();
441+
442+
// Number of sockets we can create. Depends on whether or not the filesystem is case sensitive.
443+
444+
#[cfg(target_os = "macos")]
445+
const NUM_FILES: usize = 36;
446+
#[cfg(not(target_os = "macos"))]
447+
const NUM_FILES: usize = 62;
448+
449+
assert_eq!(sockets.len(), NUM_FILES);
467450

468451
for socket in sockets {
469452
assert!(socket.path().exists());

0 commit comments

Comments
 (0)