From 9b64d4f859f83809471f58ab298b31fb8c16695c Mon Sep 17 00:00:00 2001 From: Igor Stadnyk Date: Sun, 16 Aug 2026 05:22:35 +0100 Subject: [PATCH] Fix empty Blosc buffer round-trips --- docs/release.md | 6 ++++++ src/numcodecs/blosc.pyx | 14 +++++++++++--- src/numcodecs/meson.build | 6 +++++- tests/test_blosc.py | 8 ++++++++ 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/docs/release.md b/docs/release.md index 78e8901c..2996de5a 100644 --- a/docs/release.md +++ b/docs/release.md @@ -14,6 +14,12 @@ ## Unreleased +### Fixes + +* Validate Blosc frames before decompression, allowing valid zero-length frames to round-trip + while rejecting truncated or size-mismatched frames. System Blosc builds now require version + 1.16.0 or newer. By {user}`Igor Stadnyk `, {issue}`831`. + ### Maintenance * **Migrate build system from setuptools/setup.py to meson-python.** This replaces the diff --git a/src/numcodecs/blosc.pyx b/src/numcodecs/blosc.pyx index d61f7358..75b15be4 100644 --- a/src/numcodecs/blosc.pyx +++ b/src/numcodecs/blosc.pyx @@ -49,6 +49,7 @@ cdef extern from "blosc.h": int numinternalthreads) nogil int blosc_decompress_ctx(const void* src, void* dest, size_t destsize, int numinternalthreads) nogil + int blosc_cbuffer_validate(const void* cbuffer, size_t cbytes, size_t* nbytes) void blosc_cbuffer_sizes(const void* cbuffer, size_t* nbytes, size_t* cbytes, size_t* blocksize) char* blosc_cbuffer_complib(const void* cbuffer) @@ -350,7 +351,7 @@ def decompress(source, dest=None): memoryview dest_mv Py_buffer* dest_pb char* dest_ptr - size_t nbytes, cbytes, blocksize + size_t nbytes # obtain source memoryview source_mv = ensure_contiguous_memoryview(source) @@ -359,8 +360,10 @@ def decompress(source, dest=None): # get source pointer source_ptr = source_pb.buf - # determine buffer size - blosc_cbuffer_sizes(source_ptr, &nbytes, &cbytes, &blocksize) + # validate source and determine decompressed buffer size + ret = blosc_cbuffer_validate(source_ptr, source_pb.len, &nbytes) + if ret != 0: + raise RuntimeError('error during blosc decompression: %d' % ret) # setup destination buffer if dest is None: @@ -382,6 +385,11 @@ def decompress(source, dest=None): raise ValueError('destination buffer too small; expected at least %s, ' 'got %s' % (nbytes, dest_nbytes)) + # Blosc reports the number of decompressed bytes, so zero is successful + # for a validated empty frame rather than an error. + if nbytes == 0: + return dest + # perform decompression if _get_use_threads(): # allow blosc to use threads internally diff --git a/src/numcodecs/meson.build b/src/numcodecs/meson.build index eca2b2c6..117b67b6 100644 --- a/src/numcodecs/meson.build +++ b/src/numcodecs/meson.build @@ -153,7 +153,11 @@ zlib_dep = declare_dependency( ) # --- Vendored blosc --- -blosc_dep = dependency('blosc', required: get_option('system_blosc')) +blosc_dep = dependency( + 'blosc', + version: '>=1.16.0', + required: get_option('system_blosc'), +) if not blosc_dep.found() blosc_sources = files( diff --git a/tests/test_blosc.py b/tests/test_blosc.py index 537dc350..dc032119 100644 --- a/tests/test_blosc.py +++ b/tests/test_blosc.py @@ -75,6 +75,14 @@ def test_encode_decode(array, codec): check_encode_decode(array, codec) +def test_empty_encode_decode(use_threads): + blosc.use_threads = use_threads + try: + check_encode_decode(np.empty(0, dtype='u1'), Blosc()) + finally: + blosc.use_threads = None + + def test_config(): codec = Blosc(cname='zstd', clevel=3, shuffle=1) check_config(codec)