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]>
This commit is contained in:
@@ -61,9 +61,11 @@ fn peak_during<T>(f: impl FnOnce() -> T) -> (T, usize) {
|
||||
|
||||
/// What decoding one HDF5 chunk of `limit` bytes from `input` may hold at
|
||||
/// once: the output, a few blocks of scratch (each no larger than the
|
||||
/// output), the offsets table, and the Zstandard decoder's state.
|
||||
/// output), the offsets table, and the Zstandard decoder's state, which has
|
||||
/// a fixed ceiling: a window of at most 128 KiB (or twice the stream) and a
|
||||
/// block's table of sequences (up to 98,303 of 12 bytes, 1.2 MB).
|
||||
fn bound(limit: usize, input: &[u8]) -> usize {
|
||||
6 * limit + 2 * input.len() + (1 << 20)
|
||||
6 * limit + 2 * input.len() + (2 << 20)
|
||||
}
|
||||
|
||||
fn lock() -> std::sync::MutexGuard<'static, ()> {
|
||||
@@ -348,3 +350,48 @@ fn b2nd_chunk_larger_than_the_array_is_not_allocated() {
|
||||
r.map(|v| v.len())
|
||||
);
|
||||
}
|
||||
|
||||
/// ruzstd reserves a frame's declared window (up to 100 MiB) before it
|
||||
/// decodes a frame with a decoder it has used before: a Blosc2 chunk of
|
||||
/// two 16-byte Zstandard streams, each declaring a 96 MiB window,
|
||||
/// allocated 96 MiB. c-blosc2 compresses each block with its size known,
|
||||
/// so its windows never exceed the block.
|
||||
#[test]
|
||||
fn zstd_window_is_bounded_by_the_output() {
|
||||
let _g = lock();
|
||||
let mut z = 0xfd2f_b528u32.to_le_bytes().to_vec();
|
||||
// No single segment, no checksum; window 2^26 + 4/8 of it = 96 MiB.
|
||||
z.extend_from_slice(&[0x00, (16 << 3) | 4]);
|
||||
// One raw block, last, of 16 bytes.
|
||||
let h = 1 | (16 << 3);
|
||||
z.extend_from_slice(&[h as u8, (h >> 8) as u8, 0]);
|
||||
z.extend_from_slice(&[7; 16]);
|
||||
// Two blocks of 16 bytes, one stream each (not split), Zstandard
|
||||
// (codec 4).
|
||||
let chunk = |z: &[u8]| {
|
||||
let mut c = vec![5u8, 1, 0x10 | (4 << 5), 1];
|
||||
for v in [32i32, 16, 0] {
|
||||
c.extend_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
let first = 24 + 4 + z.len();
|
||||
c.extend_from_slice(&24i32.to_le_bytes());
|
||||
c.extend_from_slice(&(first as i32).to_le_bytes());
|
||||
for _ in 0..2 {
|
||||
c.extend_from_slice(&(z.len() as i32).to_le_bytes());
|
||||
c.extend_from_slice(z);
|
||||
}
|
||||
let n = c.len() as i32;
|
||||
c[12..16].copy_from_slice(&n.to_le_bytes());
|
||||
c
|
||||
};
|
||||
let c = chunk(&z);
|
||||
let (r, peak) = peak_during(|| blosc2_decompress_chunk(&c, 32));
|
||||
assert!(peak <= bound(32, &c), "peak {peak} bytes ({r:?})");
|
||||
assert!(r.is_err(), "{r:?}");
|
||||
// The same streams with a window they can use read.
|
||||
z[5] = 0;
|
||||
assert_eq!(
|
||||
blosc2_decompress_chunk(&chunk(&z), 32).unwrap(),
|
||||
vec![7; 32]
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user