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]>
Fuzz Testing for clawhdf5-format
Uses cargo-fuzz (libFuzzer) to test parser robustness against malformed inputs.
Prerequisites
cargo install cargo-fuzz
rustup toolchain install nightly
Fuzz Targets
| Target | Parser | Description |
|---|---|---|
fuzz_superblock |
Superblock::parse |
Superblock parsing (v0-v3) with signature search |
fuzz_object_header |
ObjectHeader::parse |
Object header v1/v2 with various offset/length sizes |
fuzz_datatype |
Datatype::parse |
All 12 HDF5 datatype classes (recursive) |
fuzz_dataspace |
Dataspace::parse |
Dataspace messages with various length sizes |
fuzz_fractal_heap |
FractalHeapHeader::parse |
Fractal heap header parsing |
fuzz_btree_v2 |
BTreeV2Header::parse |
B-tree v2 header parsing |
fuzz_filter_pipeline |
FilterPipeline::parse |
Filter pipeline messages (v1/v2) |
fuzz_full_file |
signature + superblock + root group | End-to-end file parsing chain |
fuzz_dataset_read |
Dataset::read_* (via clawhdf5) |
Walks every dataset in the parsed file and exercises the contiguous/chunked/compact raw-data read paths (chunked_read.rs, data_read.rs) that fuzz_full_file doesn't reach |
Running
Run a single target (runs indefinitely until stopped or a crash is found):
cd crates/clawhdf5-format
cargo +nightly fuzz run fuzz_datatype
Run with a time limit (seconds):
cargo +nightly fuzz run fuzz_datatype -- -max_total_time=60
Run all targets for 30 seconds each:
for target in fuzz_superblock fuzz_object_header fuzz_datatype fuzz_dataspace \
fuzz_fractal_heap fuzz_btree_v2 fuzz_filter_pipeline fuzz_full_file \
fuzz_dataset_read; do
echo "=== $target ==="
cargo +nightly fuzz run "$target" -- -max_total_time=30 -max_len=4096
done
CI
These targets are not run in CI (.gitea/workflows/ci.yml) — cargo-fuzz
requires nightly and each meaningful run takes minutes, which doesn't fit a
per-PR gate. Run them manually on a schedule (e.g. before a release, or after
touching parser code) instead.
Reproducing Crashes
If a crash is found, the input is saved to fuzz/artifacts/<target>/. Reproduce with:
cargo +nightly fuzz run fuzz_datatype fuzz/artifacts/fuzz_datatype/crash-<hash>
Minimize the crashing input:
cargo +nightly fuzz tmin fuzz_datatype fuzz/artifacts/fuzz_datatype/crash-<hash>
Design
Each fuzz target feeds arbitrary bytes directly to a parser entry point. The parsers must never panic on any input -- they should return Err(FormatError) for malformed data. Any panic found by fuzzing is a bug that should be fixed with proper bounds checks and error returns.