Commit Graph
5 Commits
Author SHA1 Message Date
osobhandClaude Opus 5.5 2ba4bc97d8 test(format): fuzz Blosc2 decoding for peak allocation
The Blosc2 fuzz tests checked only for panics and the size of the output,
so the offsets-chunk amplification passed all 40,000 iterations.
tests/blosc2_alloc_bounds.rs now measures peak allocation (a counting
global allocator) and asserts it stays within 6x the HDF5 chunk size plus
twice the input plus 2 MiB (ruzstd's fixed state) for:

- 20,000 mutated fixture frames, decoded with their real chunk size as
  the limit, with edits aimed at the frame's and chunks' size fields;
- 20,000 mutated first chunks on their own;
- 5,000 frames built from random header sizes, offsets chunks and B2ND
  shapes (padding chunk and block shapes, chunks larger than the array,
  NaN, zero and repeated-value chunks).

Against the code before this series every test in the file fails (the
fuzz tests at frame iteration 3913, a zstd window, and random frame 289,
the offsets chunk); now the worst frame peaks at 0.48 of the bound.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 11:35:08 -05:00
osobhandClaude Opus 5.5 d9e4dfb6e6 fix(format): cap the Zstandard window at what the output can need
ruzstd reserves a frame's declared window (up to its 100 MiB default)
when a decoder is reset for a new frame, before decoding anything. The
Blosc, Blosc2 and bitshuffle decoders reuse one decoder per chunk, so a
Blosc2 chunk of two 16-byte streams, each declaring a 96 MiB window,
allocated 128 MiB. zstd_decode_into now sets the decoder's maximum window
to twice the stream's output (at least 128 KiB): c-blosc, c-blosc2 and
bitshuffle compress each block in one call with its size known, so
libzstd's window never exceeds the block.

Found by tracking peak allocation in the Blosc2 fuzz test.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 11:34:28 -05:00
osobhandClaude Opus 5.5 22dc87b07c fix(format): never hold a B2ND chunk's padding
B2ND chunks were decoded whole, padding included, with up to 16x the HDF5
chunk size as their limit, so a crafted frame made each chunk allocate and
fill up to 16x the output (4 GiB for a 256 MiB HDF5 chunk). The padding is
the real bound (prod(ceil(c/b)*b) per chunk), but that can be 2^ndim times
the array, so it is no longer held at all:

- A Blosc2 chunk is now decoded block by block (decode_blocks), each block
  handed to a sink as it is ready, with at most three blocks of scratch.
  blosc2_decompress_chunk and plain frames still collect every block.
- reassemble places each B2ND block straight into the output and skips
  blocks that are all padding (they are not decoded unless the delta
  filter needs the first block). A frame's NaN chunks are handed over one
  B2ND block at a time and its zero chunks cost nothing.
- A B2ND chunk must decode to exactly its padded size, its Blosc2 blocks
  must be whole B2ND blocks no larger than the output, and a chunk may not
  be larger than the array (hdf5-blosc2's chunk is the array), so a block
  is never larger than the output.

Peak allocation for a 10-D array padded to 13x (NaN, repeated-value and
stored-block chunks) and for a 16x chunk was 4.5 MB and 17.8 MB for
315 KB and 1 MiB outputs before, and is now within the tests' bound.
Blosc2 files written by hdf5plugin in 9-D and 12-D, an 8 MiB single chunk,
1x1x1 and edge-chunk shapes still read exactly.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 11:30:39 -05:00
osobhandClaude Opus 5.5 e05530a805 fix(format): an empty Blosc2 chunk no longer allocates its block size
A chunk header with nbytes 0 and no special type kept its declared block
size (up to 512 MiB): the block size was clamped to nbytes only when nbytes
was positive, and the scratch blocks were allocated before the (empty)
block loop, so a 20-byte chunk allocated about 1 GiB. The block size is now
clamped to nbytes always, and an empty chunk returns before any scratch is
allocated.

A frame chunk must also decode to the size the frame header gives it
(chunksize, or the remainder for the last chunk), and is decoded with that
as its limit, so an empty chunk in a frame for a non-empty HDF5 chunk is an
error rather than an empty result.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 11:27:06 -05:00
osobhandClaude Opus 5.5 989335b67b fix(format): bound a Blosc2 frame's offsets chunk by the HDF5 chunk size
parse_frame sized the offsets chunk from the frame header's own nbytes and
chunksize, so a 173-byte frame declaring 32 Mi chunks, with a 40-byte
repeated-value offsets chunk, built 256 MiB (up to 2 GiB) of offsets for a
1 MiB HDF5 chunk and then returned 4 bytes. The offsets chunk is now capped
at the output limit (at least 128 bytes); a frame whose nbytes/chunksize
imply more chunks than that is refused before anything is allocated.

tests/blosc2_alloc_bounds.rs measures peak allocation with a counting
global allocator; the reviewer's frame failed it (decoded Ok(4)) before.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 11:26:28 -05:00