Fix b2nd metadata buffer overflow#781
Conversation
There was a problem hiding this comment.
Pull request overview
This PR addresses a heap-buffer-overflow risk in several Blosc2 plugins by ensuring that buffers passed to b2nd_deserialize_meta() are sized for the full supported B2ND_MAX_DIM (16) rather than an implicit 8-dimension assumption, and adds additional safety checks in ndmean around metadata deserialization and dimension bounds.
Changes:
- Hardened
shape/chunkshape/blockshapebuffer allocations in multiple plugins to useB2ND_MAX_DIM. - Added error handling for
b2nd_deserialize_meta()andndimbounds validation inplugins/filters/ndmean/ndmean.c. - Applied the allocation fix across compression/decompression paths in affected codec plugins.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 8 comments.
| File | Description |
|---|---|
| plugins/filters/ndmean/ndmean.c | Allocates metadata arrays sized for B2ND_MAX_DIM, checks deserialization failure, and validates ndim before using fixed-size internal arrays. |
| plugins/codecs/zfp/blosc2-zfp.c | Updates dimension-metadata buffer allocations to B2ND_MAX_DIM across ZFP codec entry points. |
| plugins/codecs/ndlz/ndlz8x8.c | Updates dimension-metadata buffer allocations to B2ND_MAX_DIM for NDLZ 8x8 compression path. |
| plugins/codecs/ndlz/ndlz4x4.c | Updates dimension-metadata buffer allocations to B2ND_MAX_DIM for NDLZ 4x4 compression path. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| int64_t *shape = malloc(B2ND_MAX_DIM * sizeof(int64_t)); | ||
| int32_t *chunkshape = malloc(B2ND_MAX_DIM * sizeof(int32_t)); | ||
| int32_t *blockshape = malloc(B2ND_MAX_DIM * sizeof(int32_t)); |
There was a problem hiding this comment.
Fixed. The b2nd_deserialize_meta return is now captured and checked together with an ndim range check (1..ZFP_MAX_DIM) right after free(smeta), so malformed/truncated metadata can no longer leave ndim/blockshape uninitialized. The blockshape[i] < 4 early return now frees shape/chunkshape/blockshape (leak resolved).
| int64_t *shape = malloc(B2ND_MAX_DIM * sizeof(int64_t)); | ||
| int32_t *chunkshape = malloc(B2ND_MAX_DIM * sizeof(int32_t)); | ||
| int32_t *blockshape = malloc(B2ND_MAX_DIM * sizeof(int32_t)); |
There was a problem hiding this comment.
Fixed. Return value captured and validated via zfp_check_output_size, which rejects failed deserialization and out-of-range ndim before the switch(ndim)/zfp_field_* calls (and also bounds the decompressed size against output_len). Buffers freed on the error path.
| int64_t *shape = malloc(B2ND_MAX_DIM * sizeof(int64_t)); | ||
| int32_t *chunkshape = malloc(B2ND_MAX_DIM * sizeof(int32_t)); | ||
| int32_t *blockshape = malloc(B2ND_MAX_DIM * sizeof(int32_t)); |
There was a problem hiding this comment.
Fixed. Deserialize return + ndim range now checked after free(smeta); the blockshape[i] < 4 early return frees shape/chunkshape/blockshape.
| int64_t *shape = malloc(B2ND_MAX_DIM * sizeof(int64_t)); | ||
| int32_t *chunkshape = malloc(B2ND_MAX_DIM * sizeof(int32_t)); | ||
| int32_t *blockshape = malloc(B2ND_MAX_DIM * sizeof(int32_t)); |
There was a problem hiding this comment.
Fixed. Return value captured and validated via zfp_check_output_size before any zfp_field_* selection, so parse failures / bad ndim can no longer cause OOB. Buffers freed on the error path.
| int64_t *shape = malloc(B2ND_MAX_DIM * sizeof(int64_t)); | ||
| int32_t *chunkshape = malloc(B2ND_MAX_DIM * sizeof(int32_t)); | ||
| int32_t *blockshape = malloc(B2ND_MAX_DIM * sizeof(int32_t)); |
There was a problem hiding this comment.
Fixed. Deserialize return + ndim range now checked after free(smeta); the blockshape[i] < 4 early return frees shape/chunkshape/blockshape.
| int64_t *shape = malloc(B2ND_MAX_DIM * sizeof(int64_t)); | ||
| int32_t *chunkshape = malloc(B2ND_MAX_DIM * sizeof(int32_t)); | ||
| int32_t *blockshape = malloc(B2ND_MAX_DIM * sizeof(int32_t)); |
There was a problem hiding this comment.
Fixed. Return value captured and validated via zfp_check_output_size (rejects failed deserialization and out-of-range ndim, and bounds size vs output_len) before zfp_field_*. Buffers freed on the error path.
| int64_t *shape = malloc(B2ND_MAX_DIM * sizeof(int64_t)); | ||
| int32_t *chunkshape = malloc(B2ND_MAX_DIM * sizeof(int32_t)); | ||
| int32_t *blockshape = malloc(B2ND_MAX_DIM * sizeof(int32_t)); |
There was a problem hiding this comment.
Fixed. The b2nd_deserialize_meta return is now captured and checked (folded into the ndim != 2 guard). Every early return (ndim, input_len, output_len, overhead) now frees shape/chunkshape/blockshape, and the overhead path also frees bufarea.
| int64_t *shape = malloc(B2ND_MAX_DIM * sizeof(int64_t)); | ||
| int32_t *chunkshape = malloc(B2ND_MAX_DIM * sizeof(int32_t)); | ||
| int32_t *blockshape = malloc(B2ND_MAX_DIM * sizeof(int32_t)); |
There was a problem hiding this comment.
Fixed. Deserialize return captured and checked (folded into the ndim != 2 guard), and all early returns (ndim, input_len, output_len, overhead) now free shape/chunkshape/blockshape.
| int32_t dim = blockshape[i]; | ||
| if (dim < 0) { | ||
| BLOSC_TRACE_ERROR("Negative blockshape"); | ||
| return BLOSC2_ERROR_FAILURE; | ||
| } | ||
| if (dim == 0) { | ||
| nbytes = 0; | ||
| break; | ||
| } | ||
| if (nbytes > cap / dim) { | ||
| BLOSC_TRACE_ERROR("Decompressed block size exceeds the output buffer (%d bytes)", output_len); | ||
| return BLOSC2_ERROR_FAILURE; | ||
| } | ||
| nbytes *= dim; |
There was a problem hiding this comment.
Fixed. Non-positive block dimensions are now rejected outright (if (dim <= 0) return BLOSC2_ERROR_FAILURE;), replacing the dim == 0 → nbytes = 0; break; shortcut. A crafted zero dimension can no longer slip past the size guard or reach zfp_field_*/zfp_decompress with a 0-sized dimension.
| free(shape); | ||
| free(chunkshape); | ||
| free(blockshape); | ||
| BLOSC_TRACE_ERROR("ZFP does not support blocks smaller than cells (4x...x4"); |
There was a problem hiding this comment.
Fixed — added the closing parenthesis: "ZFP does not support blocks smaller than cells (4x...x4)".
| free(shape); | ||
| free(chunkshape); | ||
| free(blockshape); | ||
| BLOSC_TRACE_ERROR("ZFP does not support blocks smaller than cells (4x...x4"); |
There was a problem hiding this comment.
Fixed — added the closing parenthesis: "ZFP does not support blocks smaller than cells (4x...x4)".
| free(shape); | ||
| free(chunkshape); | ||
| free(blockshape); | ||
| BLOSC_TRACE_ERROR("ZFP does not support blocks smaller than cells (4x...x4"); |
There was a problem hiding this comment.
Fixed — added the closing parenthesis: "ZFP does not support blocks smaller than cells (4x...x4)". (Same one-line fix applied to all three compressors.)
| static int zfp_check_output_size(int deserialize_rc, int8_t ndim, | ||
| const int32_t *blockshape, | ||
| int32_t typesize, int32_t output_len) { | ||
| if (deserialize_rc < 0) { | ||
| BLOSC_TRACE_ERROR("Cannot deserialize b2nd meta info"); |
There was a problem hiding this comment.
Added plugins/codecs/zfp/test_zfp_oversized_blockshape.c (wired into CMakeLists + ctest as test_zfp_oversized_blockshape). It drives all three decoders (zfp_acc/prec/rate_decompress) with crafted "b2nd" metalayers — blockshape larger than output_len, a zero block dimension, and an int64-overflow blockshape (2e9×2e9) — and asserts each returns an error without writing past the output buffer, so this fix can't regress silently.
| uint8_t *smeta = NULL; | ||
| int smeta_len = b2nd_serialize_meta(ndim, shape, chunkshape, blockshape, "<f4", 0, &smeta); | ||
| if (smeta_len < 0) { | ||
| blosc2_schunk_free(schunk); | ||
| return NULL; | ||
| } | ||
| blosc2_meta_add(schunk, "b2nd", smeta, smeta_len); | ||
| free(smeta); | ||
| return schunk; |
There was a problem hiding this comment.
Fixed. make_schunk now checks the blosc2_meta_add return and returns NULL on failure; the test harness treats a NULL schunk as a setup failure (-- FAIL), so the metalayer must actually attach for the case to count as a pass. This prevents the decoders failing for the wrong reason ("b2nd layer not found").
| int main(void) { | ||
| int result = 0; | ||
| printf("ZFP decompress hardening regression tests:\n"); | ||
|
|
||
| /* 64*64*4 = 16384 bytes claimed vs a 256-byte output buffer */ | ||
| result |= expect_rejected("oversized blockshape", 64, 64, 256); | ||
| /* a zero block dimension must be invalid input, not a size-0 shortcut */ | ||
| result |= expect_rejected("zero block dimension", 0, 64, 256); | ||
| /* prod(blockshape)*typesize overflows int64 (must not wrap past the guard) */ | ||
| result |= expect_rejected("int64 overflow blockshape", 2000000000, 2000000000, 256); | ||
|
|
||
| if (result == 0) { | ||
| printf("All ZFP hardening checks passed.\n"); | ||
| } | ||
| return result; | ||
| } |
There was a problem hiding this comment.
Fixed. main() now calls blosc2_init() at the start and blosc2_destroy() before returning, matching the other plugin tests, so the plugin mechanism/global state is initialized consistently across build configurations.
| free(shape); | ||
| free(chunkshape); | ||
| free(blockshape); | ||
| BLOSC_TRACE_ERROR("ZFP does not support blocks smaller than cells (4x...x4"); |
There was a problem hiding this comment.
Fixed — added the closing parenthesis: "...cells (4x...x4)".
| free(shape); | ||
| free(chunkshape); | ||
| free(blockshape); | ||
| BLOSC_TRACE_ERROR("ZFP does not support blocks smaller than cells (4x...x4"); |
There was a problem hiding this comment.
Fixed — added the closing parenthesis: "...cells (4x...x4)".
| free(shape); | ||
| free(chunkshape); | ||
| free(blockshape); | ||
| BLOSC_TRACE_ERROR("ZFP does not support blocks smaller than cells (4x...x4"); |
There was a problem hiding this comment.
Fixed — added the closing parenthesis: "...cells (4x...x4)". (Same one-line fix applied to all three compressors.)
b643a32 to
07a917e
Compare
Harden ndlz/zfp/ndmean plugin decoders against crafted b2nd metalayers Untrusted-input hardening for the multidimensional plugin codecs/filters, whose block geometry is taken from the attacker-controlled "b2nd" metalayer: * zfp (blosc2-zfp.c): add zfp_check_output_size() and call it in all three decompressors. It validates the b2nd_deserialize_meta return, ndim range, typesize, and that prod(blockshape)*typesize fits in output_len, computed overflow-safe so a crafted blockshape cannot wrap past the guard. Non-positive block dimensions are rejected. Without this, zfp_decompress could write past the block output buffer (heap overflow). The compressors also now check the deserialize return / ndim and free their buffers on every early return. * ndlz (ndlz4x4.c, ndlz8x8.c): check the b2nd_deserialize_meta return and free shape/chunkshape/blockshape (and bufarea) on the early-return paths. * ndmean (ndmean.c): validate the deserialize return and ndim range; size the shape/chunkshape/blockshape buffers for B2ND_MAX_DIM. * Add regression test test_zfp_oversized_blockshape covering oversized, zero-dimension, and int64-overflow blockshapes for all three zfp decoders. @
07a917e to
48d4f8a
Compare
|
any update on this? |
|
Thanks @metsw24-max ! |
This PR fixes a heap-buffer-overflow vulnerability in multiple Blosc2 plugins caused by incorrect buffer sizing during
b2ndmetadata deserialization.The issue arises because$8 < \text{ndim} \le 16$ could therefore trigger out-of-bounds writes on heap-allocated memory.
b2nd_deserialize_meta()supports up toB2ND_MAX_DIM(16) dimensions, but several plugins previously allocated fixed-size buffers for only 8 dimensions. A crafted b2nd metalayer withRoot Cause
The function
b2nd_deserialize_meta()writes dimension-related metadata into the following arrays:shapechunkshapeblockshapeIt allows up to
B2ND_MAX_DIM(16) dimensions. However, multiple plugins incorrectly allocated these buffers using:This mismatch between the maximum possible
ndim(16) and the allocated buffer size (8) results in a heap-buffer-overflow when processing crafted or malicious input.Changes
1. Buffer Size Hardening
All affected allocations are updated from a fixed size of 8 to
B2ND_MAX_DIM. This ensures consistency with the maximum dimension supported byb2nd_deserialize_meta().2. ndmean Safety Improvements
In
ndmean_forwardandndmean_backward:This prevents invalid or oversized dimension values from being used in fixed-size internal arrays.
3. Cross-Plugin Consistency Fix
The same buffer sizing correction is applied across:
Compression paths
Decompression paths
Both forward and backward filter implementations