Skip to content

Commit 0553e1e

Browse files
committed
Completely overhaul fuzz testing
adds testing for almost every numerical intrinsic
1 parent 430c0b4 commit 0553e1e

File tree

11 files changed

+932
-129
lines changed

11 files changed

+932
-129
lines changed

src/int/mod.rs

+11
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ pub trait Int:
7272
/// Prevents the need for excessive conversions between signed and unsigned
7373
fn logical_shr(self, other: u32) -> Self;
7474

75+
/// Absolute difference between two integers.
76+
fn abs_diff(self, other: Self) -> Self::UnsignedInt;
77+
7578
// copied from primitive integers, but put in a trait
7679
fn is_zero(self) -> bool;
7780
fn max_value() -> Self;
@@ -251,6 +254,10 @@ macro_rules! int_impl {
251254
me
252255
}
253256

257+
fn abs_diff(self, other: Self) -> Self {
258+
(self.wrapping_sub(other) as $ity).wrapping_abs() as $uty
259+
}
260+
254261
int_impl_common!($uty, $bits);
255262
}
256263

@@ -274,6 +281,10 @@ macro_rules! int_impl {
274281
me as $ity
275282
}
276283

284+
fn abs_diff(self, other: Self) -> $uty {
285+
self.wrapping_sub(other).wrapping_abs() as $uty
286+
}
287+
277288
int_impl_common!($ity, $bits);
278289
}
279290
};

testcrate/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ doctest = false
1111
[build-dependencies]
1212
rand = "0.7"
1313

14-
[dev-dependencies]
14+
[dependencies]
1515
# For fuzzing tests we want a deterministic seedable RNG. We also eliminate potential
1616
# problems with system RNGs on the variety of platforms this crate is tested on.
1717
# `xoshiro128**` is used for its quality, size, and speed at generating `u32` shift amounts.

testcrate/src/lib.rs

