Skip to content

Commit f91cc10

Browse files
authored
use a not-cryptographic rng for input generator (#217)
1 parent f401669 commit f91cc10

File tree

5 files changed

+124
-38
lines changed

5 files changed

+124
-38
lines changed

lib/bolero-engine/Cargo.toml

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,20 @@ readme = "../../README.md"
1212
rust-version = "1.57.0"
1313

1414
[features]
15-
rng = ["rand", "bolero-generator/alloc"]
15+
rng = ["rand", "rand_xoshiro", "bolero-generator/alloc"]
1616

1717
[dependencies]
1818
anyhow = "1"
1919
bolero-generator = { version = "0.10", path = "../bolero-generator", default-features = false }
2020
lazy_static = "1"
2121
pretty-hex = "0.3"
2222
rand = { version = "0.8", optional = true }
23+
rand_xoshiro = { version = "0.6", optional = true }
2324

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

2728
[dev-dependencies]
2829
bolero-generator = { path = "../bolero-generator", features = ["std"] }
2930
rand = "^0.8"
31+
rand_xoshiro = "0.6"

lib/bolero-engine/src/rng.rs

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
use crate::{driver, panic, ByteSliceTestInput, Engine, TargetLocation, Test};
22
use core::{fmt::Debug, time::Duration};
3-
use rand::{rngs::StdRng, Rng, RngCore, SeedableRng};
3+
use rand::{Rng, RngCore, SeedableRng};
44
use std::time::Instant;
55

6+
pub use rand_xoshiro::Xoshiro256PlusPlus as Recommended;
7+
68
#[derive(Clone, Copy, Debug)]
79
pub struct Options {
810
pub test_time: Option<Duration>,
@@ -170,7 +172,7 @@ where
170172
}
171173

172174
struct RngState {
173-
rng: StdRng,
175+
rng: Recommended,
174176
max_len: usize,
175177
options: driver::Options,
176178
buffer: Vec<u8>,
@@ -179,7 +181,7 @@ struct RngState {
179181
impl RngState {
180182
fn new(seed: u64, max_len: usize, options: driver::Options) -> Self {
181183
Self {
182-
rng: StdRng::seed_from_u64(seed),
184+
rng: SeedableRng::seed_from_u64(seed),
183185
max_len,
184186
options,
185187
buffer: vec![],

lib/bolero-generator/src/uniform.rs

+109-28
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use core::ops::{Bound, RangeBounds};
33

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

89
pub trait FillBytes {
@@ -19,8 +20,15 @@ pub trait FillBytes {
1920
}
2021

2122
macro_rules! uniform_int {
22-
($ty:ident, $unsigned:ident $(, $smaller:ident)?) => {
23+
($ty:ident, $unsigned:ident $(, $smaller:ident)*) => {
2324
impl Uniform for $ty {
25+
#[inline(always)]
26+
fn sample_unbound<F: FillBytes>(fill: &mut F) -> Option<$ty> {
27+
let mut bytes = [0u8; core::mem::size_of::<$ty>()];
28+
fill.fill_bytes(&mut bytes)?;
29+
return Some(<$ty>::from_le_bytes(bytes));
30+
}
31+
2432
#[inline]
2533
fn sample<F: FillBytes>(fill: &mut F, min: Bound<&$ty>, max: Bound<&$ty>) -> Option<$ty> {
2634
match (min, max) {
@@ -45,19 +53,11 @@ macro_rules! uniform_int {
4553
| (Bound::Unbounded, Bound::Included(&$ty::MAX))
4654
| (Bound::Included(&$ty::MIN), Bound::Unbounded)
4755
| (Bound::Included(&$ty::MIN), Bound::Included(&$ty::MAX)) => {
48-
let mut bytes = [0u8; core::mem::size_of::<$ty>()];
49-
fill.fill_bytes(&mut bytes)?;
50-
return Some(<$ty>::from_le_bytes(bytes));
56+
return Self::sample_unbound(fill);
5157
}
5258
_ => {}
5359
}
5460

55-
// if we're in direct mode, just sample a value and check if it's within the provided range
56-
if fill.mode() == DriverMode::Direct {
57-
return Self::sample(fill, Bound::Unbounded, Bound::Unbounded)
58-
.filter(|value| (min, max).contains(value));
59-
}
60-
6161
let lower = match min {
6262
Bound::Included(&v) => v,
6363
Bound::Excluded(v) => v.saturating_add(1),
@@ -90,16 +90,15 @@ macro_rules! uniform_int {
9090

9191
return Some(value);
9292
}
93-
})?
93+
})*
9494

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

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

@@ -118,23 +117,77 @@ uniform_int!(u8, u8);
118117
uniform_int!(i8, u8);
119118
uniform_int!(u16, u16, u8);
120119
uniform_int!(i16, u16, u8);
121-
uniform_int!(u32, u32, u16);
122-
uniform_int!(i32, u32, u16);
123-
uniform_int!(u64, u64, u32);
124-
uniform_int!(i64, u64, u32);
125-
uniform_int!(u128, u128, u64);
126-
uniform_int!(i128, u128, u64);
127-
uniform_int!(usize, usize, u64);
128-
uniform_int!(isize, usize, u64);
120+
uniform_int!(u32, u32, u8, u16);
121+
uniform_int!(i32, u32, u8, u16);
122+
uniform_int!(u64, u64, u8, u16, u32);
123+
uniform_int!(i64, u64, u8, u16, u32);
124+
uniform_int!(usize, usize, u8, u16, u32);
125+
uniform_int!(isize, usize, u8, u16, u32);
126+
uniform_int!(u128, u128, u8, u16, u32, u64);
127+
uniform_int!(i128, u128, u8, u16, u32, u64);
128+
129+
trait Scaled: Sized {
130+
fn scale(self, range: Self) -> Self;
131+
}
132+
133+
macro_rules! scaled {
134+
($s:ty, $upper:ty) => {
135+
impl Scaled for $s {
136+
#[inline(always)]
137+
fn scale(self, range: Self) -> Self {
138+
// similar approach to Lemire random sampling
139+
// see https://lemire.me/blog/2019/06/06/nearly-divisionless-random-integer-generation-on-various-systems/
140+
let m = self as $upper * range as $upper;
141+
(m >> Self::BITS) as Self
142+
}
143+
}
144+
};
145+
}
146+
147+
scaled!(u8, u16);
148+
scaled!(u16, u32);
149+
scaled!(u32, u64);
150+
scaled!(u64, u128);
151+
scaled!(usize, u128);
152+
153+
impl Scaled for u128 {
154+
#[inline(always)]
155+
fn scale(self, range: Self) -> Self {
156+
// adapted from mulddi3 https://github.com/llvm/llvm-project/blob/6a3982f8b7e37987659706cb3e6427c54c9bc7ce/compiler-rt/lib/builtins/multi3.c#L19
157+
const BITS_IN_DWORD_2: u32 = 64;
158+
const LOWER_MASK: u128 = u128::MAX >> BITS_IN_DWORD_2;
159+
160+
let a = self;
161+
let b = range;
162+
163+
let mut low = (a & LOWER_MASK) * (b & LOWER_MASK);
164+
let mut t = low >> BITS_IN_DWORD_2;
165+
low &= LOWER_MASK;
166+
t += (a >> BITS_IN_DWORD_2) * (b & LOWER_MASK);
167+
low += (t & LOWER_MASK) << BITS_IN_DWORD_2;
168+
let mut high = t >> BITS_IN_DWORD_2;
169+
t = low >> BITS_IN_DWORD_2;
170+
low &= LOWER_MASK;
171+
t += (b >> BITS_IN_DWORD_2) * (a & LOWER_MASK);
172+
low += (t & LOWER_MASK) << BITS_IN_DWORD_2;
173+
high += t >> BITS_IN_DWORD_2;
174+
high += (a >> BITS_IN_DWORD_2) * (b >> BITS_IN_DWORD_2);
175+
176+
// discard the low bits
177+
let _ = low;
178+
179+
high
180+
}
181+
}
129182

130183
impl Uniform for char {
184+
#[inline(always)]
185+
fn sample_unbound<F: FillBytes>(fill: &mut F) -> Option<Self> {
186+
Self::sample(fill, Bound::Unbounded, Bound::Unbounded)
187+
}
188+
131189
#[inline]
132190
fn sample<F: FillBytes>(fill: &mut F, min: Bound<&Self>, max: Bound<&Self>) -> Option<Self> {
133-
if fill.mode() == DriverMode::Direct {
134-
let value = u32::sample(fill, Bound::Unbounded, Bound::Unbounded)?;
135-
return char::from_u32(value);
136-
}
137-
138191
const START: u32 = 0xD800;
139192
const LEN: u32 = 0xE000 - START;
140193

@@ -174,6 +227,15 @@ impl Uniform for char {
174227
#[cfg(test)]
175228
mod tests {
176229
use super::*;
230+
use core::fmt;
231+
232+
#[test]
233+
fn scaled_u128_test() {
234+
assert_eq!(0u128.scale(3), 0);
235+
assert_eq!(u128::MAX.scale(3), 2);
236+
assert_eq!((u128::MAX - 1).scale(3), 2);
237+
assert_eq!((u128::MAX / 2).scale(3), 1);
238+
}
177239

178240
#[derive(Clone, Copy, Debug)]
179241
struct Byte {
@@ -210,7 +272,7 @@ mod tests {
210272
}
211273
}
212274

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

216278
impl<T: SeenValue> Default for Seen<T> {
@@ -219,18 +281,35 @@ mod tests {
219281
}
220282
}
221283

284+
impl<T: SeenValue> fmt::Debug for Seen<T> {
285+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
286+
f.debug_list()
287+
.entries(
288+
self.0
289+
.iter()
290+
.enumerate()
291+
.filter_map(|(idx, seen)| if *seen { Some(idx) } else { None }),
292+
)
293+
.finish()
294+
}
295+
}
296+
222297
impl<T: SeenValue> Seen<T> {
223298
fn insert(&mut self, v: T) {
224299
self.0[v.index()] = true;
225300
}
226301
}
227302

228303
trait SeenValue: Copy + Uniform + core::fmt::Debug {
304+
const ENTRIES: usize;
305+
229306
fn index(self) -> usize;
230307
fn fill_expected(min: Bound<Self>, max: Bound<Self>, seen: &mut Seen<Self>);
231308
}
232309

233310
impl SeenValue for u8 {
311+
const ENTRIES: usize = 256;
312+
234313
fn index(self) -> usize {
235314
self as _
236315
}
@@ -245,6 +324,8 @@ mod tests {
245324
}
246325

247326
impl SeenValue for i8 {
327+
const ENTRIES: usize = 256;
328+
248329
fn index(self) -> usize {
249330
(self as isize + -(i8::MIN as isize)).try_into().unwrap()
250331
}

lib/bolero/src/test/input.rs

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
#![cfg_attr(fuzzing_random, allow(dead_code))]
22

3-
use bolero_engine::RngInput;
3+
use bolero_engine::{rng::Recommended as Rng, RngInput};
44
use bolero_generator::{driver, TypeGenerator};
5-
use rand::{rngs::StdRng, SeedableRng};
5+
use rand::SeedableRng;
66
use std::{io::Read, path::PathBuf};
77

88
pub enum TestInput {
@@ -42,16 +42,16 @@ impl RngTest {
4242
&self,
4343
buffer: &'a mut Vec<u8>,
4444
options: &'a driver::Options,
45-
) -> RngInput<'a, StdRng> {
46-
RngInput::new(StdRng::seed_from_u64(self.seed), buffer, options)
45+
) -> RngInput<'a, Rng> {
46+
RngInput::new(Rng::seed_from_u64(self.seed), buffer, options)
4747
}
4848

4949
pub fn buffered_input<'a>(
5050
&self,
5151
buffer: &'a mut Vec<u8>,
5252
options: &'a driver::Options,
5353
) -> RngBufferedInput<'a> {
54-
let rng = StdRng::seed_from_u64(self.seed);
54+
let rng = Rng::seed_from_u64(self.seed);
5555
let driver = RngBufferedDriver { rng, buffer };
5656
let driver = driver::Rng::new(driver, options);
5757
RngBufferedInput {
@@ -62,7 +62,7 @@ impl RngTest {
6262
}
6363

6464
pub struct RngBufferedDriver<'a> {
65-
rng: StdRng,
65+
rng: Rng,
6666
buffer: &'a mut Vec<u8>,
6767
}
6868

lib/bolero/src/test/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ impl TestEngine {
8484
let iterations = self.rng_cfg.iterations_or_default();
8585
let max_len = self.rng_cfg.max_len_or_default();
8686
let seed = self.rng_cfg.seed_or_rand();
87+
// use StdRng for high entropy seeds
8788
let mut seed_rng = StdRng::seed_from_u64(seed);
8889

8990
(0..iterations)

0 commit comments

Comments
 (0)