Skip to content

Commit

Permalink
Merge pull request #466 from pitdicker/fork_protection
Browse files Browse the repository at this point in the history
Add Unix fork protection
  • Loading branch information
dhardy committed Jun 15, 2018
2 parents 757f8ee + 5edec75 commit bd3a806
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 55 deletions.
2 changes: 1 addition & 1 deletion src/lib.rs
Expand Up @@ -251,7 +251,7 @@ extern crate rand_core;
#[cfg(feature = "log")] #[macro_use] extern crate log;
#[cfg(not(feature = "log"))] macro_rules! trace { ($($x:tt)*) => () }
#[cfg(not(feature = "log"))] macro_rules! debug { ($($x:tt)*) => () }
#[cfg(all(feature="std", not(feature = "log")))] macro_rules! info { ($($x:tt)*) => () }
#[cfg(not(feature = "log"))] macro_rules! info { ($($x:tt)*) => () }
#[cfg(not(feature = "log"))] macro_rules! warn { ($($x:tt)*) => () }
#[cfg(all(feature="std", not(feature = "log")))] macro_rules! error { ($($x:tt)*) => () }

Expand Down
215 changes: 161 additions & 54 deletions src/rngs/adapter/reseeding.rs
Expand Up @@ -16,44 +16,71 @@ use core::mem::size_of;
use rand_core::{RngCore, CryptoRng, SeedableRng, Error, ErrorKind};
use rand_core::block::{BlockRngCore, BlockRng};