+257
Original file line numberDiff line numberDiff line change
@@ -1 +1,258 @@
1+
//! This crate is for integration testing and fuzz testing of functions in `compiler-builtins`. This
2+
//! includes publicly documented intrinsics and some internal alternative implementation functions
3+
//! such as `usize_leading_zeros_riscv` (which are tested because they are configured for
4+
//! architectures not tested by the CI).
5+
//!
6+
//! The general idea is to use a combination of edge case testing and randomized fuzz testing. The
7+
//! edge case testing is crucial for checking cases like where both inputs are equal or equal to
8+
//! special values such as `i128::MIN`, which is unlikely for the random fuzzer by itself to
9+
//! encounter. The randomized fuzz testing is specially designed to cover wide swaths of search
10+
//! space in as few iterations as possible. See `fuzz_values` in `testcrate/tests/misc.rs` for an
11+
//! example.
12+
//!
13+
//! Some floating point tests are disabled on x86 architectures without SSE, because they do not
14+
//! have correct rounding.
115
#![no_std]
16+
17+
use compiler_builtins::float::Float;
18+
use compiler_builtins::int::Int;
19+
20+
use rand_xoshiro::rand_core::{RngCore, SeedableRng};
21+
use rand_xoshiro::Xoshiro128StarStar;
22+
23+
/// Sets the number of fuzz iterations run for most tests. In practice, the vast majority of bugs
24+
/// are caught by the edge case testers. Most of the remaining bugs triggered by more complex
25+
/// sequences are caught well within 10_000 fuzz iterations. For classes of algorithms like division
26+
/// that are vulnerable to rare edge cases, we want 1_000_000 iterations to be more confident. In
27+
/// practical CI, however, we only want to run the more strenuous test once to catch algorithmic
28+
/// level bugs, and run the 10_000 iteration test on most targets. Target-dependent bugs are likely
29+
/// to involve miscompilation and misconfiguration that is likely to break algorithms in quickly
30+
/// caught ways. We choose to configure `N = 1_000_000` iterations for `x86_64` targets which are
31+
/// likely to have fast hardware, and run `N = 10_000` for all other targets.
32+
pub const N: u32 = if cfg!(target_arch = "x86_64") {
33+
1_000_000
34+
} else {
35+
10_000
36+
};
37+
38+
/// Random fuzzing step. When run several times, it results in excellent fuzzing entropy such as:
39+
/// 11110101010101011110111110011111
40+
/// 10110101010100001011101011001010
41+
/// 1000000000000000
42+
/// 10000000000000110111110000001010
43+
/// 1111011111111101010101111110101
44+
/// 101111111110100000000101000000
45+
/// 10000000110100000000100010101
46+
/// 1010101010101000
47+
fn fuzz_step<I: Int>(rng: &mut Xoshiro128StarStar, x: &mut I) {
48+
let ones = !I::ZERO;
49+
let bit_indexing_mask: u32 = I::BITS - 1;
50+
// It happens that all the RNG we need can come from one call. 7 bits are needed to index a
51+
// worst case 128 bit integer, and there are 4 indexes that need to be made plus 4 bits for
52+
// selecting operations
53+
let rng32 = rng.next_u32();
54+
55+
// Randomly OR, AND, and XOR randomly sized and shifted continuous strings of
56+
// ones with `lhs` and `rhs`.
57+
let r0 = bit_indexing_mask & rng32;
58+
let r1 = bit_indexing_mask & (rng32 >> 7);
59+
let mask = ones.wrapping_shl(r0).rotate_left(r1);
60+
match (rng32 >> 14) % 4 {
61+
0 => *x |= mask,
62+
1 => *x &= mask,
63+
// both 2 and 3 to make XORs as common as ORs and ANDs combined
64+
_ => *x ^= mask,
65+
}
66+
67+
// Alternating ones and zeros (e.x. 0b1010101010101010). This catches second-order
68+
// problems that might occur for algorithms with two modes of operation (potentially
69+
// there is some invariant that can be broken and maintained via alternating between modes,
70+
// breaking the algorithm when it reaches the end).
71+
let mut alt_ones = I::ONE;
72+
for _ in 0..(I::BITS / 2) {
73+
alt_ones <<= 2;
74+
alt_ones |= I::ONE;
75+
}
76+
let r0 = bit_indexing_mask & (rng32 >> 16);
77+
let r1 = bit_indexing_mask & (rng32 >> 23);
78+
let mask = alt_ones.wrapping_shl(r0).rotate_left(r1);
79+
match rng32 >> 30 {
80+
0 => *x |= mask,
81+
1 => *x &= mask,
82+
_ => *x ^= mask,
83+
}
84+
}
85+
86+
// We need macros like this, because `#![no_std]` prevents us from using iterators
87+
macro_rules! edge_cases {
88+
($I:ident, $case:ident, $inner:block) => {
89+
for i0 in 0..$I::FUZZ_NUM {
90+
let mask_lo = (!$I::UnsignedInt::ZERO).wrapping_shr($I::FUZZ_LENGTHS[i0] as u32);
91+
for i1 in i0..I::FUZZ_NUM {
92+
let mask_hi =
93+
(!$I::UnsignedInt::ZERO).wrapping_shl($I::FUZZ_LENGTHS[i1 - i0] as u32);
94+
let $case = I::from_unsigned(mask_lo & mask_hi);
95+
$inner
96+
}
97+
}
98+
};
99+
}
100+
101+
/// Feeds a series of fuzzing inputs to `f`. The fuzzer first uses an algorithm designed to find
102+
/// edge cases, followed by a more random fuzzer that runs `n` times.
103+
pub fn fuzz<I: Int, F: FnMut(I)>(n: u32, mut f: F) {
104+
// edge case tester. Calls `f` 210 times for u128.
105+
// zero gets skipped by the loop
106+
f(I::ZERO);
107+
edge_cases!(I, case, {
108+
f(case);
109+
});
110+
111+
// random fuzzer
112+
let mut rng = Xoshiro128StarStar::seed_from_u64(0);
113+
let mut x: I = Int::ZERO;
114+
for _ in 0..n {
115+
fuzz_step(&mut rng, &mut x);
116+
f(x)
117+
}
118+
}
119+
120+
/// The same as `fuzz`, except `f` has two inputs.
121+
pub fn fuzz_2<I: Int, F: Fn(I, I)>(n: u32, f: F) {
122+
// Check cases where the first and second inputs are zero. Both call `f` 210 times for `u128`.
123+
edge_cases!(I, case, {
124+
f(I::ZERO, case);
125+
});
126+
edge_cases!(I, case, {
127+
f(case, I::ZERO);
128+
});
129+
// Nested edge tester. Calls `f` 44100 times for `u128`.
130+
edge_cases!(I, case0, {
131+
edge_cases!(I, case1, {
132+
f(case0, case1);
133+
})
134+
});
135+
136+
// random fuzzer
137+
let mut rng = Xoshiro128StarStar::seed_from_u64(0);
138+
let mut x: I = I::ZERO;
139+
let mut y: I = I::ZERO;
140+
for _ in 0..n {
141+
fuzz_step(&mut rng, &mut x);
142+
fuzz_step(&mut rng, &mut y);
143+
f(x, y)
144+
}
145+
}
146+
147+
/// Tester for shift functions
148+
pub fn fuzz_shift<I: Int, F: Fn(I, u32)>(f: F) {
149+
// Shift functions are very simple and do not need anything other than shifting a small
150+
// set of random patterns for every fuzz length.
151+
let mut rng = Xoshiro128StarStar::seed_from_u64(0);
152+
let mut x: I = Int::ZERO;
153+
for i in 0..I::FUZZ_NUM {
154+
fuzz_step(&mut rng, &mut x);
155+
f(x, Int::ZERO);
156+
f(x, I::FUZZ_LENGTHS[i] as u32);
157+
}
158+
}
159+
160+
fn fuzz_float_step<F: Float>(rng: &mut Xoshiro128StarStar, f: &mut F) {
161+
let rng32 = rng.next_u32();
162+
// we need to fuzz the different parts of the float separately, because the masking on larger
163+
// significands will tend to set the exponent to all ones or all zeros frequently
164+
165+
// sign bit fuzzing
166+
let sign = (rng32 & 1) != 0;
167+
168+
// exponent fuzzing. Only 4 bits for the selector needed.
169+
let ones = (F::Int::ONE << F::EXPONENT_BITS) - F::Int::ONE;
170+
let r0 = (rng32 >> 1) % F::EXPONENT_BITS;
171+
let r1 = (rng32 >> 5) % F::EXPONENT_BITS;
172+
// custom rotate shift. Note that `F::Int` is unsigned, so we can shift right without smearing
173+
// the sign bit.
174+
let mask = if r1 == 0 {
175+
ones.wrapping_shr(r0)
176+
} else {
177+
let tmp = ones.wrapping_shr(r0);
178+
(tmp.wrapping_shl(r1) | tmp.wrapping_shr(F::EXPONENT_BITS - r1)) & ones
179+
};
180+
let mut exp = (f.repr() & F::EXPONENT_MASK) >> F::SIGNIFICAND_BITS;
181+
match (rng32 >> 9) % 4 {
182+
0 => exp |= mask,
183+
1 => exp &= mask,
184+
_ => exp ^= mask,
185+
}
186+
187+
// significand fuzzing
188+
let mut sig = f.repr() & F::SIGNIFICAND_MASK;
189+
fuzz_step(rng, &mut sig);
190+
sig &= F::SIGNIFICAND_MASK;
191+
192+
*f = F::from_parts(sign, exp, sig);
193+
}
194+
195+
macro_rules! float_edge_cases {
196+
($F:ident, $case:ident, $inner:block) => {
197+
for exponent in [
198+
F::Int::ZERO,
199+
F::Int::ONE,
200+
F::Int::ONE << (F::EXPONENT_BITS / 2),
201+
(F::Int::ONE << (F::EXPONENT_BITS - 1)) - F::Int::ONE,
202+
F::Int::ONE << (F::EXPONENT_BITS - 1),
203+
(F::Int::ONE << (F::EXPONENT_BITS - 1)) + F::Int::ONE,
204+
(F::Int::ONE << F::EXPONENT_BITS) - F::Int::ONE,
205+
]
206+
.iter()
207+
{
208+
for significand in [
209+
F::Int::ZERO,
210+
F::Int::ONE,
211+
F::Int::ONE << (F::SIGNIFICAND_BITS / 2),
212+
(F::Int::ONE << (F::SIGNIFICAND_BITS - 1)) - F::Int::ONE,
213+
F::Int::ONE << (F::SIGNIFICAND_BITS - 1),
214+
(F::Int::ONE << (F::SIGNIFICAND_BITS - 1)) + F::Int::ONE,
215+
(F::Int::ONE << F::SIGNIFICAND_BITS) - F::Int::ONE,
216+
]
217+
.iter()
218+
{
219+
for sign in [false, true].iter() {
220+
let $case = F::from_parts(*sign, *exponent, *significand);
221+
$inner
222+
}
223+
}
224+
}
225+
};
226+
}
227+
228+
pub fn fuzz_float<F: Float, E: Fn(F)>(n: u32, f: E) {
229+
float_edge_cases!(F, case, {
230+
f(case);
231+
});
232+
233+
// random fuzzer
234+
let mut rng = Xoshiro128StarStar::seed_from_u64(0);
235+
let mut x = F::ZERO;
236+
for _ in 0..n {
237+
fuzz_float_step(&mut rng, &mut x);
238+
f(x);
239+
}
240+
}
241+
242+
pub fn fuzz_float_2<F: Float, E: Fn(F, F)>(n: u32, f: E) {
243+
float_edge_cases!(F, case0, {
244+
float_edge_cases!(F, case1, {
245+
f(case0, case1);
246+
});
247+
});
248+
249+
// random fuzzer
250+
let mut rng = Xoshiro128StarStar::seed_from_u64(0);
251+
let mut x = F::ZERO;
252+
let mut y = F::ZERO;
253+
for _ in 0..n {
254+
fuzz_float_step(&mut rng, &mut x);
255+
fuzz_float_step(&mut rng, &mut y);
256+
f(x, y)
257+
}
258+
}

0 commit comments

Comments
 (0)