Skip to content

Commit f8768db

Browse files
committed
use confstr(_CS_DARWIN_USER_TEMP_DIR, ...) as a TMPDIR fallback on darwin
1 parent f8577b2 commit f8768db

File tree

3 files changed

+109
-7
lines changed

3 files changed

+109
-7
lines changed

library/std/src/env.rs

+12-3
Original file line numberDiff line numberDiff line change
@@ -600,19 +600,28 @@ pub fn home_dir() -> Option<PathBuf> {
600600
/// may result in "insecure temporary file" security vulnerabilities. Consider
601601
/// using a crate that securely creates temporary files or directories.
602602
///
603+
/// Note that the returned value may be a symbolic link, not a directory.
604+
///
603605
/// # Platform-specific behavior
604606
///
605607
/// On Unix, returns the value of the `TMPDIR` environment variable if it is
606-
/// set, otherwise for non-Android it returns `/tmp`. On Android, since there
607-
/// is no global temporary folder (it is usually allocated per-app), it returns
608-
/// `/data/local/tmp`.
608+
/// set, otherwise the value is OS-specific:
609+
/// - On Android, there is no global temporary folder (it is usually allocated
610+
/// per-app), it returns `/data/local/tmp`.
611+
/// - On Darwin-based OSes (macOS, iOS, etc) it returns the directory provided
612+
/// by `confstr(_CS_DARWIN_USER_TEMP_DIR, ...)`, as recommended by [Apple's
613+
/// security guidelines][appledoc].
614+
/// - On all other unix-based OSes, it returns `/tmp`.
615+
///
609616
/// On Windows, the behavior is equivalent to that of [`GetTempPath2`][GetTempPath2] /
610617
/// [`GetTempPath`][GetTempPath], which this function uses internally.
618+
///
611619
/// Note that, this [may change in the future][changes].
612620
///
613621
/// [changes]: io#platform-specific-behavior
614622
/// [GetTempPath2]: https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppath2a
615623
/// [GetTempPath]: https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppatha
624+
/// [appledoc]: https://developer.apple.com/library/archive/documentation/Security/Conceptual/SecureCodingGuide/Articles/RaceConditions.html#//apple_ref/doc/uid/TP40002585-SW10
616625
///
617626
/// ```no_run
618627
/// use std::env;

library/std/src/sys/unix/os.rs

+72-4
Original file line numberDiff line numberDiff line change
@@ -579,12 +579,80 @@ pub fn page_size() -> usize {
579579
unsafe { libc::sysconf(libc::_SC_PAGESIZE) as usize }
580580
}
581581

582+
// Returns the value for [`confstr(key, ...)`][posix_confstr]. Currently only
583+
// used on Darwin, but should work on any unix (in case we need to get
584+
// `_CS_PATH` or `_CS_V[67]_ENV` in the future).
585+
//
586+
// [posix_confstr]:
587+
// https://pubs.opengroup.org/onlinepubs/9699919799/functions/confstr.html
588+
#[cfg(any(target_os = "macos", target_os = "ios", target_os = "tvos", target_os = "watchos"))]
589+
fn confstr(key: c_int, size_hint: Option<usize>) -> io::Result<OsString> {
590+
let mut buf: Vec<u8> = Vec::new();
591+
let mut bytes_needed_including_nul = size_hint
592+
.unwrap_or_else(|| {
593+
// Treat "None" as "do an extra call to get the length". In theory
594+
// we could move this into the loop below, but it's hard to do given
595+
// that it isn't 100% clear if it's legal to pass 0 for `len` when
596+
// the buffer isn't null.
597+
unsafe { libc::confstr(key, core::ptr::null_mut(), 0) }
598+
})
599+
.max(1);
600+
// If the value returned by `confstr` is greater than the len passed into
601+
// it, then the value was truncated, meaning we need to retry. Note that
602+
// while `confstr` results don't seem to change for a process, it's unclear
603+
// if this is guaranteed anywhere, so looping does seem required.
604+
while bytes_needed_including_nul > buf.capacity() {
605+
// We write into the spare capacity of `buf`. This lets us avoid
606+
// changing buf's `len`, which both simplifies `reserve` computation,
607+
// allows working with `Vec<u8>` instead of `Vec<MaybeUninit<u8>>`, and
608+
// may avoid a copy, since the Vec knows that none of the bytes are needed
609+
// when reallocating (well, in theory anyway).
610+
buf.reserve(bytes_needed_including_nul);
611+
// `confstr` returns
612+
// - 0 in the case of errors: we break and return an error.
613+
// - The number of bytes written, iff the provided buffer is enough to
614+
// hold the entire value: we break and return the data in `buf`.
615+
// - Otherwise, the number of bytes needed (including nul): we go
616+
// through the loop again.
617+
bytes_needed_including_nul =
618+
unsafe { libc::confstr(key, buf.as_mut_ptr().cast::<c_char>(), buf.capacity()) };
619+
}
620+
// `confstr` returns 0 in the case of an error.
621+
if bytes_needed_including_nul == 0 {
622+
return Err(io::Error::last_os_error());
623+
}
624+
// Safety: `confstr(..., buf.as_mut_ptr(), buf.capacity())` returned a
625+
// non-zero value, meaning `bytes_needed_including_nul` bytes were
626+
// initialized.
627+
unsafe {
628+
buf.set_len(bytes_needed_including_nul);
629+
// Remove the NUL-terminator.
630+
let last_byte = buf.pop();
631+
// ... and smoke-check that it *was* a NUL-terminator.
632+
assert_eq!(last_byte, Some(0), "`confstr` provided a string which wasn't nul-terminated");
633+
};
634+
Ok(OsString::from_vec(buf))
635+
}
636+
637+
#[cfg(target_vendor = "apple")]
638+
fn darwin_temp_dir() -> PathBuf {
639+
confstr(libc::_CS_DARWIN_USER_TEMP_DIR, Some(64)).map(PathBuf::from).unwrap_or_else(|_| {
640+
// It failed for whatever reason (there are several possible reasons),
641+
// so return the global one.
642+
PathBuf::from("/tmp")
643+
})
644+
}
645+
582646
pub fn temp_dir() -> PathBuf {
583647
crate::env::var_os("TMPDIR").map(PathBuf::from).unwrap_or_else(|| {
584-
if cfg!(target_os = "android") {
585-
PathBuf::from("/data/local/tmp")
586-
} else {
587-
PathBuf::from("/tmp")
648+
cfg_if::cfg_if! {
649+
if #[cfg(any(target_os = "macos", target_os = "ios", target_os = "tvos", target_os = "watchos"))] {
650+
darwin_temp_dir()
651+
} else if #[cfg(target_os = "android")] {
652+
PathBuf::from("/data/local/tmp")
653+
} else {
654+
PathBuf::from("/tmp")
655+
}
588656
}
589657
})
590658
}

library/std/src/sys/unix/os/tests.rs

+25
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,28 @@ fn test_parse_glibc_version() {
2121
assert_eq!(parsed, super::parse_glibc_version(version_str));
2222
}
2323
}
24+
25+
// Smoke check `confstr`, do it for several hint values, to ensure our resizing
26+
// logic is correct.
27+
#[test]
28+
#[cfg(target_os = "macos")]
29+
fn test_confstr() {
30+
for key in [libc::_CS_DARWIN_USER_TEMP_DIR, libc::_CS_PATH] {
31+
let value_nohint = super::confstr(key, None).unwrap_or_else(|e| {
32+
panic!("confstr({key}, None) failed: {e:?}");
33+
});
34+
let end = (value_nohint.len() + 1) * 2;
35+
for hint in 0..end {
36+
assert_eq!(
37+
super::confstr(key, Some(hint)).as_deref().ok(),
38+
Some(&*value_nohint),
39+
"confstr({key}, Some({hint})) failed",
40+
);
41+
}
42+
}
43+
// Smoke check that we don't loop forever or something if the input was not valid.
44+
for hint in [None, Some(0), Some(1)] {
45+
let hopefully_invalid = 123456789_i32;
46+
assert!(super::confstr(hopefully_invalid, hint).is_err());
47+
}
48+
}

0 commit comments

Comments
 (0)