Bug#1150531: trixie-pu: package c-blosc2/2.23.1+ds-2+deb13u1
Bastian Germann
bage at debian.org
Sat Oct 10 12:30:51 BST 2026
Package: release.debian.org
Severity: normal
Tags: trixie
X-Debbugs-Cc: c-blosc2 at packages.debian.org
Control: affects -1 + src:c-blosc2
User: release.debian.org at packages.debian.org
Usertags: pu
[ Reason ]
CVE-2026-103222: integer/pointer overflow
[ Impact ]
Users are vulnerable to being exploited via CVE-2026-103222 with forged
user input.
[ Tests ]
The patch file also includes a regression test for this. It passes on
pkg build.
[ Risks ]
None, just some boundary enforcement. Valid files should not be
affected.
[ Checklist ]
[x] *all* changes are documented in the d/changelog
[x] I reviewed all changes and I approve them
[x] attach debdiff against the package in stable
[x] the issue is verified as fixed in unstable
[ Changes ]
The untouched upstream patch is included and applies cleanly on the
trixie version.
[ Other info ]
Team upload.
-------------- next part --------------
diff -Nru c-blosc2-2.23.1+ds/debian/changelog c-blosc2-2.23.1+ds/debian/changelog
--- c-blosc2-2.23.1+ds/debian/changelog 2026-07-18 09:36:28.000000000 +0200
+++ c-blosc2-2.23.1+ds/debian/changelog 2026-10-10 13:02:56.000000000 +0200
@@ -1,3 +1,11 @@
+c-blosc2 (2.23.1+ds-2+deb13u1) trixie; urgency=high
+
+ * Team upload.
+ * Fix integer and pointer overflow in blosclz decompression bounds checks.
+ (Closes: #1149701, CVE-2026-103222)
+
+ -- Bastian Germann <bage at debian.org> Sat, 10 Oct 2026 13:02:56 +0200
+
c-blosc2 (2.23.1+ds-2) unstable; urgency=medium
* Standards version bumped to 4.7.4, no changes.
diff -Nru c-blosc2-2.23.1+ds/debian/patches/0004-CVE-2026-103222.patch c-blosc2-2.23.1+ds/debian/patches/0004-CVE-2026-103222.patch
--- c-blosc2-2.23.1+ds/debian/patches/0004-CVE-2026-103222.patch 1970-01-01 01:00:00.000000000 +0100
+++ c-blosc2-2.23.1+ds/debian/patches/0004-CVE-2026-103222.patch 2026-10-10 12:13:05.000000000 +0200
@@ -0,0 +1,363 @@
+Origin: upstream, fe2964d114d97847f56570a0ab2be2c57ccbeedc
+From: Francesc Alted <francesc at blosc.org>
+Date: Mon, 31 Aug 2026 10:54:57 +0200
+Subject: Fix integer and pointer overflow in blosclz decompression bounds checks
+
+ In blosclz_decompress, match length accumulation in the length-extension
+ loop (while (code == 255)) was unbounded, allowing crafted chunks with
+ millions of 0xFF bytes to grow len up to ~255 MB or more.
+
+ On 32-bit platforms (and wasm32), pointer arithmetic `op + len` wrapped
+ modulo 2^32 when the output buffer landed high in the address space,
+ bypassing the `op + len > op_limit` guard and causing heap out-of-bounds
+ writes. On 64-bit platforms, an excessively large input could similarly
+ cause signed int32 overflow and undefined behavior.
+
+ Fixes:
+ - Cap match length accumulation inside the extension loop with
+ `if (len > maxout) return 0;` to reject invalid lengths early.
+ - Convert all pointer-addition bounds checks (`op + len > op_limit`,
+ literal copy checks, and compressor macros) into subtraction form
+ (`len > op_limit - op`) to prevent pointer arithmetic wraparound.
+ - Add regression test in tests/test_blosclz_bounds.c.
+
+ Thanks to Lylan from QI-ANXIN Technology Research Institute for
+ discovering and reporting this issue.
+---
+ blosc/blosclz.c | 33 +++---
+ tests/test_blosclz_bounds.c | 204 ++++++++++++++++++++++++++++++++++++
+ 2 files changed, 222 insertions(+), 15 deletions(-)
+ create mode 100644 tests/test_blosclz_bounds.c
+
+diff --git a/blosc/blosclz.c b/blosc/blosclz.c
+index e1af7db8b..75cbe81ea 100644
+--- a/blosc/blosclz.c
++++ b/blosc/blosclz.c
+@@ -246,7 +246,7 @@ static uint8_t* get_run_or_match(uint8_t* ip, uint8_t* ip_bound, const uint8_t*
+
+
+ #define LITERAL(ip, op, op_limit, anchor, copy) { \
+- if (BLOSCLZ_UNLIKELY((op) + 2 > (op_limit))) \
++ if (BLOSCLZ_UNLIKELY((op_limit) - (op) < 2)) \
+ goto out; \
+ *(op)++ = *(anchor)++; \
+ (ip) = (anchor); \
+@@ -268,29 +268,29 @@ static uint8_t* get_run_or_match(uint8_t* ip, uint8_t* ip_bound, const uint8_t*
+ }
+
+ #define MATCH_SHORT(op, op_limit, len, distance) { \
+- if (BLOSCLZ_UNLIKELY((op) + 2 > (op_limit))) \
++ if (BLOSCLZ_UNLIKELY((op_limit) - (op) < 2)) \
+ goto out; \
+ *(op)++ = (uint8_t)(((len) << 5U) + ((distance) >> 8U));\
+ *(op)++ = (uint8_t)(((distance) & 255U)); \
+ }
+
+ #define MATCH_LONG(op, op_limit, len, distance) { \
+- if (BLOSCLZ_UNLIKELY((op) + 1 > (op_limit))) \
++ if (BLOSCLZ_UNLIKELY((op_limit) - (op) < 1)) \
+ goto out; \
+ *(op)++ = (uint8_t)((7U << 5U) + ((distance) >> 8U)); \
+ for ((len) -= 7; (len) >= 255; (len) -= 255) { \
+- if (BLOSCLZ_UNLIKELY((op) + 1 > (op_limit))) \
++ if (BLOSCLZ_UNLIKELY((op_limit) - (op) < 1)) \
+ goto out; \
+ *(op)++ = 255; \
+ } \
+- if (BLOSCLZ_UNLIKELY((op) + 2 > (op_limit))) \
++ if (BLOSCLZ_UNLIKELY((op_limit) - (op) < 2)) \
+ goto out; \
+ *(op)++ = (uint8_t)(len); \
+ *(op)++ = (uint8_t)(((distance) & 255U)); \
+ }
+
+ #define MATCH_SHORT_FAR(op, op_limit, len, distance) { \
+- if (BLOSCLZ_UNLIKELY((op) + 4 > (op_limit))) \
++ if (BLOSCLZ_UNLIKELY((op_limit) - (op) < 4)) \
+ goto out; \
+ *(op)++ = (uint8_t)(((len) << 5U) + 31); \
+ *(op)++ = 255; \
+@@ -299,15 +299,15 @@ static uint8_t* get_run_or_match(uint8_t* ip, uint8_t* ip_bound, const uint8_t*
+ }
+
+ #define MATCH_LONG_FAR(op, op_limit, len, distance) { \
+- if (BLOSCLZ_UNLIKELY((op) + 1 > (op_limit))) \
++ if (BLOSCLZ_UNLIKELY((op_limit) - (op) < 1)) \
+ goto out; \
+ *(op)++ = (7U << 5U) + 31; \
+ for ((len) -= 7; (len) >= 255; (len) -= 255) { \
+- if (BLOSCLZ_UNLIKELY((op) + 1 > (op_limit))) \
++ if (BLOSCLZ_UNLIKELY((op_limit) - (op) < 1)) \
+ goto out; \
+ *(op)++ = 255; \
+ } \
+- if (BLOSCLZ_UNLIKELY((op) + 4 > (op_limit))) \
++ if (BLOSCLZ_UNLIKELY((op_limit) - (op) < 4)) \
+ goto out; \
+ *(op)++ = (uint8_t)(len); \
+ *(op)++ = 255; \
+@@ -585,7 +585,7 @@ int blosclz_compress(const int clevel, const void* input, int length,
+ ip++;
+ }
+
+- if (BLOSCLZ_UNLIKELY(op + 1 > op_limit))
++ if (BLOSCLZ_UNLIKELY(op_limit - op < 1))
+ goto out;
+
+ /* assuming literal copy */
+@@ -594,7 +594,7 @@ int blosclz_compress(const int clevel, const void* input, int length,
+
+ /* left-over as literal copy */
+ while (BLOSCLZ_UNLIKELY(ip <= ip_bound)) {
+- if (BLOSCLZ_UNLIKELY(op + 2 > op_limit)) goto out;
++ if (BLOSCLZ_UNLIKELY(op_limit - op < 2)) goto out;
+ *op++ = *ip++;
+ copy++;
+ if (BLOSCLZ_UNLIKELY(copy == MAX_COPY)) {
+@@ -708,6 +708,9 @@ int blosclz_decompress(const void* input, int length, void* output, int maxout)
+ }
+ code = *ip++;
+ len += code;
++ if (BLOSCLZ_UNLIKELY(len > maxout)) {
++ return 0;
++ }
+ } while (code == 255);
+ }
+ else {
+@@ -731,11 +734,11 @@ int blosclz_decompress(const void* input, int length, void* output, int maxout)
+ }
+ }
+
+- if (BLOSCLZ_UNLIKELY(op + len > op_limit)) {
++ if (BLOSCLZ_UNLIKELY(len > op_limit - op)) {
+ return 0;
+ }
+
+- if (BLOSCLZ_UNLIKELY(ref - 1 < (uint8_t*)output)) {
++ if (BLOSCLZ_UNLIKELY(ref <= (uint8_t*)output)) {
+ return 0;
+ }
+
+@@ -772,10 +775,10 @@ int blosclz_decompress(const void* input, int length, void* output, int maxout)
+ else {
+ // literal
+ ctrl++;
+- if (BLOSCLZ_UNLIKELY(op + ctrl > op_limit)) {
++ if (BLOSCLZ_UNLIKELY(ctrl > op_limit - op)) {
+ return 0;
+ }
+- if (BLOSCLZ_UNLIKELY(ip + ctrl > ip_limit)) {
++ if (BLOSCLZ_UNLIKELY(ctrl > ip_limit - ip)) {
+ return 0;
+ }
+
+diff --git a/tests/test_blosclz_bounds.c b/tests/test_blosclz_bounds.c
+new file mode 100644
+index 000000000..45d8f5d02
+--- /dev/null
++++ b/tests/test_blosclz_bounds.c
+@@ -0,0 +1,204 @@
++/*********************************************************************
++ Blosc - Blocked Shuffling and Compression Library
++
++ Regression test for blosclz bounds checking and integer overflow.
++
++ Copyright (c) 2026 Blosc Development Team <blosc at blosc.org>
++ https://blosc.org
++ License: BSD 3-Clause (see LICENSE.txt)
++**********************************************************************/
++
++#include "test_common.h"
++#include "blosclz.h"
++
++static int test_blosclz_oversized_match(void) {
++ int maxout = 65536;
++ int input_len = 100000;
++ uint8_t* input = malloc((size_t)input_len);
++ uint8_t* output = malloc((size_t)maxout);
++ if (!input || !output) {
++ free(input);
++ free(output);
++ return EXIT_FAILURE;
++ }
++
++ /* Craft a blosclz stream:
++ 1 literal byte first, then match token 0xE0 (len_code=7), followed by many 0xFF bytes */
++ input[0] = 0; /* literal token (1 byte) */
++ input[1] = 'A'; /* 1 literal byte */
++ input[2] = 0xE0; /* match token with len = 7 - 1 */
++ memset(input + 3, 0xFF, (size_t)(input_len - 5));
++ input[input_len - 2] = 0x01; /* offset low byte */
++ input[input_len - 1] = 0x00; /* offset high byte */
++
++ int dsize = blosclz_decompress(input, input_len, output, maxout);
++ if (dsize != 0) {
++ printf("Expected blosclz_decompress to return 0 for oversized match, got %d\n", dsize);
++ free(input);
++ free(output);
++ return EXIT_FAILURE;
++ }
++
++ free(input);
++ free(output);
++ return EXIT_SUCCESS;
++}
++
++static int test_blosclz_oversized_literal(void) {
++ int maxout = 10;
++ uint8_t input[33];
++ uint8_t* output = malloc((size_t)maxout);
++ if (!output) {
++ return EXIT_FAILURE;
++ }
++
++ /* Literal ctrl = 31 -> 32 bytes literal, which exceeds maxout = 10 */
++ input[0] = 31;
++ memset(input + 1, 'X', 32);
++
++ int dsize = blosclz_decompress(input, 33, output, maxout);
++ if (dsize != 0) {
++ printf("Expected blosclz_decompress to return 0 for literal > maxout, got %d\n", dsize);
++ free(output);
++ return EXIT_FAILURE;
++ }
++
++ free(output);
++ return EXIT_SUCCESS;
++}
++
++static int test_blosclz_chunk_poc(void) {
++ int32_t blocksize = 65536;
++ int32_t cbytes = 100000;
++ uint8_t* chunk = calloc(1, (size_t)cbytes);
++ uint8_t* output = malloc((size_t)blocksize);
++ if (!chunk || !output) {
++ free(chunk);
++ free(output);
++ return EXIT_FAILURE;
++ }
++
++ /* Blosc2 chunk header */
++ chunk[0] = 2; /* Blosc format version */
++ chunk[1] = 4; /* Blosc2 format version */
++ chunk[2] = 0; /* Flags (no filter) */
++ chunk[3] = 1; /* Typesize */
++
++ /* nbytes */
++ chunk[4] = (uint8_t)(blocksize & 0xff);
++ chunk[5] = (uint8_t)((blocksize >> 8) & 0xff);
++ chunk[6] = (uint8_t)((blocksize >> 16) & 0xff);
++ chunk[7] = (uint8_t)((blocksize >> 24) & 0xff);
++
++ /* blocksize */
++ chunk[8] = (uint8_t)(blocksize & 0xff);
++ chunk[9] = (uint8_t)((blocksize >> 8) & 0xff);
++ chunk[10] = (uint8_t)((blocksize >> 16) & 0xff);
++ chunk[11] = (uint8_t)((blocksize >> 24) & 0xff);
++
++ /* cbytes */
++ chunk[12] = (uint8_t)(cbytes & 0xff);
++ chunk[13] = (uint8_t)((cbytes >> 8) & 0xff);
++ chunk[14] = (uint8_t)((cbytes >> 16) & 0xff);
++ chunk[15] = (uint8_t)((cbytes >> 24) & 0xff);
++
++ /* Codec is BLOSC_BLOSCLZ (0) at byte 20 */
++ chunk[20] = BLOSC_BLOSCLZ;
++
++ int header_len = BLOSC_EXTENDED_HEADER_LENGTH;
++ chunk[header_len] = 0; /* 1-byte literal ctrl */
++ chunk[header_len + 1] = 'A'; /* literal data */
++ chunk[header_len + 2] = 0xE0; /* match token */
++ memset(chunk + header_len + 3, 0xFF, (size_t)(cbytes - header_len - 5));
++ chunk[cbytes - 2] = 0x01;
++ chunk[cbytes - 1] = 0x00;
++
++ int dsize = blosc2_decompress(chunk, cbytes, output, blocksize);
++ if (dsize >= 0) {
++ printf("Expected blosc2_decompress to reject crafted chunk, got %d\n", dsize);
++ free(chunk);
++ free(output);
++ return EXIT_FAILURE;
++ }
++
++ free(chunk);
++ free(output);
++ return EXIT_SUCCESS;
++}
++
++static int test_blosclz_roundtrip_valid(void) {
++ const int size = 100000;
++ uint8_t* src = malloc((size_t)size);
++ uint8_t* compressed = malloc((size_t)size + 1000);
++ uint8_t* decompressed = malloc((size_t)size);
++
++ if (!src || !compressed || !decompressed) {
++ free(src);
++ free(compressed);
++ free(decompressed);
++ return EXIT_FAILURE;
++ }
++
++ for (int i = 0; i < size; i++) {
++ src[i] = (uint8_t)(i % 127);
++ }
++
++ int csize = blosclz_compress(5, src, size, compressed, size + 1000, NULL);
++ if (csize <= 0) {
++ printf("blosclz_compress failed: %d\n", csize);
++ free(src);
++ free(compressed);
++ free(decompressed);
++ return EXIT_FAILURE;
++ }
++
++ int dsize = blosclz_decompress(compressed, csize, decompressed, size);
++ if (dsize != size) {
++ printf("blosclz_decompress returned %d, expected %d\n", dsize, size);
++ free(src);
++ free(compressed);
++ free(decompressed);
++ return EXIT_FAILURE;
++ }
++
++ if (memcmp(src, decompressed, (size_t)size) != 0) {
++ printf("Decompressed data mismatch!\n");
++ free(src);
++ free(compressed);
++ free(decompressed);
++ return EXIT_FAILURE;
++ }
++
++ free(src);
++ free(compressed);
++ free(decompressed);
++ return EXIT_SUCCESS;
++}
++
++int main(void) {
++ blosc2_init();
++
++ if (test_blosclz_oversized_match() != EXIT_SUCCESS) {
++ blosc2_destroy();
++ return EXIT_FAILURE;
++ }
++
++ if (test_blosclz_oversized_literal() != EXIT_SUCCESS) {
++ blosc2_destroy();
++ return EXIT_FAILURE;
++ }
++
++ if (test_blosclz_chunk_poc() != EXIT_SUCCESS) {
++ blosc2_destroy();
++ return EXIT_FAILURE;
++ }
++
++ if (test_blosclz_roundtrip_valid() != EXIT_SUCCESS) {
++ blosc2_destroy();
++ return EXIT_FAILURE;
++ }
++
++ blosc2_destroy();
++ printf("All blosclz bounds tests passed successfully.\n");
++ return EXIT_SUCCESS;
++}
diff -Nru c-blosc2-2.23.1+ds/debian/patches/series c-blosc2-2.23.1+ds/debian/patches/series
--- c-blosc2-2.23.1+ds/debian/patches/series 2026-07-18 09:36:28.000000000 +0200
+++ c-blosc2-2.23.1+ds/debian/patches/series 2026-10-10 13:02:56.000000000 +0200
@@ -1,3 +1,4 @@
0001-Fix-build-flags.patch
0002-Link-shared-libzstd.patch
0003-Fix-privacy-breaches.patch
+0004-CVE-2026-103222.patch
More information about the debian-science-maintainers
mailing list