/// A wrapper around any PRNG which reseeds the underlying PRNG after it has
/// generated a certain number of random bytes.
/// A wrapper around any PRNG that implements [`BlockRngCore`], that adds the
/// ability to reseed it.
///
/// When the RNG gets cloned, the clone is reseeded on first use.
/// `ReseedingRng` reseeds the underlying PRNG in the following cases:
///
/// Reseeding is never strictly *necessary*. Cryptographic PRNGs don't have a
/// limited number of bytes they can output, or at least not a limit reachable
/// in any practical way. There is no such thing as 'running out of entropy'.
/// - 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 within
/// the next few generated values, depending on the block size of the
/// underlying PRNG. For [`ChaChaCore`] and [`Hc128Core`] this is a maximum of
/// 15 `u32` values before reseeding.
/// - After the PRNG has generated a configurable number of random bytes.
///
/// Some small non-cryptographic PRNGs can have very small periods, for
/// example less than 2<sup>64</sup>. Would reseeding help to ensure that you do
/// not wrap around at the end of the period? A period of 2<sup>64</sup> still
/// takes several centuries of CPU-years on current hardware. Reseeding will
/// actually make things worse, because the reseeded PRNG will just continue
/// somewhere else *in the same period*, with a high chance of overlapping with
/// previously used parts of it.
/// # When should reseeding after a fixed number of generated bytes be used?
///
/// # When should you use `ReseedingRng`?
/// Reseeding after a fixed number of generated bytes is never strictly
/// *necessary*. Cryptographic PRNGs don't have a limited number of bytes they
/// can output, or at least not a limit reachable in any practical way. There is
/// no such thing as 'running out of entropy'.
///
/// - Reseeding can be seen as some form of 'security in depth'. Even if in the
/// future a cryptographic weakness is found in the CSPRNG being used,
/// occasionally reseeding should make exploiting it much more difficult or
/// even impossible.
/// - It can be used as a poor man's cryptography (not recommended, just use a
/// good CSPRNG). Previous implementations of `thread_rng` for example used
/// `ReseedingRng` with the ISAAC RNG. That algorithm, although apparently
/// strong and with no known attack, does not come with any proof of security
/// and does not meet the current standards for a cryptographically secure
/// PRNG. By reseeding it frequently (every 32 kiB) it seems safe to assume
/// there is no attack that can operate on the tiny window between reseeds.
/// Occasionally reseeding can be seen as some form of 'security in depth'. Even
/// if in the future a cryptographic weakness is found in the CSPRNG being used,
/// or a flaw in the implementation, occasionally reseeding should make
/// exploiting it much more difficult or even impossible.
///
/// Use [`ReseedingRng::new`] with a `threshold` of `0` to disable reseeding
/// after a fixed number of generated bytes.
///
/// # Error handling
///
/// Although extremely unlikely, reseeding the wrapped PRNG can fail.
/// `ReseedingRng` will never panic but try to handle the error intelligently
/// through some combination of retrying and delaying reseeding until later.
/// Although unlikely, reseeding the wrapped PRNG can fail. `ReseedingRng` will
/// never panic but try to handle the error intelligently through some
/// combination of retrying and delaying reseeding until later.
/// If handling the source error fails `ReseedingRng` will continue generating
/// data from the wrapped PRNG without reseeding.
///
/// Manually calling [`reseed()`] will not have this retry or delay logic, but
/// reports the error.
///
/// # Example
///
/// ```
/// use rand::prelude::*;
/// use rand::prng::chacha::ChaChaCore; // Internal part of ChaChaRng that
/// // implements BlockRngCore
/// use rand::rngs::OsRng;
/// use rand::rngs::adapter::ReseedingRng;
///
/// let prng = ChaChaCore::from_entropy();
// FIXME: it is better to use EntropyRng as reseeder, but that doesn't implement
// clone yet.
/// let reseeder = OsRng::new().unwrap();
/// let mut reseeding_rng = ReseedingRng::new(prng, 0, reseeder);
///
/// println!("{}", reseeding_rng.gen::<u64>());
///
/// let mut cloned_rng = reseeding_rng.clone();
/// assert!(reseeding_rng.gen::<u64>() != cloned_rng.gen::<u64>());
/// ```
///
/// [`ChaChaCore`]: ../../prng/chacha/struct.ChaChaCore.html
/// [`Hc128Core`]: ../../prng/hc128/struct.Hc128Core.html
/// [`BlockRngCore`]: ../../../rand_core/block/trait.BlockRngCore.html
/// [`ReseedingRng::new`]: struct.ReseedingRng.html#method.new
/// [`reseed()`]: struct.ReseedingRng.html#method.reseed
#[derive(Debug)]
pub struct ReseedingRng<R, Rsdr>(BlockRng<ReseedingCore<R, Rsdr>>)
where R: BlockRngCore + SeedableRng,
Expand All @@ -63,13 +90,12 @@ impl<R, Rsdr> ReseedingRng<R, Rsdr>
where R: BlockRngCore + SeedableRng,
Rsdr: RngCore
{
/// Create a new `ReseedingRng` with the given parameters.
/// Create a new `ReseedingRng` from an existing PRNG, combined with a RNG
/// to use as reseeder.
///
/// # Arguments
///
/// * `rng`: the random number generator to use.
/// * `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 generated
/// values.
pub fn new(rng: R, threshold: u64, reseeder: Rsdr) -> Self {
ReseedingRng(BlockRng::new(ReseedingCore::new(rng, threshold, reseeder)))
}
Expand Down Expand Up @@ -126,6 +152,7 @@ struct ReseedingCore<R, Rsdr> {
reseeder: Rsdr,
threshold: i64,
bytes_until_reseed: i64,
fork_counter: usize,
}

impl<R, Rsdr> BlockRngCore for ReseedingCore<R, Rsdr>
Expand All @@ -136,11 +163,13 @@ where R: BlockRngCore + SeedableRng,
type Results = <R as BlockRngCore>::Results;

fn generate(&mut self, results: &mut Self::Results) {
if self.bytes_until_reseed <= 0 {
// We get better performance by not calling only `auto_reseed` here
let global_fork_counter = fork::get_fork_counter();
if self.bytes_until_reseed <= 0 ||
self.is_forked(global_fork_counter) {
// We get better performance by not calling only `reseed` here
// and continuing with the rest of the function, but by directly
// returning from a non-inlined function.
return self.reseed_and_generate(results);
return self.reseed_and_generate(results, global_fork_counter);
}
let num_bytes = results.as_ref().len() * size_of::<Self::Item>();
self.bytes_until_reseed -= num_bytes as i64;
Expand All @@ -152,20 +181,26 @@ impl<R, Rsdr> ReseedingCore<R, Rsdr>
where R: BlockRngCore + SeedableRng,
Rsdr: RngCore
{
/// Create a new `ReseedingCore` with the given parameters.
///
/// # Arguments
///
/// * `rng`: the random number generator to use.
/// * `threshold`: the number of generated bytes after which to reseed the RNG.
/// * `reseeder`: the RNG to use for reseeding.
pub fn new(rng: R, threshold: u64, reseeder: Rsdr) -> Self {
assert!(threshold <= ::core::i64::MAX as u64);
/// Create a new `ReseedingCore`.
fn new(rng: R, threshold: u64, reseeder: Rsdr) -> Self {
use ::core::i64::MAX;
fork::register_fork_handler();

// Because generating more values than `i64::MAX` takes centuries on
// current hardware, we just clamp to that value.
// Also we set a threshold of 0, which indicates no limit, to that
// value.
let threshold =
if threshold == 0 { MAX }
else if threshold <= MAX as u64 { threshold as i64 }
else { MAX };

ReseedingCore {
inner: rng,
reseeder,
threshold: threshold as i64,
bytes_until_reseed: threshold as i64,
fork_counter: 0,
}
}

Expand All @@ -177,26 +212,48 @@ where R: BlockRngCore + SeedableRng,
})
}

fn is_forked(&self, global_fork_counter: usize) -> bool {
// In theory, on 32-bit platforms, it is possible for
// `global_fork_counter` to wrap around after ~4e9 forks.
//
// This check will detect a fork in the normal case where
// `fork_counter < global_fork_counter`, and also when the difference
// between both is greater than `isize::MAX` (wrapped around).
//
// It will still fail to detect a fork if there have been more than
// `isize::MAX` forks, without any reseed in between. Seems unlikely
// enough.
(self.fork_counter.wrapping_sub(global_fork_counter) as isize) < 0
}

#[inline(never)]
fn reseed_and_generate(&mut self,
results: &mut <Self as BlockRngCore>::Results)
results: &mut <Self as BlockRngCore>::Results,
global_fork_counter: usize)
{
trace!("Reseeding RNG after {} generated bytes",
self.threshold - self.bytes_until_reseed);
let threshold = if let Err(e) = self.reseed() {
if self.is_forked(global_fork_counter) {
info!("Fork detected, reseeding RNG");
} else {
trace!("Reseeding RNG (periodic reseed)");
}

let num_bytes =
results.as_ref().len() * size_of::<<R as BlockRngCore>::Item>();

let threshold = if let Err(e) = self.reseed() {
let delay = match e.kind {
ErrorKind::Transient => 0,
ErrorKind::Transient => num_bytes as i64,
kind @ _ if kind.should_retry() => self.threshold >> 8,
_ => self.threshold,
};
warn!("Reseeding RNG delayed reseeding by {} bytes due to \
error from source: {}", delay, e);
error from source: {}", delay, e);
delay
} else {
self.fork_counter = global_fork_counter;
self.threshold
};

let num_bytes = results.as_ref().len() * size_of::<<R as BlockRngCore>::Item>();

self.bytes_until_reseed = threshold - num_bytes as i64;
self.inner.generate(results);
}
Expand All @@ -212,6 +269,7 @@ where R: BlockRngCore + SeedableRng + Clone,
reseeder: self.reseeder.clone(),
threshold: self.threshold,
bytes_until_reseed: 0, // reseed clone on first use
fork_counter: self.fork_counter,
}
}
}
Expand All @@ -220,6 +278,55 @@ impl<R, Rsdr> CryptoRng for ReseedingCore<R, Rsdr>
where R: BlockRngCore + SeedableRng + CryptoRng,
Rsdr: RngCore + CryptoRng {}


