From 0901fb14991457495ebc7fb6fd13256141b92e36 Mon Sep 17 00:00:00 2001 From: osobh Date: Sun, 20 Sep 2026 17:03:21 -0700 Subject: [PATCH] test(fuzz): fuzz B-tree v2 traversal, not just header parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- crates/clawhdf5-format/fuzz/.gitignore | 3 ++ .../fuzz/fuzz_targets/fuzz_btree_v2.rs | 33 +++++++++++++++---- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/crates/clawhdf5-format/fuzz/.gitignore b/crates/clawhdf5-format/fuzz/.gitignore index 2f7896d..fe68c97 100644 --- a/crates/clawhdf5-format/fuzz/.gitignore +++ b/crates/clawhdf5-format/fuzz/.gitignore @@ -1 +1,4 @@ target/ +corpus/ +artifacts/ +coverage/ diff --git a/crates/clawhdf5-format/fuzz/fuzz_targets/fuzz_btree_v2.rs b/crates/clawhdf5-format/fuzz/fuzz_targets/fuzz_btree_v2.rs index 370b2d8..a33b4af 100644 --- a/crates/clawhdf5-format/fuzz/fuzz_targets/fuzz_btree_v2.rs +++ b/crates/clawhdf5-format/fuzz/fuzz_targets/fuzz_btree_v2.rs @@ -1,15 +1,36 @@ #![no_main] +use clawhdf5_format::btree_v2::{BTreeV2Header, collect_btree_v2_records}; use libfuzzer_sys::fuzz_target; fuzz_target!(|data: &[u8]| { for &offset_size in &[4u8, 8] { for &length_size in &[4u8, 8] { - let _ = clawhdf5_format::btree_v2::BTreeV2Header::parse( - data, - 0, - offset_size, - length_size, - ); + if let Ok(header) = BTreeV2Header::parse(data, 0, offset_size, length_size) { + let _ = collect_btree_v2_records(data, &header, 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); });