Skip to content

Commit ce72175

Browse files
committed
use_file: Remove use of spin-locks
Remove the general purpose spin-lock from getrandom, and don't spin when polling /dev/random. We can just use the poll() call itself to have threads wait for /dev/urandom to be safe. We also remove the use of spin locks when opening the persistent fd for platforms that require it. We take an approach similar to unsync_init, but in the very rare case that we open the file twice, we close the unnecessary fd. Signed-off-by: Joe Richey <[email protected]>
1 parent d661aa7 commit ce72175

File tree

3 files changed

+86
-94
lines changed

3 files changed

+86
-94
lines changed

src/use_file.rs

Lines changed: 86 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
// except according to those terms.
88

99
//! Implementations that just need to read from a file
10-
use crate::util_libc::{last_os_error, open_readonly, sys_fill_exact, LazyFd};
10+
use crate::util::LazyUsize;
11+
use crate::util_libc::{last_os_error, open_readonly, sys_fill_exact};
1112
use crate::Error;
13+
use core::sync::atomic::{AtomicUsize, Ordering::Relaxed};
1214

1315
#[cfg(target_os = "redox")]
1416
const FILE_PATH: &str = "rand:\0";
@@ -21,10 +23,21 @@ const FILE_PATH: &str = "rand:\0";
2123
target_os = "illumos"
2224
))]
2325
const FILE_PATH: &str = "/dev/random\0";
26+
#[cfg(any(target_os = "android", target_os = "linux"))]
27+
const FILE_PATH: &str = "/dev/urandom\0";
2428

