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

