From e15f90ed65943a5a307ebc62f94f9cecd3e11e43 Mon Sep 17 00:00:00 2001 From: obsidian_nomad Date: Sun, 15 Nov 2026 23:10:29 +0530 Subject: [PATCH] Reject Packer buffer exports while packing in the C extension Packer only checked for live buffer exports on entry to pack(), pack_map_pairs() and pack_ext_type(). Python code that runs during packing (a default callback, the pairs iterable, a list or dict subclass) could call memoryview(packer), after which msgpack_pack_write() reallocated pk.buf and left the export pointing at freed memory. Refuse exports from __getbuffer__ while one of those methods is running. The pure Python Packer already raises BufferError in this case because BytesIO cannot be resized while exported. --- msgpack/_packer.pyx | 38 +++++++++++++++++++++++++++----------- test/test_buffer.py | 14 ++++++++++++++ 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/msgpack/_packer.pyx b/msgpack/_packer.pyx index c44c6366..d3d00483 100644 --- a/msgpack/_packer.pyx +++ b/msgpack/_packer.pyx @@ -106,6 +106,7 @@ cdef class Packer: cdef object _berrors cdef const char *unicode_errors cdef size_t exports # number of exported buffers + cdef size_t packing # nesting depth of pack calls that can run Python code cdef bint strict_types cdef bint use_float cdef bint autoreset @@ -118,6 +119,7 @@ cdef class Packer: self.pk.buf_size = buf_size self.pk.length = 0 self.exports = 0 + self.packing = 0 def __dealloc__(self): PyMem_Free(self.pk.buf) @@ -273,11 +275,14 @@ cdef class Packer: def pack(self, object obj): cdef int ret self._check_exports() + self.packing += 1 try: ret = self._pack(obj, DEFAULT_RECURSE_LIMIT) except: self.pk.length = 0 raise + finally: + self.packing -= 1 if ret: # should not happen. raise RuntimeError("internal error") if self.autoreset: @@ -288,10 +293,14 @@ cdef class Packer: @cython.critical_section def pack_ext_type(self, typecode, data): self._check_exports() - if len(data) > ITEM_LIMIT: - raise ValueError("ext data too large") - msgpack_pack_ext(&self.pk, typecode, len(data)) - msgpack_pack_raw_body(&self.pk, data, len(data)) + self.packing += 1 + try: + if len(data) > ITEM_LIMIT: + raise ValueError("ext data too large") + msgpack_pack_ext(&self.pk, typecode, len(data)) + msgpack_pack_raw_body(&self.pk, data, len(data)) + finally: + self.packing -= 1 if self.autoreset: buf = PyBytes_FromStringAndSize(self.pk.buf, self.pk.length) self.pk.length = 0 @@ -332,13 +341,17 @@ cdef class Packer: (`len(pairs)` and `for k, v in pairs:` should be supported.) """ self._check_exports() - size = len(pairs) - if size > ITEM_LIMIT: - raise ValueError("map too large") - msgpack_pack_map(&self.pk, size) - for k, v in pairs: - self._pack(k) - self._pack(v) + self.packing += 1 + try: + size = len(pairs) + if size > ITEM_LIMIT: + raise ValueError("map too large") + msgpack_pack_map(&self.pk, size) + for k, v in pairs: + self._pack(k) + self._pack(v) + finally: + self.packing -= 1 if self.autoreset: buf = PyBytes_FromStringAndSize(self.pk.buf, self.pk.length) self.pk.length = 0 @@ -367,6 +380,9 @@ cdef class Packer: @cython.critical_section def __getbuffer__(self, Py_buffer *buffer, int flags): + # Packing may reallocate pk.buf, which would leave the export dangling. + if self.packing: + raise BufferError("Packer cannot export its buffer while packing") PyBuffer_FillInfo(buffer, self, self.pk.buf, self.pk.length, 1, flags) self.exports += 1 diff --git a/test/test_buffer.py b/test/test_buffer.py index ca097222..79695462 100644 --- a/test/test_buffer.py +++ b/test/test_buffer.py @@ -47,3 +47,17 @@ def test_packer_getbuffer(): buffer.release() packer.pack(42) assert bytes(packer) == b"\x92*\xa5hello*" + + +def test_packer_getbuffer_while_packing(): + views = [] + + def default(obj): + views.append(packer.getbuffer()) + return b"x" * 1024 # large enough to grow the internal buffer + + packer = Packer(autoreset=False, buf_size=16, default=default) + with raises(BufferError): + packer.pack([object()]) + with raises(BufferError): + packer.pack_map_pairs([(object(), 1)])