From 09a615c09761d5066b19d4b58a6a1d0e515ede45 Mon Sep 17 00:00:00 2001 From: Tobias Bucher Date: Sun, 26 Aug 2018 21:13:44 +0200 Subject: [PATCH] Reduce number of syscalls in `rand` In case that it is statically known that the OS doesn't support `getrandom` (non-Linux) or becomes clear at runtime that `getrandom` isn't available (`ENOSYS`), the opened fd ("/dev/urandom") isn't closed after the function, so that future calls can reuse it. This saves repeated `open`/`close` system calls at the cost of one permanently open fd. Additionally, this skips the initial zero-length `getrandom` call and directly hands the user buffer to the operating system, saving one `getrandom` syscall. --- src/libstd/sys/unix/rand.rs | 96 ++++++++++++++++++++++--------------- 1 file changed, 57 insertions(+), 39 deletions(-) diff --git a/src/libstd/sys/unix/rand.rs b/src/libstd/sys/unix/rand.rs index 01c0ada4ffb..857d0370c29 100644 --- a/src/libstd/sys/unix/rand.rs +++ b/src/libstd/sys/unix/rand.rs @@ -30,8 +30,23 @@ mod imp { use fs::File; use io::Read; use libc; + use sync::atomic::{AtomicBool, AtomicI32, Ordering}; use sys::os::errno; + static GETRANDOM_URANDOM_FD: AtomicI32 = AtomicI32::new(-1); + #[cfg(any(target_os = "linux", target_os = "android"))] + static GETRANDOM_UNAVAILABLE: AtomicBool = AtomicBool::new(false); + + #[cfg(any(target_os = "linux", target_os = "android"))] + fn is_getrandom_permanently_unavailable() -> bool { + GETRANDOM_UNAVAILABLE.load(Ordering::Relaxed) + } + + #[cfg(not(any(target_os = "linux", target_os = "android")))] + fn is_getrandom_permanently_unavailable() -> bool { + true + } + #[cfg(any(target_os = "linux", target_os = "android"))] fn getrandom(buf: &mut [u8]) -> libc::c_long { unsafe { @@ -40,9 +55,14 @@ fn getrandom(buf: &mut [u8]) -> libc::c_long { } #[cfg(not(any(target_os = "linux", target_os = "android")))] - fn getrandom(_buf: &mut [u8]) -> libc::c_long { -1 } + fn getrandom_fill_bytes(_buf: &mut [u8]) -> bool { false } + #[cfg(any(target_os = "linux", target_os = "android"))] fn getrandom_fill_bytes(v: &mut [u8]) -> bool { + if is_getrandom_permanently_unavailable() { + return false; + } + let mut read = 0; while read < v.len() { let result = getrandom(&mut v[read..]); @@ -50,8 +70,10 @@ fn getrandom_fill_bytes(v: &mut [u8]) -> bool { let err = errno() as libc::c_int; if err == libc::EINTR { continue; + } else if err == libc::ENOSYS { + GETRANDOM_UNAVAILABLE.store(true, Ordering::Relaxed); } else if err == libc::EAGAIN { - return false + return false; } else { panic!("unexpected getrandom error: {}", err); } @@ -59,52 +81,48 @@ fn getrandom_fill_bytes(v: &mut [u8]) -> bool { read += result as usize; } } - - return true + true } - #[cfg(any(target_os = "linux", target_os = "android"))] - fn is_getrandom_available() -> bool { - use io; - use sync::atomic::{AtomicBool, Ordering}; - use sync::Once; - - static CHECKER: Once = Once::new(); - static AVAILABLE: AtomicBool = AtomicBool::new(false); - - CHECKER.call_once(|| { - let mut buf: [u8; 0] = []; - let result = getrandom(&mut buf); - let available = if result == -1 { - let err = io::Error::last_os_error().raw_os_error(); - err != Some(libc::ENOSYS) - } else { - true - }; - AVAILABLE.store(available, Ordering::Relaxed); - }); - - AVAILABLE.load(Ordering::Relaxed) - } - - #[cfg(not(any(target_os = "linux", target_os = "android")))] - fn is_getrandom_available() -> bool { false } - pub fn fill_bytes(v: &mut [u8]) { // getrandom_fill_bytes here can fail if getrandom() returns EAGAIN, // meaning it would have blocked because the non-blocking pool (urandom) - // has not initialized in the kernel yet due to a lack of entropy the + // has not initialized in the kernel yet due to a lack of entropy. The // fallback we do here is to avoid blocking applications which could // depend on this call without ever knowing they do and don't have a - // work around. The PRNG of /dev/urandom will still be used but not - // over a completely full entropy pool - if is_getrandom_available() && getrandom_fill_bytes(v) { - return + // work around. The PRNG of /dev/urandom will still be used but over a + // possibly predictable entropy pool. + if getrandom_fill_bytes(v) { + return; } - let mut file = File::open("/dev/urandom") - .expect("failed to open /dev/urandom"); - file.read_exact(v).expect("failed to read /dev/urandom"); + // getrandom failed for some reason. If the getrandom call is + // permanently unavailable (OS without getrandom, or OS version without + // getrandom), we'll keep around the fd for /dev/urandom for future + // requests, to avoid re-opening the file on every call. + // + // Otherwise, open /dev/urandom, read from it, and close it again. + use super::super::ext::io::{FromRawFd, IntoRawFd}; + let mut fd = GETRANDOM_URANDOM_FD.load(Ordering::Relaxed); + let mut close_fd = false; + if fd == -1 { + if !is_getrandom_permanently_unavailable() { + close_fd = true; + } + let file = File::open("/dev/urandom").expect("failed to open /dev/urandom"); + fd = file.into_raw_fd(); + // If some other thread also opened /dev/urandom and set the global + // fd already, we close our fd at the end of the function. + if !close_fd && GETRANDOM_URANDOM_FD.compare_and_swap(-1, fd, Ordering::Relaxed) != -1 { + close_fd = true; + } + } + let mut file = unsafe { File::from_raw_fd(fd) }; + let res = file.read_exact(v); + if !close_fd { + let _ = file.into_raw_fd(); + } + res.expect("failed to read /dev/urandom"); } } -- 2.44.0