From 9f4909c8709cfff635f0e7f107a155a2f9637024 Mon Sep 17 00:00:00 2001 From: "Michael R. Crusoe" <1330696+mr-c@users.noreply.github.com> Date: Fri, 28 Aug 2026 07:01:54 +0200 Subject: [PATCH 1/6] wheels: also build for pyodide using emscripten. (#732) --- .github/workflows/wheel.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/wheel.yml b/.github/workflows/wheel.yml index 6250cfd0..ef707d1b 100644 --- a/.github/workflows/wheel.yml +++ b/.github/workflows/wheel.yml @@ -21,6 +21,9 @@ jobs: - os: ubuntu-24.04 cibw_archs: riscv64 name_suffix: "-riscv64" + - os: ubuntu-24.04-arm + cibw_platform: pyodide + name_suffix: "-pyodide" runs-on: ${{ matrix.os }} name: Build wheels on ${{ matrix.os }}${{ matrix.name_suffix || '' }} @@ -50,6 +53,7 @@ jobs: CIBW_TEST_COMMAND: "pytest {package}/test" CIBW_SKIP: "cp38-* cp39-* cp310-win_arm64" CIBW_ARCHS: ${{ matrix.cibw_archs || 'auto' }} + CIBW_PLATFORM: ${{ matrix.cibw_platform || 'auto' }} - name: Build sdist if: runner.os == 'Linux' && runner.arch == 'X64' From 7a6392014dd248a03539f043b13d4eaa25b2fcef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez=20Mondrag=C3=B3n?= <16805946+edgarrmondragon@users.noreply.github.com> Date: Wed, 2 Sep 2026 10:23:48 -0600 Subject: [PATCH 2/6] Support packing Python 3.15 `frozentdict` (PEP 814) (#738) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Support packing a Python 3.15 `frozendict`. This PR doesn't support _unpacking_ into a `frozendict` other than with the generic `object_pairs_hook=frozendict`. --------- Signed-off-by: Edgar Ramírez Mondragón Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- msgpack/_packer.pyx | 3 ++- msgpack/fallback.py | 14 +++++++++++++- pyproject.toml | 2 +- requirements.txt | 2 +- test/test_pack.py | 20 ++++++++++++++++++++ 5 files changed, 37 insertions(+), 4 deletions(-) diff --git a/msgpack/_packer.pyx b/msgpack/_packer.pyx index ca6cf983..52f4f1f2 100644 --- a/msgpack/_packer.pyx +++ b/msgpack/_packer.pyx @@ -4,6 +4,7 @@ from cpython.datetime cimport ( PyDateTime_CheckExact, PyDelta_CheckExact, datetime_tzinfo, timedelta_days, timedelta_seconds, timedelta_microseconds, ) +from cpython.frozendict cimport PyAnyDict_Check, PyAnyDict_CheckExact cdef ExtType cdef Timestamp @@ -202,7 +203,7 @@ cdef class Packer: rawval = o msgpack_pack_raw(&self.pk, L) msgpack_pack_raw_body(&self.pk, rawval, L) - elif PyDict_CheckExact(o) if strict else PyDict_Check(o): + elif PyAnyDict_CheckExact(o) if strict else PyAnyDict_Check(o): L = len(o) if L > ITEM_LIMIT: raise ValueError("dict is too large") diff --git a/msgpack/fallback.py b/msgpack/fallback.py index e219786e..9cc0320e 100644 --- a/msgpack/fallback.py +++ b/msgpack/fallback.py @@ -37,6 +37,17 @@ def newlist_hint(size): return [] +# frozendict is a builtin type added in Python 3.15 (PEP 814). +if sys.version_info >= (3, 15): + import builtins + + frozendict = builtins.frozendict +else: + + class frozendict: + pass + + from .exceptions import BufferFull, ExtraData, FormatError, OutOfData, StackError from .ext import ExtType, Timestamp @@ -695,6 +706,7 @@ def _pack( list_types = list else: list_types = (list, tuple) + dict_types = (dict, frozendict) while True: if nest_limit < 0: raise ValueError("recursion limit exceeded") @@ -788,7 +800,7 @@ def _pack( for i in range(n): self._pack(obj[i], nest_limit - 1) return - if check(obj, dict): + if check(obj, dict_types): return self._pack_map_pairs(len(obj), obj.items(), nest_limit - 1) if self._datetime and check(obj, _DateTime) and obj.tzinfo is not None: diff --git a/pyproject.toml b/pyproject.toml index ebc6b50d..aee0d5ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,6 @@ lint.select = [ [dependency-groups] dev = [ - "cython>=3.2.5", + "cython>=3.3.0", "pytest>=9.0.3", ] diff --git a/requirements.txt b/requirements.txt index 7991e3f0..5e3122f6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -cython==3.2.5 +cython==3.3.0 setuptools==78.1.1 pytest build diff --git a/test/test_pack.py b/test/test_pack.py index f44bd557..9ca6e182 100644 --- a/test/test_pack.py +++ b/test/test_pack.py @@ -1,6 +1,7 @@ #!/usr/bin/env python import struct +import sys from collections import OrderedDict from io import BytesIO @@ -162,6 +163,25 @@ def pair_hook(seq): assert unpackb(packb(od), object_pairs_hook=pair_hook, use_list=1) == seq +@pytest.mark.skipif(sys.version_info < (3, 15), reason="frozendict requires Python 3.15+") +def test_frozendict(): + import builtins + + fd = builtins.frozendict(a=1, b=[1, 2, 3]) + unpacked = unpackb(packb(fd), use_list=1) + assert isinstance(unpacked, dict) + assert unpacked == fd + + unpacked = unpackb(packb(fd), use_list=1, object_pairs_hook=builtins.frozendict) + assert isinstance(unpacked, builtins.frozendict) + assert unpacked == fd + + packer = Packer(strict_types=True) + unpacked = unpackb(packer.pack(fd), use_list=1) + assert isinstance(unpacked, dict) + assert unpacked == fd + + def test_pairlist(): pairlist = [(b"a", 1), (2, b"b"), (b"foo", b"bar")] packer = Packer() From 0014ca934b0b1f507d5364528422811fa8be9f1a Mon Sep 17 00:00:00 2001 From: Jake Wang Date: Sun, 20 Sep 2026 02:44:18 -0400 Subject: [PATCH 3/6] Reject negative array and map header sizes (#742) --- msgpack/_packer.pyx | 4 ++++ msgpack/fallback.py | 4 ++++ test/test_limits.py | 10 ++++++++++ 3 files changed, 18 insertions(+) diff --git a/msgpack/_packer.pyx b/msgpack/_packer.pyx index 52f4f1f2..c44c6366 100644 --- a/msgpack/_packer.pyx +++ b/msgpack/_packer.pyx @@ -300,6 +300,8 @@ cdef class Packer: @cython.critical_section def pack_array_header(self, long long size): self._check_exports() + if size < 0: + raise ValueError("array size must be non-negative") if size > ITEM_LIMIT: raise ValueError("array too large") msgpack_pack_array(&self.pk, size) @@ -311,6 +313,8 @@ cdef class Packer: @cython.critical_section def pack_map_header(self, long long size): self._check_exports() + if size < 0: + raise ValueError("map size must be non-negative") if size > ITEM_LIMIT: raise ValueError("map too learge") msgpack_pack_map(&self.pk, size) diff --git a/msgpack/fallback.py b/msgpack/fallback.py index 9cc0320e..569650af 100644 --- a/msgpack/fallback.py +++ b/msgpack/fallback.py @@ -837,6 +837,8 @@ def pack_map_pairs(self, pairs): return ret def pack_array_header(self, n): + if n < 0: + raise ValueError("array size must be non-negative") if n >= 2**32: raise ValueError self._pack_array_header(n) @@ -846,6 +848,8 @@ def pack_array_header(self, n): return ret def pack_map_header(self, n): + if n < 0: + raise ValueError("map size must be non-negative") if n >= 2**32: raise ValueError self._pack_map_header(n) diff --git a/test/test_limits.py b/test/test_limits.py index 5aa47ba0..2099b916 100644 --- a/test/test_limits.py +++ b/test/test_limits.py @@ -52,6 +52,16 @@ def test_map_header(): packer.pack_array_header(2**32) +@pytest.mark.parametrize("method", ["pack_array_header", "pack_map_header"]) +@pytest.mark.parametrize("size", [-1, -(2**32)]) +def test_negative_header_size(method, size): + packer = Packer(autoreset=False) + packer.pack("existing") + with pytest.raises(PackValueError, match="size must be non-negative"): + getattr(packer, method)(size) + assert packer.bytes() == packb("existing") + + def test_max_str_len(): d = "x" * 3 packed = packb(d) From 475c1b774adb873483095f5d7f11aa007c9f1519 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 13:11:26 +0900 Subject: [PATCH 4/6] Bump the all-dependencies group across 1 directory with 2 updates (#741) --- .github/workflows/wheel.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheel.yml b/.github/workflows/wheel.yml index ef707d1b..5459924b 100644 --- a/.github/workflows/wheel.yml +++ b/.github/workflows/wheel.yml @@ -42,12 +42,12 @@ jobs: - name: Set up QEMU for emulation if: matrix.cibw_archs == 'riscv64' - uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 + uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0 with: platforms: ${{ matrix.cibw_archs }} - name: Build - uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0 + uses: pypa/cibuildwheel@e090b81e30c4d855ea63bf4b6e59204c09a101ae # v4.2.1 env: CIBW_TEST_REQUIRES: "pytest" CIBW_TEST_COMMAND: "pytest {package}/test" From 620b0eb4c89cd72d4098564537624d9dbb2ce141 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 29 Sep 2026 10:27:32 +0900 Subject: [PATCH 5/6] Merge commit from fork --- msgpack/_unpacker.pyx | 45 +++++++++++++---- msgpack/unpack.h | 34 +++++++------ msgpack/unpack_container_header.h | 3 +- msgpack/unpack_template.h | 81 +++++++++++++++++-------------- 4 files changed, 101 insertions(+), 62 deletions(-) diff --git a/msgpack/_unpacker.pyx b/msgpack/_unpacker.pyx index 7e406622..f4b89d43 100644 --- a/msgpack/_unpacker.pyx +++ b/msgpack/_unpacker.pyx @@ -40,20 +40,22 @@ cdef extern from "unpack.h": Py_ssize_t max_map_len Py_ssize_t max_ext_len + ctypedef struct unpack_context + ctypedef int (*execute_fn)(unpack_context* ctx, const char* data, + Py_ssize_t len, Py_ssize_t* off) except -1 + ctypedef struct unpack_context: msgpack_user user - PyObject* obj - Py_ssize_t count + execute_fn execute - ctypedef int (*execute_fn)(unpack_context* ctx, const char* data, - Py_ssize_t len, Py_ssize_t* off) except -1 execute_fn unpack_construct execute_fn unpack_skip execute_fn read_array_header execute_fn read_map_header void unpack_init(unpack_context* ctx) - object unpack_data(unpack_context* ctx) + # Transfers the result's owned reference to Cython and clears its slot. + object unpack_take_result(unpack_context* ctx) void unpack_clear(unpack_context* ctx) cdef inline init_ctx(unpack_context *ctx, @@ -191,16 +193,16 @@ def unpackb(object packed, *, object object_hook=None, object list_hook=None, max_str_len, max_bin_len, max_array_len, max_map_len, max_ext_len) ret = unpack_construct(&ctx, buf, buf_len, &off) if ret == 1: - obj = unpack_data(&ctx) + obj = unpack_take_result(&ctx) if off < buf_len: # buf may point into a temporary contiguous copy owned by view, # so the extra data must be copied out before releasing view. raise ExtraData(obj, PyBytes_FromStringAndSize(buf+off, buf_len-off)) return obj finally: + unpack_clear(&ctx) PyBuffer_Release(&view); - unpack_clear(&ctx) if ret == 0: raise ValueError("Unpack failed: incomplete input") elif ret == -2: @@ -214,6 +216,11 @@ def unpackb(object packed, *, object object_hook=None, object list_hook=None, cdef class Unpacker: """Streaming unpacker. + If an operation needs more data, resume it with the same method after + feeding more bytes. ``unpack()`` and iteration may be used interchangeably. + Switching between unpacking, skipping, header readers, or ``read_bytes()`` + while an object is incomplete raises ``ValueError`` in the C extension. + Arguments: :param file_like: @@ -339,6 +346,9 @@ cdef class Unpacker: Py_ssize_t max_ext_len=-1): cdef const char *cerr=NULL + if self._unpacking: + raise RuntimeError("Unpacker.__init__() cannot be called while unpacking is in progress") + unpack_clear(&self.ctx) unpack_init(&self.ctx) if self.buf != NULL: @@ -473,18 +483,29 @@ cdef class Unpacker: cdef object obj cdef Py_ssize_t prev_head + if self._unpacking: + raise RuntimeError("Unpacker cannot be called recursively while unpacking is in progress") + if self.ctx.execute != NULL and self.ctx.execute != execute: + raise ValueError("Cannot switch unpacking methods while an object is incomplete") + self._unpacking = True try: while 1: prev_head = self.buf_head if prev_head < self.buf_tail: - ret = execute(&self.ctx, self.buf, self.buf_tail, &self.buf_head) - self.stream_offset += self.buf_head - prev_head + self.ctx.execute = execute + try: + ret = execute(&self.ctx, self.buf, self.buf_tail, &self.buf_head) + except: + unpack_clear(&self.ctx) + raise + finally: + self.stream_offset += self.buf_head - prev_head else: ret = 0 if ret == 1: - obj = unpack_data(&self.ctx) + obj = unpack_take_result(&self.ctx) unpack_init(&self.ctx) return obj if ret == 0: @@ -510,6 +531,10 @@ cdef class Unpacker: def read_bytes(self, Py_ssize_t nbytes): """Read a specified number of raw bytes from the stream""" cdef Py_ssize_t nread + if self._unpacking: + raise RuntimeError("Unpacker.read_bytes() cannot be called while unpacking is in progress") + if self.ctx.execute != NULL: + raise ValueError("Cannot switch unpacking methods while an object is incomplete") nread = min(self.buf_tail - self.buf_head, nbytes) ret = PyBytes_FromStringAndSize(self.buf + self.buf_head, nread) self.buf_head += nread diff --git a/msgpack/unpack.h b/msgpack/unpack.h index 774354b6..a7acc16c 100644 --- a/msgpack/unpack.h +++ b/msgpack/unpack.h @@ -35,11 +35,13 @@ typedef struct unpack_user { Py_ssize_t max_str_len, max_bin_len, max_array_len, max_map_len, max_ext_len; } unpack_user; -typedef PyObject* msgpack_unpack_object; +typedef PyObject* msgpack_unpack_object; //TODO: remove typedef and use PyObject* directly struct unpack_context; typedef struct unpack_context unpack_context; typedef int (*execute_fn)(unpack_context *ctx, const char* data, Py_ssize_t len, Py_ssize_t* off); +/* Constructors receive a NULL output slot and store an owned reference on + * success. On failure the slot remains NULL. */ static inline int unpack_callback_uint16(unpack_user* u, uint16_t d, msgpack_unpack_object* o) { PyObject *p = PyLong_FromLong((long)d); @@ -137,6 +139,7 @@ static inline int unpack_callback_array(unpack_user* u, unsigned int n, msgpack_ return 0; } +/* Consumes o on both success and failure; borrows the container. */ static inline int unpack_callback_array_item(unpack_user* u, unsigned int current, msgpack_unpack_object* c, msgpack_unpack_object o) { if (u->use_list) @@ -146,14 +149,16 @@ static inline int unpack_callback_array_item(unpack_user* u, unsigned int curren return 0; } +/* Replaces the owned container reference with the hook result, or NULL on + * failure. The original reference is consumed in either case. */ static inline int unpack_callback_array_end(unpack_user* u, msgpack_unpack_object* c) { if (u->list_hook) { PyObject *new_c = PyObject_CallFunctionObjArgs(u->list_hook, *c, NULL); - if (!new_c) - return -1; Py_DECREF(*c); *c = new_c; + if (!new_c) + return -1; } return 0; } @@ -177,10 +182,13 @@ static inline int unpack_callback_map(unpack_user* u, unsigned int n, msgpack_un return 0; } +/* Consumes k and v on both success and failure; borrows the container. */ static inline int unpack_callback_map_item(unpack_user* u, unsigned int current, msgpack_unpack_object* c, msgpack_unpack_object k, msgpack_unpack_object v) { if (u->strict_map_key && !PyUnicode_CheckExact(k) && !PyBytes_CheckExact(k)) { PyErr_Format(PyExc_ValueError, "%.100s is not allowed for map key when strict_map_key=True", Py_TYPE(k)->tp_name); + Py_DECREF(k); + Py_DECREF(v); return -1; } if (PyUnicode_CheckExact(k)) { @@ -188,30 +196,28 @@ static inline int unpack_callback_map_item(unpack_user* u, unsigned int current, } if (u->has_pairs_hook) { msgpack_unpack_object item = PyTuple_Pack(2, k, v); - if (!item) - return -1; Py_DECREF(k); Py_DECREF(v); + if (!item) + return -1; PyList_SET_ITEM(*c, current, item); return 0; } - else if (PyDict_SetItem(*c, k, v) == 0) { - Py_DECREF(k); - Py_DECREF(v); - return 0; - } - return -1; + int ret = PyDict_SetItem(*c, k, v); + Py_DECREF(k); + Py_DECREF(v); + return ret; } +/* Same ownership contract as unpack_callback_array_end. */ static inline int unpack_callback_map_end(unpack_user* u, msgpack_unpack_object* c) { if (u->object_hook) { PyObject *new_c = PyObject_CallFunctionObjArgs(u->object_hook, *c, NULL); - if (!new_c) - return -1; - Py_DECREF(*c); *c = new_c; + if (!new_c) + return -1; } return 0; } diff --git a/msgpack/unpack_container_header.h b/msgpack/unpack_container_header.h index e48d43ba..015e8b5e 100644 --- a/msgpack/unpack_container_header.h +++ b/msgpack/unpack_container_header.h @@ -46,9 +46,8 @@ static inline int unpack_container_header(unpack_context* ctx, const char* data, return -1; } - if (unpack_callback_uint32(&ctx->user, size, &ctx->stack[0].obj) < 0) + if (unpack_callback_uint32(&ctx->user, size, &ctx->result) < 0) return -1; return 1; } - diff --git a/msgpack/unpack_template.h b/msgpack/unpack_template.h index 797a2f91..5f0709a4 100644 --- a/msgpack/unpack_template.h +++ b/msgpack/unpack_template.h @@ -23,6 +23,7 @@ #endif typedef struct unpack_stack { + /* Each non-NULL pointer owns a reference. Moving it clears the source. */ PyObject* obj; Py_ssize_t size; Py_ssize_t count; @@ -35,32 +36,40 @@ struct unpack_context { unsigned int cs; unsigned int trail; unsigned int top; + /* Completed result and the operation to resume after incomplete input. */ + PyObject* result; + execute_fn execute; unpack_stack stack[MSGPACK_EMBED_STACK_SIZE]; }; +static inline PyObject* unpack_take_ref(PyObject** slot) +{ + PyObject* obj = *slot; + *slot = NULL; + return obj; +} static inline void unpack_init(unpack_context* ctx) { ctx->cs = CS_HEADER; ctx->trail = 0; ctx->top = 0; - ctx->stack[0].obj = NULL; + ctx->result = NULL; + ctx->execute = NULL; } -static inline PyObject* unpack_data(unpack_context* ctx) +static inline PyObject* unpack_take_result(unpack_context* ctx) { - return (ctx)->stack[0].obj; + return unpack_take_ref(&ctx->result); } static inline void unpack_clear(unpack_context *ctx) { for (unsigned int i = 0; i < ctx->top; i++) { - /* 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[i].map_key); Py_CLEAR(ctx->stack[i].obj); } + Py_CLEAR(ctx->result); unpack_init(ctx); } @@ -78,22 +87,20 @@ static inline int unpack_execute(bool construct, unpack_context* ctx, const char unpack_stack* stack = ctx->stack; unpack_user* user = &ctx->user; + /* The pending value owns a reference until moved to a frame or callback. */ PyObject* obj = NULL; unpack_stack* c = NULL; int ret; -#define construct_cb(name) \ - construct && unpack_callback ## name - #define push_simple_value(func) \ - if(construct_cb(func)(user, &obj) < 0) { goto _failed; } \ + if(construct && unpack_callback##func(user, &obj) < 0) { goto _failed; } \ goto _push #define push_fixed_value(func, arg) \ - if(construct_cb(func)(user, arg, &obj) < 0) { goto _failed; } \ + if(construct && unpack_callback##func(user, arg, &obj) < 0) { goto _failed; } \ goto _push #define push_variable_value(func, base, pos, len) \ - if(construct_cb(func)(user, \ + if(construct && unpack_callback##func(user, \ (const char*)base, (const char*)pos, len, &obj) < 0) { goto _failed; } \ goto _push @@ -109,10 +116,12 @@ static inline int unpack_execute(bool construct, unpack_context* ctx, const char #define start_container(func, count_, ct_) \ if(top >= MSGPACK_EMBED_STACK_SIZE) { ret = -3; goto _end; } \ - if(construct_cb(func)(user, count_, &stack[top].obj) < 0) { goto _failed; } \ - if((count_) == 0) { obj = stack[top].obj; \ - if (construct_cb(func##_end)(user, &obj) < 0) { goto _failed; } \ + if(construct && unpack_callback##func(user, count_, &obj) < 0) { goto _failed; } \ + if((count_) == 0) { \ + if (construct && unpack_callback##func##_end(user, &obj) < 0) { goto _failed; } \ goto _push; } \ + stack[top].obj = unpack_take_ref(&obj); \ + stack[top].map_key = NULL; \ stack[top].ct = ct_; \ stack[top].size = count_; \ stack[top].count = 0; \ @@ -308,26 +317,32 @@ static inline int unpack_execute(bool construct, unpack_context* ctx, const char c = &stack[top-1]; switch(c->ct) { case CT_ARRAY_ITEM: - if(construct_cb(_array_item)(user, c->count, &c->obj, obj) < 0) { goto _failed; } + if (construct) { + if (unpack_callback_array_item(user, c->count, &c->obj, + unpack_take_ref(&obj)) < 0) { goto _failed; } + } if(++c->count == c->size) { - obj = c->obj; - if (construct_cb(_array_end)(user, &obj) < 0) { goto _failed; } + obj = unpack_take_ref(&c->obj); --top; + if (construct && unpack_callback_array_end(user, &obj) < 0) { goto _failed; } /*printf("stack pop %d\n", top);*/ goto _push; } goto _header_again; case CT_MAP_KEY: - c->map_key = obj; + c->map_key = unpack_take_ref(&obj); c->ct = CT_MAP_VALUE; goto _header_again; case CT_MAP_VALUE: - if(construct_cb(_map_item)(user, c->count, &c->obj, c->map_key, obj) < 0) { goto _failed; } - c->map_key = NULL; + if (construct) { + if (unpack_callback_map_item(user, c->count, &c->obj, + unpack_take_ref(&c->map_key), + unpack_take_ref(&obj)) < 0) { goto _failed; } + } if(++c->count == c->size) { - obj = c->obj; - if (construct_cb(_map_end)(user, &obj) < 0) { goto _failed; } + obj = unpack_take_ref(&c->obj); --top; + if (construct && unpack_callback_map_end(user, &obj) < 0) { goto _failed; } /*printf("stack pop %d\n", top);*/ goto _push; } @@ -340,6 +355,7 @@ static inline int unpack_execute(bool construct, unpack_context* ctx, const char } _header_again: + assert(obj == NULL); cs = CS_HEADER; ++p; } while(p != pe); @@ -349,7 +365,7 @@ static inline int unpack_execute(bool construct, unpack_context* ctx, const char _finish: if (!construct) unpack_callback_nil(user, &obj); - stack[0].obj = obj; + ctx->result = unpack_take_ref(&obj); ++p; ret = 1; /*printf("-- finish --\n"); */ @@ -361,17 +377,18 @@ static inline int unpack_execute(bool construct, unpack_context* ctx, const char goto _end; _out: + assert(obj == NULL); ret = 0; goto _end; _end: + Py_XDECREF(obj); ctx->cs = cs; ctx->trail = trail; ctx->top = top; *off = p - (const unsigned char*)data; return ret; -#undef construct_cb } #undef NEXT_CS @@ -387,18 +404,10 @@ static inline int unpack_execute(bool construct, unpack_context* ctx, const char #undef start_container static int unpack_construct(unpack_context *ctx, const char *data, Py_ssize_t len, Py_ssize_t *off) { - int ret = unpack_execute(1, ctx, data, len, off); - if (ret == -1) { - unpack_clear(ctx); - } - return ret; + return unpack_execute(1, ctx, data, len, off); } static int unpack_skip(unpack_context *ctx, const char *data, Py_ssize_t len, Py_ssize_t *off) { - int ret = unpack_execute(0, ctx, data, len, off); - if (ret == -1) { - unpack_clear(ctx); - } - return ret; + return unpack_execute(0, ctx, data, len, off); } #define unpack_container_header read_array_header From 742cb6d7ad141343edcccc829b5b06d0c6f8fcb5 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 29 Sep 2026 10:31:31 +0900 Subject: [PATCH 6/6] release v1.2.3 --- CHANGELOG.md | 20 ++++++++++++++++++++ msgpack/__init__.py | 4 ++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90ffac1b..323543c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +# 1.2.3 + +Release Date: 2026-09-29 + +- Fix memory leaks in the C extension when map key validation or container hooks fail. + See [GHSA-j586-36cw-2gc2](https://github.com/msgpack/msgpack-python/security/advisories/GHSA-j586-36cw-2gc2). +- Fix use-after-free errors when cleaning up an incomplete or failed `Unpacker.skip()` + after previously unpacking a nested object in the C extension. +- Raise `ValueError` in the C extension when switching between unpacking, skipping, + header readers, or `read_bytes()` while an object is incomplete. Resume with the + same method after feeding more data; `unpack()` and iteration remain interchangeable. +- Prevent reentrant unpacking, header reads, `read_bytes()`, and reinitialization + of the same C `Unpacker` while unpacking is in progress. +- Reject negative sizes in `Packer.pack_array_header()` and `Packer.pack_map_header()`. #742 +- Support packing Python 3.15 `frozendict` objects. #738 +- Update Cython from 3.2.5 to 3.3.0. #738 +- Build wheels for Pyodide using Emscripten. #732 +- Update cibuildwheel to 4.2.1 and the QEMU setup action to 4.4.0. #741 + + # 1.2.2 Release Date: 2026-08-27 diff --git a/msgpack/__init__.py b/msgpack/__init__.py index a2ee5669..0947863f 100644 --- a/msgpack/__init__.py +++ b/msgpack/__init__.py @@ -4,8 +4,8 @@ from .exceptions import * # noqa: F403 from .ext import ExtType, Timestamp -version = (1, 2, 2) -__version__ = "1.2.2" +version = (1, 2, 3) +__version__ = "1.2.3" if os.environ.get("MSGPACK_PUREPYTHON"):