h5rs tools, browser reader, libhdf5 header checks, plugin filters, concurrency benchmark #14
@@ -374,4 +374,65 @@ mod tests {
|
|||||||
ctx.max_output = 100;
|
ctx.max_output = 100;
|
||||||
assert!(bitshuffle_decode(&enc, &ctx).is_err());
|
assert!(bitshuffle_decode(&enc, &ctx).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Random and mutated chunks, in every mode, and hostile `cd_values`:
|
||||||
|
/// errors are fine, panics are not.
|
||||||
|
#[cfg(feature = "bitshuffle")]
|
||||||
|
#[test]
|
||||||
|
fn fuzzed_chunks_never_panic() {
|
||||||
|
use crate::test_fuzz::{Rng, fuzz_decoder};
|
||||||
|
let data: Vec<u8> = (0..3001u32)
|
||||||
|
.flat_map(|i| ((i / 7) as u16).to_le_bytes())
|
||||||
|
.collect();
|
||||||
|
for (comp, block) in [(0, 0), (0, 16), (2, 0), (2, 64), (3, 0), (3, 1024)] {
|
||||||
|
let f = ctx_for(vec![0, 4, 2, block, comp]);
|
||||||
|
let ctx = FilterContext {
|
||||||
|
filter: &f,
|
||||||
|
element_size: 2,
|
||||||
|
max_output: data.len(),
|
||||||
|
};
|
||||||
|
let seeds = vec![
|
||||||
|
bitshuffle_encode(&data, &ctx).unwrap(),
|
||||||
|
bitshuffle_encode(&data[..34], &ctx).unwrap(),
|
||||||
|
bitshuffle_encode(&data[..512], &ctx).unwrap(),
|
||||||
|
];
|
||||||
|
fuzz_decoder(
|
||||||
|
0xb5 + comp as u64 * 7 + block as u64,
|
||||||
|
&seeds,
|
||||||
|
4_000,
|
||||||
|
data.len(),
|
||||||
|
|s| bitshuffle_decode(s, &ctx),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Hostile filter parameters on a valid chunk.
|
||||||
|
let mut rng = Rng::new(0xcd);
|
||||||
|
let good = ctx_for(vec![0, 4, 2, 0, 2]);
|
||||||
|
let enc = bitshuffle_encode(
|
||||||
|
&data,
|
||||||
|
&FilterContext {
|
||||||
|
filter: &good,
|
||||||
|
element_size: 2,
|
||||||
|
max_output: data.len(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
for _ in 0..3_000 {
|
||||||
|
let cd: Vec<u32> = (0..rng.below(7))
|
||||||
|
.map(|_| match rng.below(4) {
|
||||||
|
0 => rng.below(5) as u32,
|
||||||
|
1 => u32::MAX - rng.below(4) as u32,
|
||||||
|
2 => 1 << rng.below(32),
|
||||||
|
_ => rng.next_u64() as u32,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let f = ctx_for(cd);
|
||||||
|
let ctx = FilterContext {
|
||||||
|
filter: &f,
|
||||||
|
element_size: 2,
|
||||||
|
max_output: data.len(),
|
||||||
|
};
|
||||||
|
let _ = bitshuffle_decode(&enc, &ctx);
|
||||||
|
let _ = bitshuffle_decode(&data, &ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -622,4 +622,66 @@ mod tests {
|
|||||||
assert!(blosc_decompress(&frame, 1000).is_err(), "cbytes={cbytes}");
|
assert!(blosc_decompress(&frame, 1000).is_err(), "cbytes={cbytes}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A BloscLZ frame (our encoder cannot write one): a single block,
|
||||||
|
/// one stream, no shuffle.
|
||||||
|
fn blosclz_frame() -> Vec<u8> {
|
||||||
|
let stream = [2, b'a', b'b', b'c', (6 << 5), 2, 0, b'Z'];
|
||||||
|
let mut f = vec![2u8, 1, 0, 1];
|
||||||
|
for v in [12u32, 12, (HEADER + 4 + 4 + stream.len()) as u32] {
|
||||||
|
f.extend_from_slice(&v.to_le_bytes());
|
||||||
|
}
|
||||||
|
f.extend_from_slice(&((HEADER + 4) as u32).to_le_bytes());
|
||||||
|
f.extend_from_slice(&(stream.len() as u32).to_le_bytes());
|
||||||
|
f.extend_from_slice(&stream);
|
||||||
|
f
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Random and mutated frames, every codec and shuffle: errors are fine,
|
||||||
|
/// panics are not.
|
||||||
|
#[test]
|
||||||
|
fn fuzzed_frames_never_panic() {
|
||||||
|
let limit = 6000;
|
||||||
|
let data: Vec<u8> = (0..1500u32).flat_map(|i| (i / 5).to_le_bytes()).collect();
|
||||||
|
let mut seeds = vec![blosclz_frame()];
|
||||||
|
for codec in [1u32, 3, 4, 5] {
|
||||||
|
for shuffle in [0u32, 1, 2] {
|
||||||
|
for (ts, n) in [(4usize, data.len()), (4, 520), (1, 300), (2, 4)] {
|
||||||
|
let f = desc(vec![2, 2, ts as u32, 0, 5, shuffle, codec]);
|
||||||
|
let ctx = FilterContext {
|
||||||
|
filter: &f,
|
||||||
|
element_size: ts,
|
||||||
|
max_output: n,
|
||||||
|
};
|
||||||
|
seeds.push(blosc_encode(&data[..n], &ctx).unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Stored raw.
|
||||||
|
let f = desc(vec![2, 2, 4, 0, 0, 1, 1]);
|
||||||
|
let ctx = FilterContext {
|
||||||
|
filter: &f,
|
||||||
|
element_size: 4,
|
||||||
|
max_output: 64,
|
||||||
|
};
|
||||||
|
seeds.push(blosc_encode(&data[..64], &ctx).unwrap());
|
||||||
|
crate::test_fuzz::fuzz_decoder(0xb10, &seeds, 30_000, limit, |s| {
|
||||||
|
blosc_decompress(s, limit)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// BloscLZ streams on their own, random and mutated.
|
||||||
|
#[test]
|
||||||
|
fn fuzzed_blosclz_streams_never_panic() {
|
||||||
|
let seed = blosclz_frame()[HEADER + 8..].to_vec();
|
||||||
|
let mut out = [0u8; 64];
|
||||||
|
crate::test_fuzz::fuzz_decoder(0xb11, &[seed], 30_000, 64, |s| {
|
||||||
|
let n = blosclz_decompress(s, &mut out);
|
||||||
|
if n == 0 {
|
||||||
|
Err(err("malformed"))
|
||||||
|
} else {
|
||||||
|
Ok(out[..n].to_vec())
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,10 @@ pub(crate) fn bzip2_decode(input: &[u8], ctx: &FilterContext<'_>) -> Result<Vec<
|
|||||||
return Ok(out);
|
return Ok(out);
|
||||||
}
|
}
|
||||||
if out.len() == out.capacity() {
|
if out.len() == out.capacity() {
|
||||||
let grow = out.capacity().min(max_capacity - out.capacity()).max(1);
|
let grow = out
|
||||||
|
.capacity()
|
||||||
|
.min(max_capacity.saturating_sub(out.capacity()))
|
||||||
|
.max(1);
|
||||||
out.try_reserve_exact(grow)
|
out.try_reserve_exact(grow)
|
||||||
.map_err(|_| err("cannot allocate the output buffer"))?;
|
.map_err(|_| err("cannot allocate the output buffer"))?;
|
||||||
} else if dec.total_in() as usize >= input.len()
|
} else if dec.total_in() as usize >= input.len()
|
||||||
@@ -109,4 +112,21 @@ mod tests {
|
|||||||
assert!(bzip2_decode(&enc, &small).is_err());
|
assert!(bzip2_decode(&enc, &small).is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Random and mutated streams: errors are fine, panics are not.
|
||||||
|
#[test]
|
||||||
|
fn fuzzed_streams_never_panic() {
|
||||||
|
let f = desc(9);
|
||||||
|
let data: Vec<u8> = (0..4000u32).flat_map(|i| (i % 91).to_le_bytes()).collect();
|
||||||
|
let ctx = FilterContext {
|
||||||
|
filter: &f,
|
||||||
|
element_size: 4,
|
||||||
|
max_output: data.len(),
|
||||||
|
};
|
||||||
|
let seeds = vec![
|
||||||
|
bzip2_encode(&data, &ctx).unwrap(),
|
||||||
|
bzip2_encode(&data[..40], &ctx).unwrap(),
|
||||||
|
];
|
||||||
|
crate::test_fuzz::fuzz_decoder(0xb2, &seeds, 3_000, data.len(), |s| bzip2_decode(s, &ctx));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -229,4 +229,32 @@ mod tests {
|
|||||||
let c = lzf_compress(&[1u8; 100]);
|
let c = lzf_compress(&[1u8; 100]);
|
||||||
assert!(lzf_decompress(&c, 10, 99).is_err());
|
assert!(lzf_decompress(&c, 10, 99).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Random and mutated streams: errors are fine, panics are not.
|
||||||
|
#[test]
|
||||||
|
fn fuzzed_streams_never_panic() {
|
||||||
|
let seeds: Vec<Vec<u8>> = [
|
||||||
|
b"hello hello hello hello".to_vec(),
|
||||||
|
vec![7u8; 3000],
|
||||||
|
(0..2000u32).flat_map(|i| (i % 37).to_le_bytes()).collect(),
|
||||||
|
(0..500u32)
|
||||||
|
.map(|i| (i.wrapping_mul(2_654_435_761) >> 13) as u8)
|
||||||
|
.collect(),
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.map(|d| lzf_compress(d))
|
||||||
|
.collect();
|
||||||
|
for limit in [0usize, 23, 4096, 8000] {
|
||||||
|
crate::test_fuzz::fuzz_decoder(
|
||||||
|
0x1f2 + limit as u64,
|
||||||
|
&seeds[..1],
|
||||||
|
5_000,
|
||||||
|
limit.max(23),
|
||||||
|
|s| lzf_decompress(s, limit, limit.max(23)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
crate::test_fuzz::fuzz_decoder(0x1f3, &seeds, 20_000, 8000, |s| {
|
||||||
|
lzf_decompress(s, 8000, 8000)
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,6 +117,16 @@ pub mod shared_message;
|
|||||||
pub mod signature;
|
pub mod signature;
|
||||||
pub mod superblock;
|
pub mod superblock;
|
||||||
pub mod symbol_table;
|
pub mod symbol_table;
|
||||||
|
#[cfg(all(
|
||||||
|
test,
|
||||||
|
any(
|
||||||
|
feature = "lzf",
|
||||||
|
feature = "bitshuffle",
|
||||||
|
feature = "bzip2",
|
||||||
|
feature = "blosc"
|
||||||
|
)
|
||||||
|
))]
|
||||||
|
mod test_fuzz;
|
||||||
pub mod type_builders;
|
pub mod type_builders;
|
||||||
pub mod vds;
|
pub mod vds;
|
||||||
pub mod vl_data;
|
pub mod vl_data;
|
||||||
|
|||||||
@@ -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