Package: release.debian.org
Severity: normal
Tags: trixie
X-Debbugs-Cc: [email protected]
Control: affects -1 + src:c-blosc2
User: [email protected]
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.
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 <[email protected]>  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 <[email protected]>
+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 <[email protected]>
++  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

Reply via email to