From 406360b3a8437f487dfd4732585f276c275649df Mon Sep 17 00:00:00 2001 From: Thomas Kowalski Date: Wed, 29 Apr 2026 11:23:42 +0200 Subject: [PATCH 1/2] fix: eliminate memory leak when parsing incorrect nested arrays --- msgpack/_unpacker.pyx | 1 + msgpack/unpack_template.h | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/msgpack/_unpacker.pyx b/msgpack/_unpacker.pyx index e25986ee..29cdec4a 100644 --- a/msgpack/_unpacker.pyx +++ b/msgpack/_unpacker.pyx @@ -322,6 +322,7 @@ cdef class Unpacker: self.buf = NULL def __dealloc__(self): + unpack_clear(&self.ctx) PyMem_Free(self.buf) self.buf = NULL diff --git a/msgpack/unpack_template.h b/msgpack/unpack_template.h index cce29e7a..42306618 100644 --- a/msgpack/unpack_template.h +++ b/msgpack/unpack_template.h @@ -72,6 +72,14 @@ static inline PyObject* unpack_data(unpack_context* ctx) static inline void unpack_clear(unpack_context *ctx) { + unsigned int i; + for (i = 1; i < ctx->top; i++) { + Py_CLEAR(ctx->stack[i].obj); + /* map_key holds a live reference only while waiting for the value */ + if (ctx->stack[i].ct == CT_MAP_VALUE) { + Py_CLEAR(ctx->stack[i].map_key); + } + } Py_CLEAR(ctx->stack[0].obj); } From 1b54a1ea667118c4d62ceeec1b5a1c052fe05321 Mon Sep 17 00:00:00 2001 From: Thomas Kowalski Date: Wed, 29 Apr 2026 21:29:27 +0200 Subject: [PATCH 2/2] test: add regression test --- test/test_except.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/test/test_except.py b/test/test_except.py index c56a6a30..d057f76b 100644 --- a/test/test_except.py +++ b/test/test_except.py @@ -1,6 +1,8 @@ #!/usr/bin/env python import datetime +import gc +import tracemalloc from pytest import raises @@ -80,6 +82,39 @@ def test_invalidvalue(): unpackb(b"\x91" * 3000) # nested fixarray(len=1) +def test_no_memory_leak_on_nested_invalid_tag() -> None: + """Regression test: unpacking nested arrays containing an invalid tag must not leak objects.""" + + kwargs: dict = { + "raw": False, + "strict_map_key": False, + "max_array_len": 1 << 20, + "max_map_len": 1 << 20, + } + n = 1000 + + for depth in range(1, 15): + data = bytes([0x91] * depth + [0xC1]) + + gc.collect() + tracemalloc.start() + s1 = tracemalloc.take_snapshot() + + for _ in range(n): + try: + unpackb(data, **kwargs) + except Exception: + pass + + gc.collect() + s2 = tracemalloc.take_snapshot() + tracemalloc.stop() + + leaked = sum(s.count_diff for s in s2.compare_to(s1, "lineno") if s.count_diff > 0) + per_call = leaked / n + assert per_call < 1.0, f"depth={depth}: {per_call:.2f} leaked objects/call (expected < 1)" + + def test_strict_map_key(): valid = {"unicode": 1, b"bytes": 2} packed = packb(valid, use_bin_type=True)