From bf05d32b55e9c39cb95869fd2496549ef90d4cf8 Mon Sep 17 00:00:00 2001 From: abhinavmir Date: Sun, 30 Aug 2026 00:38:37 -0700 Subject: [PATCH] Check exported buffers after the default callback returns The default callback can export the internal buffer with getbuffer(). The packer then continues and can reallocate that buffer. The export points at freed memory after the reallocation. Call _check_exports() after the callback returns. The packer now raises BufferError. The pure Python packer already raises BufferError in this case. --- msgpack/_packer.pyx | 3 +++ test/test_buffer.py | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/msgpack/_packer.pyx b/msgpack/_packer.pyx index ca6cf983..b2dce7f6 100644 --- a/msgpack/_packer.pyx +++ b/msgpack/_packer.pyx @@ -264,6 +264,9 @@ cdef class Packer: ret = self._pack_inner(o, 1, nest_limit) if ret == -2: o = self._default(o) + # The callback may have exported the internal buffer. + # Packing on would reallocate it and invalidate the export. + self._check_exports() else: return ret return self._pack_inner(o, 0, nest_limit) diff --git a/test/test_buffer.py b/test/test_buffer.py index ca097222..ed17f215 100644 --- a/test/test_buffer.py +++ b/test/test_buffer.py @@ -47,3 +47,25 @@ def test_packer_getbuffer(): buffer.release() packer.pack(42) assert bytes(packer) == b"\x92*\xa5hello*" + + +def test_packer_getbuffer_in_default(): + # The default callback can export the internal buffer. + # The packer must refuse to pack on, because packing on reallocates + # the buffer and leaves the export pointing at freed memory. + exported = [] + + class Unsupported: + pass + + def default(obj): + exported.append(packer.getbuffer()) + return b"A" * (2 * 1024 * 1024) + + packer = Packer(default=default, autoreset=False) + with raises(BufferError): + packer.pack([Unsupported()]) + + assert len(exported) == 1 + assert bytes(exported[0]) == b"\x91" + exported[0].release()