-
-
Notifications
You must be signed in to change notification settings - Fork 440
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Unix fork protection #466
Conversation
We could consider using |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good work!
- CI passes, so it seems this is not broken anywhere (I guess Unix implies libc or a compatible replacement)
- Presumably performance overhead is minimal
So probably fine, but I'll take a closer look later. Also this will wait until after the 0.5 release.
src/rngs/adapter/reseeding.rs
Outdated
@@ -161,11 +166,14 @@ where R: BlockRngCore + SeedableRng, | |||
/// * `reseeder`: the RNG to use for reseeding. | |||
pub fn new(rng: R, threshold: u64, reseeder: Rsdr) -> Self { | |||
assert!(threshold <= ::core::i64::MAX as u64); | |||
register_fork_handler(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You call this for every new reseeding RNG...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about this also. We can use RESEEDING_RNG_FORK_COUNTER
to see if it has already been registered, by setting it to 0 the first time (and something like u64::MAX
otherwise).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not unless you're careful to avoid reseed_counter
ever being set to MAX
. Obviously it shouldn't, but seems more fragile regarding code adjustment, so better to initialise to 0 then update to 1 maybe?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it matters much. But I would like to see someone forking a process u64::MAX
times 😄.
src/rngs/adapter/reseeding.rs
Outdated
#[cfg(all(feature="std", unix))] | ||
fn register_fork_handler() { | ||
unsafe { | ||
libc::pthread_atfork(None, None, Some(forkhandler)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
... to register a parameterless function updating a static variable. I don't think this approach is wrong, but it will probably update the counter more than necessary as soon as multiple threads/reseeding RNGs are used.
So use std::sync::Once
to register forkhandler
maybe?
acdd6d7
to
4caf86b
Compare
Forgot about emscripten, which is sort-of Unix but without threads/forking etc.
That is completely the intention 😄. |
Before:
After:
(the |
The very simple program I used for testing: extern crate rand;
extern crate libc;
extern crate env_logger;
use rand::prelude::*;
fn main() {
env_logger::init();
println!("{}", thread_rng().gen::<u32>());
unsafe {
libc::fork();
}
for i in 0..16 {
println!("{}, {}", i, thread_rng().gen::<u32>());
}
} Output:
At round nr 15 the fork is detected, the RNG of the forked process gets reseeded, and the generated values are different. |
Well, it should be detected after 16 outputs, right? Though looks like 15 above (including initial number); maybe something else used
Well presumably it still implements the pthread API but doesn't do anything? Looks like there is experimental pthread support in fact. |
My example is probably not the clearest. It uses one value from
Ah, didn't know that it is being experimented with. The example code didn't compile without removing emscripten support, but I could re-check that... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also document the fork protection on ReseedingRng
?
src/rngs/adapter/reseeding.rs
Outdated
let threshold = if let Err(e) = self.reseed() { | ||
let fork_counter = get_fork_counter(); | ||
if self.fork_counter < fork_counter { | ||
warn!("Fork detected, reseeding RNG"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably info
or trace
is more appropriate. It's not actually a "problem". In a way it would be nice to use the same level for both.
src/rngs/adapter/reseeding.rs
Outdated
if self.bytes_until_reseed <= 0 { | ||
// We get better performance by not calling only `auto_reseed` here | ||
if self.bytes_until_reseed <= 0 || | ||
self.fork_counter < get_fork_counter() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we'd get better performance by using binary OR (|
) to allow calculation of both conditions simultaneously? Normally both would be false.
src/rngs/adapter/reseeding.rs
Outdated
let num_bytes = | ||
results.as_ref().len() * size_of::<<R as BlockRngCore>::Item>(); | ||
self.fork_counter = fork_counter; | ||
self.threshold - num_bytes as i64 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
num_bytes
should be small relative to threshold
and we aren't exact with its handling anyway, so is there any point subtracting it? You've already adjusted the should_retry
behaviour here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, why make this change at all (i.e. why not revert the num_bytes
subtraction to how it was)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Forgot (and rediscovered) why I changed things here, the old core was slightly confusing for me.
threshold
is set to 0
on ErrorKind::Transient
. Then bytes_until_reseed
was threshold - num_bytes
. Does the case Transient
work correctly?
I have partly undone the changes, and only set the delay caused by ErrorKind::Transient
to num_bytes
, to get the right value when logging.
src/rngs/adapter/reseeding.rs
Outdated
fn register_fork_handler() {} | ||
|
||
#[cfg(all(feature="std", unix, not(target_os="emscripten")))] | ||
fn get_fork_counter() -> u64 { unsafe { RESEEDING_RNG_FORK_COUNTER } } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you know why reading mutable static data is unsafe? Because it's not even guaranteed that the value read will equal the old or new value if concurrent with a write.
So can get_fork_counter()
and forkhandler()
be called concurrently? If so we should instead use std::sync::atomic::AtomicUsize
(with relaxed ordering).
src/rngs/adapter/reseeding.rs
Outdated
static mut RESEEDING_RNG_FORK_COUNTER: u64 = ::core::u64::MAX; | ||
|
||
#[cfg(all(feature="std", unix, not(target_os="emscripten")))] | ||
extern fn forkhandler() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fork_handler
for consistency?
I thought it didn't matter if checking/setting the static was a bit racy, but that was wrong. It will probably not be before the weekend that I can change update this. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't need to be postponed any longer. Do you want to update it?
src/rngs/adapter/reseeding.rs
Outdated
let num_bytes = | ||
results.as_ref().len() * size_of::<<R as BlockRngCore>::Item>(); | ||
self.fork_counter = fork_counter; | ||
self.threshold - num_bytes as i64 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, why make this change at all (i.e. why not revert the num_bytes
subtraction to how it was)?
Updated, but only tested on Windows. And that doesn't do much good for Unix fork protection 😄. I'll test it this evening or tomorrow. |
fc48dce
to
e96b616
Compare
Thanks. I tested successfully: extern crate rand;
extern crate libc;
extern crate env_logger;
use rand::prelude::*;
fn main() {
env_logger::init();
println!("10 random numbers: {:?}",
(0..10).map(|_| thread_rng().gen::<u32>()).collect::<Vec<_>>());
println!("Forking");
unsafe {
libc::fork();
}
for i in 0..10 {
println!("{}\t{}", i, thread_rng().gen::<u32>());
}
} output:
|
Thank you for testing 👍. Just remembered the documentation still needs a little work. Now that |
src/rngs/adapter/reseeding.rs
Outdated
} else { | ||
// FIXME: the amount logged here may not be correct if the intial | ||
// `bytes_until_reseed` was set by a delay instead of `threshold`. | ||
trace!("Reseeding RNG after {} generated bytes", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need to include the byte count here at all. In fact since it can be incorrect, it might be better not to.
src/rngs/adapter/reseeding.rs
Outdated
// `RESEEDING_RNG_FORK_COUNTER`, it is time to reseed this RNG. | ||
// | ||
// If reseeding fails, we don't deal with this by setting a delay, but just | ||
// don't update `fork_counter`, so a reseed is attempted a soon as possible. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as soon as
src/rngs/adapter/reseeding.rs
Outdated
// If reseeding fails, we don't deal with this by setting a delay, but just | ||
// don't update `fork_counter`, so a reseed is attempted a soon as possible. | ||
#[cfg(all(feature="std", unix, not(target_os="emscripten")))] | ||
static RESEEDING_RNG_FORK_COUNTER: AtomicUsize = ATOMIC_USIZE_INIT; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could use #[cfg(..)] mod fork { .. }
so that the cfg
stuff doesn't need to be repeated so much. Not important.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Much cleaner 😄
src/rngs/adapter/reseeding.rs
Outdated
/// - On a manual call to [`reseed()`]. | ||
/// - After `clone()`, the clone will be reseeded on first use. | ||
/// - After a process is forked, the RNG in the child process is reseeded on | ||
/// first use. Several values may be returned before the fork is detected. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After a process is forked, the RNG in the child process is reseeded within the next few generated values, depending on the block size of the underlying PRNG. For ChaChaRng
and Hc128Rng
this is a maximum of 15 u32
values before reseeding.
Your doc isn't quite accurate, and also not very precise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, better 👍
src/rngs/adapter/reseeding.rs
Outdated
/// * `threshold`: the number of generated bytes after which to reseed the RNG. | ||
/// * `reseeder`: the RNG to use for reseeding. | ||
/// `threshold` sets the number of generated bytes after which to reseed the | ||
/// PRNG. Set it to zero to never reseed based on the number of renerated |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
generated
src/rngs/adapter/reseeding.rs
Outdated
// `RESEEDING_RNG_FORK_COUNTER`, it is time to reseed this RNG. | ||
// | ||
// If reseeding fails, we don't deal with this by setting a delay, but just | ||
// don't update `fork_counter`, so a reseed is attempted as soon a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as soon as
Third time lucky? 😆
And support setting no threshold with zero
b84861f
to
5edec75
Compare
This adds a simple kind of fork protection to
ReseedingRng
usingpthread_atfork
and a global static.It is not optimal in two ways:
Let's first see how bad the CI thinks it is...