Repository navigation
Description
Summary
The btree_page fuzz target (fuzz/fuzz_targets/btree_page.rs) never decodes the fuzzer-controlled page as a B+tree node, so OnDiskIndexReader / range_scan / leaf-entry decoding (read_leaf_entries::<EavtKey> etc.) get no fuzz coverage.
Why
The target writes a synthetic header plus the fuzzed bytes at page 2, sets eavt_root_page = 2, opens the file and runs a query. But PersistentFactStorage::load() never trusts that root:
fact_page_countis left0, sonum_fact_pagescomes from the fallbackeavt_root_page.saturating_sub(1)= 1.- With
num_fact_pages != 0andeavt_root_page != 0,load()compares the storedindex_checksum(left0) with the CRC32 over pages 1..page_count. They never match, soneeds_rebuildis true. - The rebuild re-indexes from fact page 1 (all zeros), writes fresh empty B+trees, and overwrites the header roots. Page 2 is never read.
So the target's result does not depend on its input. On main it was vacuous for a different reason: page_count = 0 failed with STG-007. The v8 header change in the #371 branch moved it past that point but not into node decoding. A reviewer confirmed this with an out-of-tree probe that copies load()'s branch logic: the query returned an empty result without touching page 2.
fact_page.rs is fine: it now reaches packed-fact-page decoding.
Proposed fix
Build a header that sends load() into its "trust the index roots" branch:
- Set
fact_page_countexplicitly (e.g. 1, with page 1 a valid empty packed fact page) so the fallback isn't used. - Compute
index_checksumascompute_page_checksumover pages 1..page_count, after writing the fuzzed page, and setheader_checksumto match (or leave it 0 if a zero checksum is still skipped). - Keep
eavt_root_page(and optionally the other roots) pointing at the fuzzed page, then run queries that range-scan each index (entity-bound, attribute-bound, value-bound, ref-bound) so every key type's decode path runs. - The target must still never assert success. Errors from malformed nodes are expected; only panics, hangs and OOMs are findings.
- Add a seed to
fuzz/corpus/btree_page/containing a valid v8 leaf node and a valid internal node.
Acceptance: running the target with a known-bad node (e.g. a truncated slot directory) reaches btree_v6 decode code (checked with coverage or a temporary log), and a well-formed leaf seed returns its entries from a query.
Context
Found during the final review of the #371/#287 branch (fix/371-index-value-collision, file format v8). Deferred there so as not to widen that PR.
Activity
Merge pull request #498 from project-minigraf/fuzz/387-corpus-and-pag…
adityamukho commentedon Oct 10, 2026
Done on
v3in #498 (merged 2026-10-09).btree_pagenow builds a real v8 file through the public API, overwrites one leaf or internal node of any of the five trees (optionally with the type byte flipped), recomputes that page's CRC, and runs one input-chosen probe (EAVT/AEVT/AVET/VAET/DICT scans,:valid-at,:as-of,verify()), read-only or read-write.Acceptance, checked with a temporary trace in
storage/node.rs: the valid leaf and internal seeds (fuzz/corpus/btree_page/seed_v8_leaf*.bin,seed_v8_internal*.bin) decode the fuzzed page and return the full answer, andverify()is clean. The truncated-slot and type-flipped seeds decode it and fail withINT-049. The first v3 nightly run (37987855944) fuzzedbtree_pagefor its full time with no findings.