test(fuzz): fuzz B-tree v2 traversal, not just header parsing

The existing target only called `BTreeV2Header::parse`, so the
recursive walk behind it — where a node that is its own child overflowed
the stack — was never fuzzed at all. Parsing also requires a valid
Jenkins checksum, which random input essentially never produces, so
almost every input stopped at the first branch.

The target now walks the tree after a successful parse, and also builds
a header straight from the input bytes so the traversal is reachable
without forging a checksum.

Checked both ways: against the unfixed traversal libFuzzer finds the
stack overflow (ASan: stack-overflow), and against the fix that same
input executes in 0 ms and 34.7 million further runs produce no crash,
timeout or OOM.

Corpora and crash artifacts stay out of the repository; the two crafted
inputs are covered by unit tests instead.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-09-20 17:03:21 -07:00
co-authored by Claude Opus 5
parent e9aeb110b7
commit 0901fb1499
2 changed files with 30 additions and 6 deletions
+3
View File
@@ -1 +1,4 @@
target/ target/
corpus/
artifacts/
coverage/
@@ -1,15 +1,36 @@
#![no_main] #![no_main]
use clawhdf5_format::btree_v2::{BTreeV2Header, collect_btree_v2_records};
use libfuzzer_sys::fuzz_target; use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| { fuzz_target!(|data: &[u8]| {
for &offset_size in &[4u8, 8] { for &offset_size in &[4u8, 8] {
for &length_size in &[4u8, 8] { for &length_size in &[4u8, 8] {
let _ = clawhdf5_format::btree_v2::BTreeV2Header::parse( if let Ok(header) = BTreeV2Header::parse(data, 0, offset_size, length_size) {
data, let _ = collect_btree_v2_records(data, &header, offset_size, length_size);
0,
offset_size,
length_size,
);
} }
} }
}
// Parsing a header requires a valid checksum, which random input almost
// never has, so the traversal behind it went unfuzzed — and that is where
// a node listing itself as its own child overflowed the stack. Take the
// header fields straight from the input instead and walk the rest.
let Some((fields, file)) = data.split_first_chunk::<20>() else {
return;
};
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);
}); });