#[cfg(all(feature="std", unix, not(target_os="emscripten")))]
mod fork {
extern crate libc;

use std::sync::atomic::{AtomicUsize, ATOMIC_USIZE_INIT, Ordering};
use std::sync::atomic::{AtomicBool, ATOMIC_BOOL_INIT};

// Fork protection
//
// We implement fork protection on Unix using `pthread_atfork`.
// When the process is forked, we increment `RESEEDING_RNG_FORK_COUNTER`.
// Every `ReseedingRng` stores the last known value of the static in
// `fork_counter`. If the cached `fork_counter` is less than
// `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 as
// possible.

static RESEEDING_RNG_FORK_COUNTER: AtomicUsize = ATOMIC_USIZE_INIT;

pub fn get_fork_counter() -> usize {
RESEEDING_RNG_FORK_COUNTER.load(Ordering::Relaxed)
}

static FORK_HANDLER_REGISTERED: AtomicBool = ATOMIC_BOOL_INIT;

extern fn fork_handler() {
// Note: fetch_add is defined to wrap on overflow
// (which is what we want).
RESEEDING_RNG_FORK_COUNTER.fetch_add(1, Ordering::Relaxed);
}

pub fn register_fork_handler() {
if FORK_HANDLER_REGISTERED.load(Ordering::Relaxed) == false {
unsafe { libc::pthread_atfork(None, None, Some(fork_handler)) };
FORK_HANDLER_REGISTERED.store(true, Ordering::Relaxed);
}
}
}

#[cfg(not(all(feature="std", unix, not(target_os="emscripten"))))]
mod fork {
pub fn get_fork_counter() -> usize { 0 }
pub fn register_fork_handler() {}
}


#[cfg(test)]
mod test {
use {Rng, SeedableRng};
Expand Down
2 changes: 2 additions & 0 deletions src/rngs/mod.rs
Expand Up @@ -15,6 +15,7 @@
//! - [`EntropyRng`], [`OsRng`] and [`JitterRng`] as entropy sources
//! - [`mock::StepRng`] as a simple counter for tests
//! - [`adapter::ReadRng`] to read from a file/stream
//! - [`adapter::ReseedingRng`] to reseed a PRNG on clone / process fork etc.
//!
//! # Background — Random number generators (RNGs)
//!
Expand Down Expand Up @@ -161,6 +162,7 @@
//! [`thread_rng`]: ../fn.thread_rng.html
//! [`mock::StepRng`]: mock/struct.StepRng.html
//! [`adapter::ReadRng`]: adapter/struct.ReadRng.html
//! [`adapter::ReseedingRng`]: adapter/struct.ReseedingRng.html
//! [`ChaChaRng`]: ../prng/chacha/struct.ChaChaRng.html

pub mod adapter;
Expand Down

0 comments on commit bd3a806

Please sign in to comment.