diff --git a/crates/clawhdf5-format/fuzz/corpus/fuzz_btree_v2/regression-crash-f98c19dc b/crates/clawhdf5-format/fuzz/corpus/fuzz_btree_v2/regression-crash-f98c19dc new file mode 100644 index 0000000..afb7da7 Binary files /dev/null and b/crates/clawhdf5-format/fuzz/corpus/fuzz_btree_v2/regression-crash-f98c19dc differ diff --git a/crates/clawhdf5-format/tests/robustness_tests.rs b/crates/clawhdf5-format/tests/robustness_tests.rs index befe49e..67383c8 100644 --- a/crates/clawhdf5-format/tests/robustness_tests.rs +++ b/crates/clawhdf5-format/tests/robustness_tests.rs @@ -312,3 +312,49 @@ fn provenance_mismatch_on_corruption() { "corrupted data should produce hash mismatch" ); } + +// --------------------------------------------------------------------------- +// Fuzzer finds, kept as regression tests +// --------------------------------------------------------------------------- + +/// `fuzz_btree_v2` crash input from 2026-09-20 (82 bytes): a B-tree v2 header +/// followed by internal nodes that point back into themselves. It predates the +/// depth cap and record budget added to B-tree v2 traversal that day and no +/// longer crashes; this replays the fuzz target's exact code path on it so a +/// regression fails CI rather than waiting for a fuzz run. +#[test] +fn fuzz_btree_v2_crash_f98c19dc_is_a_clean_result() { + use clawhdf5_format::btree_v2::{BTreeV2Header, collect_btree_v2_records}; + let data: &[u8] = &[ + 0x42, 0x54, 0x48, 0x44, 0x00, 0x06, 0x00, 0xed, 0xef, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x03, 0x40, 0x14, 0x93, 0x42, 0x54, 0x49, 0x4e, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x42, 0x54, 0x48, 0x44, 0x00, 0x00, 0x00, 0x13, 0x05, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x40, 0x14, 0x93, 0x42, 0x54, 0x00, 0x49, + 0x00, 0x01, 0x4e, 0x42, 0x42, 0x54, 0xbe, + ]; + assert_eq!(data.len(), 82); + for offset_size in [4u8, 8] { + for length_size in [4u8, 8] { + if let Ok(header) = BTreeV2Header::parse(data, 0, offset_size, length_size) { + let _ = collect_btree_v2_records(data, &header, offset_size, length_size); + } + } + } + let (fields, file) = data.split_first_chunk::<20>().unwrap(); + let header = BTreeV2Header { + tree_type: fields[0], + node_size: u32::from_le_bytes([fields[1], fields[2], fields[3], fields[4]]), + record_size: u16::from_le_bytes([fields[5], fields[6]]), + depth: u16::from_le_bytes([fields[7], fields[8]]), + root_node_address: u64::from(u32::from_le_bytes([ + fields[9], fields[10], fields[11], fields[12], + ])), + num_records_in_root: u16::from_le_bytes([fields[13], fields[14]]), + total_records: u64::from(u32::from_le_bytes([ + fields[15], fields[16], fields[17], fields[18], + ])), + }; + let offset_size = if fields[19] & 1 == 0 { 4 } else { 8 }; + let _ = collect_btree_v2_records(file, &header, offset_size, 8); +}