fix(format): bound what a VL read retains on a crafted global heap

VlResolver kept an owned copy of every object of every heap collection
it parsed, for the whole read. Collections nested inside each other's
object data, 32 bytes apart with each element pointing at a different
one, made retained memory O(elements x file size): 1.58 GB for a 744 KB
file (read_vl_strings did the same before VlResolver). Chaining every
collection's objects into one shared run of tiny objects made parse
time O(elements x objects) as well. libhdf5 refuses these files.

- The cache records where each object lies (GlobalHeapCollection::
  parse_index, new) instead of copying it, and is dropped past a 32 MiB
  budget.
- A collection overlapping one already read is an error: libhdf5 gives
  every collection its own block, so only a crafted file has them.
- parse and parse_index refuse a collection that runs past the end of
  the file and an object that runs past the end of its collection.

tests/vl_heap_bounds.rs measures peak heap use with a counting
allocator: 129 MB and 350 MB live before on its two crafted files (64 KB
and 176 KB), 97 KB and 0.9 MB now. Conformance unchanged at 575 of 697.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-09-26 08:56:42 -05:00
co-authored by Claude Opus 5.5
parent 8ce6eca34d
commit 41b7837d0a
5 changed files with 453 additions and 61 deletions
+12
View File
@@ -49,6 +49,18 @@
`clawhdf5_netcdf4::Variable::read_string` (checked against netCDF4-python
in `crates/clawhdf5-netcdf4/tests/interop_tests.rs`).
- **Crafted global heaps could exhaust memory.** `VlResolver` kept an owned
copy of every object of every heap collection it read, so collections
nested inside each other's object data made a 744 KB file take 1.58 GB
(and `read_vl_strings` before it did the same). The cache now records
where objects lie instead of copying them, is dropped past a 32 MiB
budget, and a collection overlapping one already read is an error
(libhdf5 never writes one). New `GlobalHeapCollection::parse_index`
locates a collection's objects without copying them; `parse` and
`parse_index` refuse a collection running past the end of the file or an
object running past its collection. Conformance unchanged at 575 of 697
(`crates/clawhdf5-format/tests/vl_heap_bounds.rs`).
### Plugin filters (2026-09-26)
- **LZF, bitshuffle, bzip2 and Blosc read and write, in pure Rust.** Files
written by h5py with `compression="lzf"`, or with hdf5plugin's