test(format): keep the 2026-09-20 B-tree v2 fuzz crash as a regression

An 82-byte fuzz_btree_v2 crash input from 2026-09-20 was left untracked
in fuzz/artifacts. Replayed today it runs cleanly: the depth cap and
record budget added to B-tree v2 traversal that day fixed it. It is now
in the committed fuzz corpus, and a robustness test replays the fuzz
target's exact code path on it so a regression fails CI rather than
waiting for someone to run the fuzzer.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-09-25 20:38:34 -05:00
co-authored by Claude Opus 5.5
parent 75bdb53342
commit 46203ea761
2 changed files with 46 additions and 0 deletions
@@ -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);
}