Audited LZF, bitshuffle, bzip2 and Blosc/BloscLZ for arithmetic on header fields and unchecked slicing. The only live bug was the Blosc frame-size underflow fixed in the previous commit; bzip2's output-growth step now uses a saturating subtraction as well (the allocator may hand back more capacity than asked for). src/test_fuzz.rs (tests only) feeds each decoder random bytes, truncated seeds and one-to-four-edit mutations of valid frames, biased towards edge-case u32 values in size and offset fields, and asserts no panic and no output over the limit (tests build with overflow checks and debug assertions). Per decoder: LZF, bzip2, bitshuffle in all six mode/block settings plus hostile cd_values, Blosc across four codecs, three shuffles, stored frames and a hand-built BloscLZ frame, and BloscLZ streams alone. With the previous commit's check removed, fuzzed_frames_never_panic panics at the same subtraction. A 100x-iteration soak (different seed) found no other panic. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
150 lines
4.7 KiB
Rust
150 lines
4.7 KiB
Rust
//! Mutation fuzzing for the filter decoders (tests only).
|
|
//!
|
|
//! A decoder fed a random or mutated frame may fail, but must not panic —
|
|
//! tests build with overflow checks and debug assertions, so an unchecked
|
|
//! subtraction, multiplication or shift on a header field, or an
|
|
//! out-of-range slice, fails the test — and must not return more than its
|
|
//! output limit.
|
|
|
|
#[cfg(not(feature = "std"))]
|
|
extern crate alloc;
|
|
#[cfg(not(feature = "std"))]
|
|
use alloc::vec::Vec;
|
|
|
|
use crate::error::FormatError;
|
|
|
|
/// xorshift64*: deterministic, so a failure reproduces.
|
|
pub(crate) struct Rng(u64);
|
|
|
|
impl Rng {
|
|
pub(crate) fn new(seed: u64) -> Rng {
|
|
Rng(seed.max(1))
|
|
}
|
|
|
|
pub(crate) fn next_u64(&mut self) -> u64 {
|
|
let mut x = self.0;
|
|
x ^= x >> 12;
|
|
x ^= x << 25;
|
|
x ^= x >> 27;
|
|
self.0 = x;
|
|
x.wrapping_mul(0x2545_F491_4F6C_DD1D)
|
|
}
|
|
|
|
/// Uniform in `0..n` (`n` > 0).
|
|
pub(crate) fn below(&mut self, n: usize) -> usize {
|
|
(self.next_u64() % n as u64) as usize
|
|
}
|
|
|
|
pub(crate) fn bytes(&mut self, n: usize) -> Vec<u8> {
|
|
(0..n).map(|_| self.next_u64() as u8).collect()
|
|
}
|
|
|
|
/// A u32 that tends to hit edge cases in size and offset fields.
|
|
fn interesting_u32(&mut self, len: usize) -> u32 {
|
|
match self.below(10) {
|
|
0 => 0,
|
|
1 => 1,
|
|
2 => self.below(20) as u32,
|
|
3 => 15 + self.below(3) as u32,
|
|
4 => u32::MAX - self.below(16) as u32,
|
|
5 => 1 << self.below(32),
|
|
6 => (len as u32)
|
|
.wrapping_add(self.below(9) as u32)
|
|
.wrapping_sub(4),
|
|
7 => i32::MAX as u32,
|
|
_ => self.next_u64() as u32,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// One to four random edits of `seed`.
|
|
pub(crate) fn mutate(rng: &mut Rng, seed: &[u8]) -> Vec<u8> {
|
|
let mut v = seed.to_vec();
|
|
for _ in 0..1 + rng.below(4) {
|
|
let len = v.len();
|
|
match rng.below(9) {
|
|
0 if len > 0 => {
|
|
let i = rng.below(len);
|
|
v[i] ^= 1 << rng.below(8);
|
|
}
|
|
1 if len > 0 => {
|
|
let i = rng.below(len);
|
|
v[i] = rng.next_u64() as u8;
|
|
}
|
|
2 if len > 0 => {
|
|
let i = rng.below(len);
|
|
v[i] = [0, 0xff, 0x7f, 0x80, 0x20, 0x1f][rng.below(6)];
|
|
}
|
|
// A size or offset field: little- or big-endian, anywhere, but
|
|
// most often in the first 32 bytes where headers live.
|
|
3 | 4 if len >= 4 => {
|
|
let span = if rng.below(2) == 0 { len.min(32) } else { len };
|
|
let i = rng.below(span - 3);
|
|
let x = rng.interesting_u32(len);
|
|
let b = if rng.below(2) == 0 {
|
|
x.to_le_bytes()
|
|
} else {
|
|
x.to_be_bytes()
|
|
};
|
|
v[i..i + 4].copy_from_slice(&b);
|
|
}
|
|
5 if len > 0 => v.truncate(rng.below(len)),
|
|
6 => {
|
|
let n = 1 + rng.below(64);
|
|
let extra = rng.bytes(n);
|
|
v.extend_from_slice(&extra);
|
|
}
|
|
7 if len > 1 => {
|
|
let a = rng.below(len);
|
|
let b = a + rng.below(len - a);
|
|
let copy = v[a..b].to_vec();
|
|
let at = rng.below(len);
|
|
v.splice(at..at, copy);
|
|
}
|
|
_ if len > 0 => {
|
|
let i = rng.below(len);
|
|
v[i] = v[i].wrapping_add(1 + rng.below(3) as u8);
|
|
}
|
|
_ => v.push(rng.next_u64() as u8),
|
|
}
|
|
}
|
|
v
|
|
}
|
|
|
|
/// Feed `iters` inputs to `decode`: mostly mutations of `seeds`, some pure
|
|
/// noise and some truncated seeds. Asserts only "no panic, output within
|
|
/// `limit`".
|
|
pub(crate) fn fuzz_decoder(
|
|
seed: u64,
|
|
seeds: &[Vec<u8>],
|
|
iters: usize,
|
|
limit: usize,
|
|
mut decode: impl FnMut(&[u8]) -> Result<Vec<u8>, FormatError>,
|
|
) {
|
|
assert!(!seeds.is_empty());
|
|
let mut rng = Rng::new(seed);
|
|
for s in seeds {
|
|
// The seeds themselves must be valid, or the fuzz explores nothing.
|
|
decode(s).expect("seed frame must decode");
|
|
}
|
|
for _ in 0..iters {
|
|
let input = match rng.below(16) {
|
|
0 => {
|
|
let n = rng.below(96);
|
|
rng.bytes(n)
|
|
}
|
|
1 => {
|
|
let s = &seeds[rng.below(seeds.len())];
|
|
s[..rng.below(s.len() + 1)].to_vec()
|
|
}
|
|
_ => {
|
|
let s = &seeds[rng.below(seeds.len())];
|
|
mutate(&mut rng, s)
|
|
}
|
|
};
|
|
if let Ok(out) = decode(&input) {
|
|
assert!(out.len() <= limit, "decoded {} > limit {limit}", out.len());
|
|
}
|
|
}
|
|
}
|