2529
pub fn getrandom_inner(dest: &mut [u8]) -> Result<(), Error> {
26-
static FD: LazyFd = LazyFd::new();
27-
let fd = FD.init(init_file).ok_or_else(last_os_error)?;
30+
// On Linux, check that /dev/urandom will not return insecure values.
31+
#[cfg(any(target_os = "android", target_os = "linux"))]
32+
{
33+
static RANDOM_INIT: LazyUsize = LazyUsize::new();
34+
if RANDOM_INIT.unsync_init(random_init) == LazyUsize::UNINIT {
35+
return Err(last_os_error());
36+
}
37+
}
38+
39+
static FD: RngFd = RngFd::new();
40+
let fd = FD.sync_init().ok_or_else(last_os_error)?;
2841
let read = |buf: &mut [u8]| unsafe { libc::read(fd, buf.as_mut_ptr() as *mut _, buf.len()) };
2942

3043
if cfg!(target_os = "emscripten") {
@@ -38,36 +51,78 @@ pub fn getrandom_inner(dest: &mut [u8]) -> Result<(), Error> {
3851
Ok(())
3952
}
4053

41-
cfg_if! {
42-
if #[cfg(any(target_os = "android", target_os = "linux"))] {
43-
fn init_file() -> Option<libc::c_int> {
44-
// Poll /dev/random to make sure it is ok to read from /dev/urandom.
45-
let mut pfd = libc::pollfd {
46-
fd: unsafe { open_readonly("/dev/random\0")? },
47-
events: libc::POLLIN,
48-
revents: 0,
49-
};
54+
// Handle to the device file used to retrive random numbers
55+
struct RngFd(AtomicUsize);
56+
57+
impl RngFd {
58+
const fn new() -> Self {
59+
Self(AtomicUsize::new(LazyUsize::UNINIT))
60+
}
5061

51-
let ret = loop {
52-
// A negative timeout means an infinite timeout.
53-
let res = unsafe { libc::poll(&mut pfd, 1, -1) };
54-
if res == 1 {
55-
break unsafe { open_readonly("/dev/urandom\0") };
56-
} else if res < 0 {
57-
let e = last_os_error().raw_os_error();
58-
if e == Some(libc::EINTR) || e == Some(libc::EAGAIN) {
59-
continue;
60-
}
61-
}
62-
// We either hard failed, or poll() returned the wrong pfd.
63-
break None;
64-
};
65-
unsafe { libc::close(pfd.fd) };
66-
ret
62+
// Initializes and returns the file descriptor. Similar to open_readonly(),
63+
// None is returned on failure. On success, this function will always return
64+
// the same file descriptor, even from different threads. Note that this
65+
// does not mean only one file is ever opened, just that one "wins".
66+
fn sync_init(&self) -> Option<libc::c_int> {
67+
// Common and fast path with no contention. Don't wast time on CAS.
68+
match self.0.load(Relaxed) {
69+
LazyUsize::UNINIT => {}
70+
current_val => return Some(current_val as libc::c_int),
6771
}
68-
} else {
69-
fn init_file() -> Option<libc::c_int> {
70-
unsafe { open_readonly(FILE_PATH) }
72+
73+
let fd = unsafe { open_readonly(FILE_PATH)? };
74+
// The fd always fits in a usize without conflicting with UNINIT.
75+
debug_assert!(fd >= 0 && (fd as usize) < LazyUsize::UNINIT);
76+
let new_val = fd as usize;
77+
// Relaxed ordering is fine, as we only have a single atomic variable.
78+
match self.0.compare_and_swap(LazyUsize::UNINIT, new_val, Relaxed) {
79+
// We won the race and initilaized the fd, so we are done.
80+
LazyUsize::UNINIT => Some(fd),
81+
// Some other thread beat us to initilizing the fd. This is quite
82+
// rare as open(2) does not block when opening the random devices.
83+
// In this case, cleanup the unnecessary fd we opened.
84+
current_val => {
85+
unsafe { libc::close(fd) };
86+
Some(current_val as libc::c_int)
87+
}
88+
}
89+
}
90+
}
91+
92+
// Returns 0 if urandom is safe to read from, LazyUsize::UNINIT otherwise.
93+
#[cfg(any(target_os = "android", target_os = "linux"))]
94+
fn random_init() -> usize {
95+
// Make sure the temporary fd used for polling is always closed.
96+
struct FdGuard(libc::c_int);
97+
impl Drop for FdGuard {
98+
fn drop(&mut self) {
99+
unsafe { libc::close(self.0) };
100+
}
101+
}
102+
103+
// Poll /dev/random to make sure it is ok to read from /dev/urandom.
104+
let fd = match unsafe { open_readonly("/dev/random\0") } {
105+
Some(fd) => FdGuard(fd),
106+
None => return LazyUsize::UNINIT,
107+
};
108+
let mut pfd = libc::pollfd {
109+
fd: fd.0,
110+
events: libc::POLLIN,
111+
revents: 0,
112+
};
113+
114+
loop {
115+
// A negative timeout means an infinite timeout.
116+
let res = unsafe { libc::poll(&mut pfd, 1, -1) };
117+
if res == 1 {
118+
return 0;
119+
} else if res < 0 {
120+
let e = last_os_error().raw_os_error();
121+
if e == Some(libc::EINTR) || e == Some(libc::EAGAIN) {
122+
continue;
123+
}
71124
}
125+
// We either hard failed, or poll() returned the wrong pfd.
126+
return LazyUsize::UNINIT;
72127
}
73128
}

src/util.rs

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,6 @@ impl LazyUsize {
3535

3636
// The initialization is not completed.
3737
pub const UNINIT: usize = usize::max_value();
38-
// The initialization is currently running.
39-
pub const ACTIVE: usize = usize::max_value() - 1;
4038

4139
// Runs the init() function at least once, returning the value of some run
4240
// of init(). Multiple callers can run their init() functions in parallel.
@@ -50,36 +48,6 @@ impl LazyUsize {
5048
}
5149
val
5250
}
53-
54-
// Synchronously runs the init() function. Only one caller will have their
55-
// init() function running at a time, and exactly one successful call will
56-
// be run. init() returning UNINIT or ACTIVE will be considered a failure,
57-
// and future calls to sync_init will rerun their init() function.
58-
pub fn sync_init(&self, init: impl FnOnce() -> usize, mut wait: impl FnMut()) -> usize {
59-
// Common and fast path with no contention. Don't wast time on CAS.
60-
match self.0.load(Relaxed) {
61-
Self::UNINIT | Self::ACTIVE => {}
62-
val => return val,
63-
}
64-
// Relaxed ordering is fine, as we only have a single atomic variable.
65-
loop {
66-
match self.0.compare_and_swap(Self::UNINIT, Self::ACTIVE, Relaxed) {
67-
Self::UNINIT => {
68-
let val = init();
69-
self.0.store(
70-
match val {
71-
Self::UNINIT | Self::ACTIVE => Self::UNINIT,
72-
val => val,
73-
},
74-
Relaxed,
75-
);
76-
return val;
77-
}
78-
Self::ACTIVE => wait(),
79-
val => return val,
80-
}
81-
}
82-
}
8351
}
8452

8553
// Identical to LazyUsize except with bool instead of usize.

src/util_libc.rs

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -89,37 +89,6 @@ impl Weak {
8989
}
9090
}
9191

92-
pub struct LazyFd(LazyUsize);
93-
94-
impl LazyFd {
95-
pub const fn new() -> Self {
96-
Self(LazyUsize::new())
97-
}
98-
99-
// If init() returns Some(x), x should be nonnegative.
100-
pub fn init(&self, init: impl FnOnce() -> Option<libc::c_int>) -> Option<libc::c_int> {
101-
let fd = self.0.sync_init(
102-
|| match init() {
103-
// OK as val >= 0 and val <= c_int::MAX < usize::MAX
104-
Some(val) => val as usize,
105-
None => LazyUsize::UNINIT,
106-
},
107-
|| unsafe {
108-
// We are usually waiting on an open(2) syscall to complete,
109-
// which typically takes < 10us if the file is a device.
110-
// However, we might end up waiting much longer if the entropy
111-
// pool isn't initialized, but even in that case, this loop will
112-
// consume a negligible amount of CPU on most platforms.
113-
libc::usleep(10);
114-
},
115-
);
116-
match fd {
117-
LazyUsize::UNINIT => None,
118-
val => Some(val as libc::c_int),
119-
}
120-
}
121-
}
122-
12392
cfg_if! {
12493
if #[cfg(any(target_os = "linux", target_os = "emscripten"))] {
12594
use libc::open64 as open;

0 commit comments

Comments
 (0)