Skip to content

Fix b2nd metadata buffer overflow#781

Merged
FrancescAlted merged 3 commits into
Blosc:mainfrom
metsw24-max:b2nd-deserialize-meta-ndim-bounds-overflow
Jun 9, 2026
Merged

Fix b2nd metadata buffer overflow#781
FrancescAlted merged 3 commits into
Blosc:mainfrom
metsw24-max:b2nd-deserialize-meta-ndim-bounds-overflow

Conversation

@metsw24-max

Copy link
Copy Markdown
Contributor

This PR fixes a heap-buffer-overflow vulnerability in multiple Blosc2 plugins caused by incorrect buffer sizing during b2nd metadata deserialization.

The issue arises because b2nd_deserialize_meta() supports up to B2ND_MAX_DIM (16) dimensions, but several plugins previously allocated fixed-size buffers for only 8 dimensions. A crafted b2nd metalayer with $8 < \text{ndim} \le 16$ could therefore trigger out-of-bounds writes on heap-allocated memory.

Root Cause

The function b2nd_deserialize_meta() writes dimension-related metadata into the following arrays:

  • shape
  • chunkshape
  • blockshape

It allows up to B2ND_MAX_DIM (16) dimensions. However, multiple plugins incorrectly allocated these buffers using:

malloc(8 * sizeof(...))

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 by b2nd_deserialize_meta().

2. ndmean Safety Improvements

In ndmean_forward and ndmean_backward:

  • Added deserialization error handling:
if (b2nd_deserialize_meta(...) < 0) {
    // cleanup and fail safely
}
  • Added bounds validation:
if (ndim <= 0 || ndim > NDMEAN_MAX_DIM) {
    return BLOSC2_ERROR_FAILURE;
}

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 / blockshape buffer allocations in multiple plugins to use B2ND_MAX_DIM.
  • Added error handling for b2nd_deserialize_meta() and ndim bounds validation in plugins/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.

Comment on lines +30 to +32
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));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment on lines +155 to +157
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));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +246 to +248
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));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Deserialize return + ndim range now checked after free(smeta); the blockshape[i] < 4 early return frees shape/chunkshape/blockshape.

Comment on lines +394 to +396
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));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +512 to +514
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));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Deserialize return + ndim range now checked after free(smeta); the blockshape[i] < 4 early return frees shape/chunkshape/blockshape.

Comment on lines +648 to +650
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));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +74 to +76
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));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +72 to +74
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));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

Comment on lines +60 to +73
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;

@metsw24-max metsw24-max Jun 4, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread plugins/codecs/zfp/blosc2-zfp.c Outdated
free(shape);
free(chunkshape);
free(blockshape);
BLOSC_TRACE_ERROR("ZFP does not support blocks smaller than cells (4x...x4");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — added the closing parenthesis: "ZFP does not support blocks smaller than cells (4x...x4)".

Comment thread plugins/codecs/zfp/blosc2-zfp.c Outdated
free(shape);
free(chunkshape);
free(blockshape);
BLOSC_TRACE_ERROR("ZFP does not support blocks smaller than cells (4x...x4");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — added the closing parenthesis: "ZFP does not support blocks smaller than cells (4x...x4)".

Comment thread plugins/codecs/zfp/blosc2-zfp.c Outdated
free(shape);
free(chunkshape);
free(blockshape);
BLOSC_TRACE_ERROR("ZFP does not support blocks smaller than cells (4x...x4");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — added the closing parenthesis: "ZFP does not support blocks smaller than cells (4x...x4)". (Same one-line fix applied to all three compressors.)

Comment on lines +35 to +39
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");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Comment on lines +44 to +52
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;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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").

Comment on lines +91 to +106
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;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread plugins/codecs/zfp/blosc2-zfp.c Outdated
free(shape);
free(chunkshape);
free(blockshape);
BLOSC_TRACE_ERROR("ZFP does not support blocks smaller than cells (4x...x4");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — added the closing parenthesis: "...cells (4x...x4)".

Comment thread plugins/codecs/zfp/blosc2-zfp.c Outdated
free(shape);
free(chunkshape);
free(blockshape);
BLOSC_TRACE_ERROR("ZFP does not support blocks smaller than cells (4x...x4");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — added the closing parenthesis: "...cells (4x...x4)".

Comment thread plugins/codecs/zfp/blosc2-zfp.c Outdated
free(shape);
free(chunkshape);
free(blockshape);
BLOSC_TRACE_ERROR("ZFP does not support blocks smaller than cells (4x...x4");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — added the closing parenthesis: "...cells (4x...x4)". (Same one-line fix applied to all three compressors.)

@metsw24-max metsw24-max force-pushed the b2nd-deserialize-meta-ndim-bounds-overflow branch from b643a32 to 07a917e Compare June 6, 2026 17:04
@
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.
@
@metsw24-max metsw24-max force-pushed the b2nd-deserialize-meta-ndim-bounds-overflow branch from 07a917e to 48d4f8a Compare June 6, 2026 17:44
@metsw24-max

Copy link
Copy Markdown
Contributor Author

any update on this?

@FrancescAlted FrancescAlted self-requested a review June 9, 2026 10:28

@FrancescAlted FrancescAlted left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@FrancescAlted FrancescAlted merged commit bb0a158 into Blosc:main Jun 9, 2026
18 checks passed
@FrancescAlted

Copy link
Copy Markdown
Member

Thanks @metsw24-max !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants