Skip to content

Commit

Permalink
use a not-cryptographic rng for input generator (#217)
Browse files Browse the repository at this point in the history
  • Loading branch information
camshaft authored May 30, 2024
1 parent f401669 commit f91cc10
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 38 deletions.
4 changes: 3 additions & 1 deletion lib/bolero-engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,20 @@ readme = "../../README.md"
rust-version = "1.57.0"

[features]
rng = ["rand", "bolero-generator/alloc"]
rng = ["rand", "rand_xoshiro", "bolero-generator/alloc"]

[dependencies]
anyhow = "1"
bolero-generator = { version = "0.10", path = "../bolero-generator", default-features = false }
lazy_static = "1"
pretty-hex = "0.3"
rand = { version = "0.8", optional = true }
rand_xoshiro = { version = "0.6", optional = true }

[target.'cfg(not(kani))'.dependencies]
backtrace = { version = "0.3", default-features = false, features = ["std"] }

[dev-dependencies]
bolero-generator = { path = "../bolero-generator", features = ["std"] }
rand = "^0.8"
rand_xoshiro = "0.6"
8 changes: 5 additions & 3 deletions lib/bolero-engine/src/rng.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use crate::{driver, panic, ByteSliceTestInput, Engine, TargetLocation, Test};
use core::{fmt::Debug, time::Duration};
use rand::{rngs::StdRng, Rng, RngCore, SeedableRng};
use rand::{Rng, RngCore, SeedableRng};
use std::time::Instant;

pub use rand_xoshiro::Xoshiro256PlusPlus as Recommended;

#[derive(Clone, Copy, Debug)]
pub struct Options {
pub test_time: Option<Duration>,
Expand Down Expand Up @@ -170,7 +172,7 @@ where
}

struct RngState {
rng: StdRng,
rng: Recommended,
max_len: usize,
options: driver::Options,
buffer: Vec<u8>,
Expand All @@ -179,7 +181,7 @@ struct RngState {
impl RngState {
fn new(seed: u64, max_len: usize, options: driver::Options) -> Self {
Self {
rng: StdRng::seed_from_u64(seed),
rng: SeedableRng::seed_from_u64(seed),
max_len,
options,
buffer: vec![],
Expand Down
137 changes: 109 additions & 28 deletions lib/bolero-generator/src/uniform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use core::ops::{Bound, RangeBounds};

pub trait Uniform: Sized + PartialEq + Eq + PartialOrd + Ord {
fn sample<F: FillBytes>(fill: &mut F, min: Bound<&Self>, max: Bound<&Self>) -> Option<Self>;
fn sample_unbound<F: FillBytes>(fill: &mut F) -> Option<Self>;
}

pub trait FillBytes {
Expand All @@ -19,8 +20,15 @@ pub trait FillBytes {
}

macro_rules! uniform_int {
($ty:ident, $unsigned:ident $(, $smaller:ident)?) => {
($ty:ident, $unsigned:ident $(, $smaller:ident)*) => {
impl Uniform for $ty {
#[inline(always)]
fn sample_unbound<F: FillBytes>(fill: &mut F) -> Option<$ty> {
let mut bytes = [0u8; core::mem::size_of::<$ty>()];
fill.fill_bytes(&mut bytes)?;
return Some(<$ty>::from_le_bytes(bytes));
}

#[inline]
fn sample<F: FillBytes>(fill: &mut F, min: Bound<&$ty>, max: Bound<&$ty>) -> Option<$ty> {
match (min, max) {
Expand All @@ -45,19 +53,11 @@ macro_rules! uniform_int {
| (Bound::Unbounded, Bound::Included(&$ty::MAX))
| (Bound::Included(&$ty::MIN), Bound::Unbounded)
| (Bound::Included(&$ty::MIN), Bound::Included(&$ty::MAX)) => {
let mut bytes = [0u8; core::mem::size_of::<$ty>()];
fill.fill_bytes(&mut bytes)?;
return Some(<$ty>::from_le_bytes(bytes));
return Self::sample_unbound(fill);
}
_ => {}
}

// if we're in direct mode, just sample a value and check if it's within the provided range
if fill.mode() == DriverMode::Direct {
return Self::sample(fill, Bound::Unbounded, Bound::Unbounded)
.filter(|value| (min, max).contains(value));
}

let lower = match min {
Bound::Included(&v) => v,
Bound::Excluded(v) => v.saturating_add(1),
Expand Down Expand Up @@ -90,16 +90,15 @@ macro_rules! uniform_int {

return Some(value);
}
})?
})*

let value: $unsigned = Uniform::sample(fill, Bound::Unbounded, Bound::Unbounded)?;
let value: $unsigned = Uniform::sample_unbound(fill)?;

if cfg!(test) {
assert!(range_inclusive < $unsigned::MAX, "range inclusive should always be less than the max value");
}
let range_exclusive = range_inclusive.wrapping_add(1);
// TODO make this less biased
let value = value % range_exclusive;
let value = value.scale(range_exclusive);
let value = value as $ty;
let value = lower.wrapping_add(value);

Expand All @@ -118,23 +117,77 @@ uniform_int!(u8, u8);
uniform_int!(i8, u8);
uniform_int!(u16, u16, u8);
uniform_int!(i16, u16, u8);
uniform_int!(u32, u32, u16);
uniform_int!(i32, u32, u16);
uniform_int!(u64, u64, u32);
uniform_int!(i64, u64, u32);
uniform_int!(u128, u128, u64);
uniform_int!(i128, u128, u64);
uniform_int!(usize, usize, u64);
uniform_int!(isize, usize, u64);
uniform_int!(u32, u32, u8, u16);
uniform_int!(i32, u32, u8, u16);
uniform_int!(u64, u64, u8, u16, u32);
uniform_int!(i64, u64, u8, u16, u32);
uniform_int!(usize, usize, u8, u16, u32);
uniform_int!(isize, usize, u8, u16, u32);
uniform_int!(u128, u128, u8, u16, u32, u64);
uniform_int!(i128, u128, u8, u16, u32, u64);

trait Scaled: Sized {
fn scale(self, range: Self) -> Self;
}

macro_rules! scaled {
($s:ty, $upper:ty) => {
impl Scaled for $s {
#[inline(always)]
fn scale(self, range: Self) -> Self {
// similar approach to Lemire random sampling
// see https://lemire.me/blog/2019/06/06/nearly-divisionless-random-integer-generation-on-various-systems/
let m = self as $upper * range as $upper;
(m >> Self::BITS) as Self
}
}
};
}

scaled!(u8, u16);
scaled!(u16, u32);
scaled!(u32, u64);
scaled!(u64, u128);
scaled!(usize, u128);

impl Scaled for u128 {
#[inline(always)]
fn scale(self, range: Self) -> Self {
// adapted from mulddi3 https://github.com/llvm/llvm-project/blob/6a3982f8b7e37987659706cb3e6427c54c9bc7ce/compiler-rt/lib/builtins/multi3.c#L19
const BITS_IN_DWORD_2: u32 = 64;
const LOWER_MASK: u128 = u128::MAX >> BITS_IN_DWORD_2;

let a = self;
let b = range;

let mut low = (a & LOWER_MASK) * (b & LOWER_MASK);
let mut t = low >> BITS_IN_DWORD_2;
low &= LOWER_MASK;
t += (a >> BITS_IN_DWORD_2) * (b & LOWER_MASK);
low += (t & LOWER_MASK) << BITS_IN_DWORD_2;
let mut high = t >> BITS_IN_DWORD_2;
t = low >> BITS_IN_DWORD_2;
low &= LOWER_MASK;
t += (b >> BITS_IN_DWORD_2) * (a & LOWER_MASK);
low += (t & LOWER_MASK) << BITS_IN_DWORD_2;
high += t >> BITS_IN_DWORD_2;
high += (a >> BITS_IN_DWORD_2) * (b >> BITS_IN_DWORD_2);

// discard the low bits
let _ = low;

high
}
}

impl Uniform for char {
#[inline(always)]
fn sample_unbound<F: FillBytes>(fill: &mut F) -> Option<Self> {
Self::sample(fill, Bound::Unbounded, Bound::Unbounded)
}

#[inline]
fn sample<F: FillBytes>(fill: &mut F, min: Bound<&Self>, max: Bound<&Self>) -> Option<Self> {
if fill.mode() == DriverMode::Direct {
let value = u32::sample(fill, Bound::Unbounded, Bound::Unbounded)?;
return char::from_u32(value);
}

const START: u32 = 0xD800;
const LEN: u32 = 0xE000 - START;

Expand Down Expand Up @@ -174,6 +227,15 @@ impl Uniform for char {
#[cfg(test)]
mod tests {
use super::*;
use core::fmt;

#[test]
fn scaled_u128_test() {
assert_eq!(0u128.scale(3), 0);
assert_eq!(u128::MAX.scale(3), 2);
assert_eq!((u128::MAX - 1).scale(3), 2);
assert_eq!((u128::MAX / 2).scale(3), 1);
}

#[derive(Clone, Copy, Debug)]
struct Byte {
Expand Down Expand Up @@ -210,7 +272,7 @@ mod tests {
}
}

#[derive(Clone, Copy, Debug, PartialEq)]
#[derive(Clone, Copy, PartialEq)]
struct Seen<T: SeenValue>([bool; 256], core::marker::PhantomData<T>);

impl<T: SeenValue> Default for Seen<T> {
Expand All @@ -219,18 +281,35 @@ mod tests {
}
}

impl<T: SeenValue> fmt::Debug for Seen<T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_list()
.entries(
self.0
.iter()
.enumerate()
.filter_map(|(idx, seen)| if *seen { Some(idx) } else { None }),
)
.finish()
}
}

impl<T: SeenValue> Seen<T> {
fn insert(&mut self, v: T) {
self.0[v.index()] = true;
}
}

trait SeenValue: Copy + Uniform + core::fmt::Debug {
const ENTRIES: usize;

fn index(self) -> usize;
fn fill_expected(min: Bound<Self>, max: Bound<Self>, seen: &mut Seen<Self>);
}

impl SeenValue for u8 {
const ENTRIES: usize = 256;

fn index(self) -> usize {
self as _
}
Expand All @@ -245,6 +324,8 @@ mod tests {
}

impl SeenValue for i8 {
const ENTRIES: usize = 256;

fn index(self) -> usize {
(self as isize + -(i8::MIN as isize)).try_into().unwrap()
}
Expand Down
12 changes: 6 additions & 6 deletions lib/bolero/src/test/input.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#![cfg_attr(fuzzing_random, allow(dead_code))]

use bolero_engine::RngInput;
use bolero_engine::{rng::Recommended as Rng, RngInput};
use bolero_generator::{driver, TypeGenerator};
use rand::{rngs::StdRng, SeedableRng};
use rand::SeedableRng;
use std::{io::Read, path::PathBuf};

pub enum TestInput {
Expand Down Expand Up @@ -42,16 +42,16 @@ impl RngTest {
&self,
buffer: &'a mut Vec<u8>,
options: &'a driver::Options,
) -> RngInput<'a, StdRng> {
RngInput::new(StdRng::seed_from_u64(self.seed), buffer, options)
) -> RngInput<'a, Rng> {
RngInput::new(Rng::seed_from_u64(self.seed), buffer, options)
}

pub fn buffered_input<'a>(
&self,
buffer: &'a mut Vec<u8>,
options: &'a driver::Options,
) -> RngBufferedInput<'a> {
let rng = StdRng::seed_from_u64(self.seed);
let rng = Rng::seed_from_u64(self.seed);
let driver = RngBufferedDriver { rng, buffer };
let driver = driver::Rng::new(driver, options);
RngBufferedInput {
Expand All @@ -62,7 +62,7 @@ impl RngTest {
}

pub struct RngBufferedDriver<'a> {
rng: StdRng,
rng: Rng,
buffer: &'a mut Vec<u8>,
}

Expand Down
1 change: 1 addition & 0 deletions lib/bolero/src/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ impl TestEngine {
let iterations = self.rng_cfg.iterations_or_default();
let max_len = self.rng_cfg.max_len_or_default();
let seed = self.rng_cfg.seed_or_rand();
// use StdRng for high entropy seeds
let mut seed_rng = StdRng::seed_from_u64(seed);

(0..iterations)
Expand Down

0 comments on commit f91cc10

Please sign in to comment.