test(format): fuzz every plugin-filter decoder for panics
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]>
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
//! 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user