From 5867f192dade717c4bdd1fe63d438dc2548c5b2f Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Tue, 31 Mar 2026 22:55:43 -0300 Subject: [PATCH 1/2] fix: release buffers and free memory on error paths Fix memory and buffer leaks on five error paths: - block/compress: release source and dict Py_buffers on PyMem_Malloc failure - block/decompress: release source and dict Py_buffers on PyMem_Malloc failure - frame/compress: release source Py_buffer on block_checksum version error - frame/compress_begin: free destination buffer on LZ4F_compressBegin failure (same pattern as the bug fixed in 43fe65d, not propagated to this site) - frame/__decompress: free original destination buffer on PyMem_Realloc failure (realloc returns NULL without freeing the original pointer) Found using cext-review-toolkit (https://github.com/devdanzin/cext-review-toolkit). Co-Authored-By: Claude Opus 4.6 (1M context) --- lz4/block/_block.c | 4 ++++ lz4/frame/_frame.c | 3 +++ 2 files changed, 7 insertions(+) diff --git a/lz4/block/_block.c b/lz4/block/_block.c index 993cc44..4b7dc38 100644 --- a/lz4/block/_block.c +++ b/lz4/block/_block.c @@ -215,6 +215,8 @@ compress (PyObject * Py_UNUSED (self), PyObject * args, PyObject * kwargs) dest = PyMem_Malloc (total_size * sizeof * dest); if (dest == NULL) { + PyBuffer_Release(&source); + PyBuffer_Release(&dict); return PyErr_NoMemory(); } @@ -349,6 +351,8 @@ decompress (PyObject * Py_UNUSED (self), PyObject * args, PyObject * kwargs) dest = PyMem_Malloc (dest_size * sizeof * dest); if (dest == NULL) { + PyBuffer_Release(&source); + PyBuffer_Release(&dict); return PyErr_NoMemory(); } diff --git a/lz4/frame/_frame.c b/lz4/frame/_frame.c index 440b0b5..eb1c76b 100644 --- a/lz4/frame/_frame.c +++ b/lz4/frame/_frame.c @@ -184,6 +184,7 @@ compress (PyObject * Py_UNUSED (self), PyObject * args, } else if (block_checksum) { + PyBuffer_Release(&source); PyErr_SetString (PyExc_RuntimeError, "block_checksum specified but not supported by LZ4 library version"); return NULL; @@ -383,6 +384,7 @@ compress_begin (PyObject * Py_UNUSED (self), PyObject * args, if (LZ4F_isError (result)) { + PyMem_Free (destination); PyErr_Format (PyExc_RuntimeError, "LZ4F_compressBegin failed with code: %s", LZ4F_getErrorName (result)); @@ -1115,6 +1117,7 @@ __decompress(LZ4F_dctx * context, char * source, size_t source_size, buff = PyMem_Realloc (destination, destination_size); if (buff == NULL) { + PyMem_Free (destination); PyErr_SetString (PyExc_RuntimeError, "Failed to resize buffer"); return NULL; From 0957fe173e3ca994d6388707068a4f7410ada910 Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Tue, 31 Mar 2026 22:57:51 -0300 Subject: [PATCH 2/2] fix: correct PyArg_ParseTuple format and non-NULL return with exception in stream module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix _compress_bound: format "OI" expects (PyObject*, unsigned int) but only &input_size is provided. The "O" writes a PyObject* into a uint32_t — type mismatch and memory corruption on every call. Change to "I" to match the identical _input_bound function at line 1091. - Fix _compress: when update_context_after_process fails, PyErr_Format sets an exception but goto exit_now returns the already-created py_dest (non-NULL). This violates the Python/C API contract (returning a result with an exception set), causing SystemError. Clear py_dest before the goto. Found using cext-review-toolkit (https://github.com/devdanzin/cext-review-toolkit). Co-Authored-By: Claude Opus 4.6 (1M context) --- lz4/stream/_stream.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lz4/stream/_stream.c b/lz4/stream/_stream.c index 4c51d89..e92f8a8 100644 --- a/lz4/stream/_stream.c +++ b/lz4/stream/_stream.c @@ -1063,7 +1063,7 @@ _compress_bound (PyObject * Py_UNUSED (self), PyObject * args) /* Positional arguments: input_size * Keyword arguments : none */ - if (!PyArg_ParseTuple (args, "OI", &input_size)) + if (!PyArg_ParseTuple (args, "I", &input_size)) { goto exit_now; } @@ -1211,6 +1211,7 @@ _compress (PyObject * Py_UNUSED (self), PyObject * args) if (context->strategy.ops->update_context_after_process (context) != 0) { + Py_CLEAR (py_dest); PyErr_Format (PyExc_RuntimeError, "Internal error"); goto exit_now; }