fix(format): read Extensible Array chunk indexes correctly

A dataset with exactly one unlimited dimension — the ordinary
append-only case — is indexed by an Extensible Array. Only its first
few chunk entries (4 by default) sit inline in the index block, and
everything past them was read with the wrong layout. In the default
shape the 37th chunk onward came back from the wrong place: a
400-chunk dataset returned 364 wrong values while reporting success,
and beyond about a thousand chunks the read failed outright. Silently
wrong data is the worse half of that.

It survived because the only Extensible Array fixture in the suite had
three chunks — inside the inline limit — so no test ever reached a data
block.

Four layout errors, each confirmed against files written by HDF5 2.0 and
against the library source rather than inferred:

- super block `u` owns 2^(u/2) data blocks, not 2^u;
- each holds 2^((u+1)/2) * data_blk_min_elmts elements — the two
  quantities double every *other* level, a half step apart;
- a super block carries a block-offset field before its data block
  addresses, which was not skipped;
- the page-init bitmap belongs to the super block, one bit per page
  packed across all of its data blocks and read MSB-first, rather than
  living inside the data block; a paged data block also ends its prefix
  with a checksum before the first page.

Where the spec left room for doubt the file settled it: decoding a
paged block's elements and reading the chunk values they address
identifies the mapping exactly, and the bitmap's 68 set bits matched
the 34 data blocks x 2 pages that 200 000 elements need, which only
holds MSB-first.

New interop tests cross every boundary — 4, 37, 400, 5 000 and 200 000
chunks, the last with paged data blocks — plus sparse (uninitialised
pages taking fill values), gzip-filtered elements and a 2-D dataset.
All three fail against the old traversal.

Writing is untouched; this was a read-path bug.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-09-20 17:18:59 -07:00
co-authored by Claude Opus 5
parent 0901fb1499
commit 367faad7f7
4 changed files with 445 additions and 257 deletions
+26
View File
@@ -2,6 +2,32 @@
## Unreleased
### Correctness
- `clawhdf5-format`: **datasets indexed by an Extensible Array returned wrong
data beyond their first few dozen chunks.** One unlimited dimension gives a
dataset an Extensible Array chunk index, whose first elements (4 by default)
sit inline in the index block and whose rest live in data blocks sized by a
formula the reader got wrong. In the default layout everything through the
36th chunk happened to line up and the 37th onwards did not: a 400-chunk
dataset silently returned wrong values from chunk 37, and datasets past
about a thousand chunks failed outright with "invalid Extensible Array data
block signature". **Reads were wrong, not
merely refused** — the caller got plausible numbers from the wrong chunks.
Four separate layout errors, each checked against files written by HDF5 2.0
and against the library source:
- the number of data blocks in super block `u` is `2^(u/2)`, not `2^u`;
- each holds `2^((u+1)/2) * data_blk_min_elmts` elements, which doubles
every *other* level rather than every level;
- a super block carries a block-offset field before its data block
addresses, which was not skipped;
- the page-init bitmap belongs to the super block, one bit per page packed
across all its data blocks (MSB first), and was being read from inside the
data block instead; a paged data block also ends its prefix with a
checksum before the first page.
Covered now by interop tests at 4, 37, 400, 5 000 and 200 000 chunks (the
last large enough for paged data blocks), plus sparse, gzip-filtered and
2-D cases. Writing is unaffected; this is a read-path bug.
### Security
- `clawhdf5-format`: **a crafted file could crash any reader through B-tree v2
traversal.** Recursion was bounded only by the depth the file claimed (a