Skip to content

Commit 1e5059f

Browse files
authored
fix: handle TLS deallocation (#324)
Previously, creating a temporary file from a TLS destructor could panic in fastrand (because the thread-local RNG may have been deallocated). Now, we fork the RNG before we create each file, falling back on an RNG with a static seed if the thread-local RNG has been deallocated. Two downsides to this patch: 1. Temporary files created during TLS deallocation will have extremely predictable names until the `getrandom` re-seed kicks in (assuming that feature is enabled). IMO, that's fine; this would panic previously. 2. `getrandom` re-seeding used to re-randomize to the entire per-thread RNG, now it only applies to the per-filename RNG. However, the will still serve its purpose as a mitigation against potential DoS attacks. I also considered managing the thread-local RNG myself instead of relying on fastrand, but that just isn't worth the added code, IMO. Thanks to @stoeckmann for reporting this and explaining the issue to me. I went with this version instead of their version because I needed to keep `tmpname` as a separate function for some tempfile v4 changes. fixes #281
1 parent c7b2e1a commit 1e5059f

File tree

2 files changed

+24
-15
lines changed

2 files changed

+24
-15
lines changed

src/util.rs

+6-4
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ use std::{io, iter::repeat_with};
44

55
use crate::error::IoResultExt;
66

7-
fn tmpname(prefix: &OsStr, suffix: &OsStr, rand_len: usize) -> OsString {
7+
fn tmpname(rng: &mut fastrand::Rng, prefix: &OsStr, suffix: &OsStr, rand_len: usize) -> OsString {
88
let capacity = prefix
99
.len()
1010
.saturating_add(suffix.len())
1111
.saturating_add(rand_len);
1212
let mut buf = OsString::with_capacity(capacity);
1313
buf.push(prefix);
1414
let mut char_buf = [0u8; 4];
15-
for c in repeat_with(fastrand::alphanumeric).take(rand_len) {
15+
for c in repeat_with(|| rng.alphanumeric()).take(rand_len) {
1616
buf.push(c.encode_utf8(&mut char_buf));
1717
}
1818
buf.push(suffix);
@@ -42,6 +42,8 @@ pub fn create_helper<R>(
4242
1
4343
};
4444

45+
// We fork the fastrand rng.
46+
let mut rng = fastrand::Rng::new();
4547
for i in 0..num_retries {
4648
// If we fail to create the file the first three times, re-seed from system randomness in
4749
// case an attacker is predicting our randomness (fastrand is predictable). If re-seeding
@@ -58,10 +60,10 @@ pub fn create_helper<R>(
5860
if i == 3 {
5961
let mut seed = [0u8; 8];
6062
if getrandom::fill(&mut seed).is_ok() {
61-
fastrand::seed(u64::from_ne_bytes(seed));
63+
rng.seed(u64::from_ne_bytes(seed));
6264
}
6365
}
64-
let path = base.join(tmpname(prefix, suffix, random_len));
66+
let path = base.join(tmpname(&mut rng, prefix, suffix, random_len));
6567
return match f(path) {
6668
Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists && num_retries > 1 => continue,
6769
// AddrInUse can happen if we're creating a UNIX domain socket and

tests/namedtempfile.rs

+18-11
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
use std::ffi::{OsStr, OsString};
44
use std::fs::File;
5-
use std::io::{Read, Seek, SeekFrom, Write};
5+
use std::io::{self, Read, Seek, SeekFrom, Write};
66
use std::path::{Path, PathBuf};
77
use tempfile::{env, tempdir, Builder, NamedTempFile, TempPath};
88

@@ -469,17 +469,24 @@ fn test_reseed() {
469469
// Deterministic seed.
470470
fastrand::seed(42);
471471

472+
// I need to create 5 conflicts but I can't just make 5 temporary files because we fork the RNG
473+
// each time we create a file.
472474
let mut attempts = 0;
473-
let _files: Vec<_> = std::iter::repeat_with(|| {
474-
Builder::new()
475-
.make(|path| {
476-
attempts += 1;
477-
File::options().write(true).create_new(true).open(path)
478-
})
479-
.unwrap()
480-
})
481-
.take(5)
482-
.collect();
475+
let mut files: Vec<_> = Vec::new();
476+
let _ = Builder::new().make(|path| -> io::Result<File> {
477+
if attempts == 5 {
478+
return Err(io::Error::new(io::ErrorKind::Other, "stop!"));
479+
}
480+
attempts += 1;
481+
let f = File::options()
482+
.write(true)
483+
.create_new(true)
484+
.open(path)
485+
.unwrap();
486+
487+
files.push(NamedTempFile::from_parts(f, TempPath::from_path(path)));
488+
Err(io::Error::new(io::ErrorKind::AlreadyExists, "fake!"))
489+
});
483490

484491
assert_eq!(5, attempts);
485492
attempts = 0;

0 commit comments

Comments
 (0)