From ea4bd9d2fec20f57576161a66f10dba51ac0a61a Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 3 Aug 2026 19:57:58 +0200 Subject: [PATCH 1/2] fix: use-after-free in unpackb() ExtraData path for non-contiguous input For non-contiguous input, get_data_from_buffer() makes a temporary contiguous copy and buf points into it. unpackb() released the buffer view (freeing that copy) in the finally block, and only afterwards read buf+off to build the ExtraData payload, so ExtraData.extra was filled from freed memory. Copy the extra data out before releasing the view. Fixes #720 --- msgpack/_unpacker.pyx | 9 +++++++-- test/test_memoryview.py | 21 ++++++++++++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/msgpack/_unpacker.pyx b/msgpack/_unpacker.pyx index e0463617..30ef5e8a 100644 --- a/msgpack/_unpacker.pyx +++ b/msgpack/_unpacker.pyx @@ -168,6 +168,7 @@ def unpackb(object packed, *, object object_hook=None, object list_hook=None, cdef char* buf = NULL cdef Py_ssize_t buf_len cdef const char* cerr = NULL + cdef object extra = None if unicode_errors is not None: cerr = unicode_errors @@ -190,13 +191,17 @@ def unpackb(object packed, *, object object_hook=None, object list_hook=None, use_list, raw, timestamp, strict_map_key, cerr, 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 and 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. + extra = PyBytes_FromStringAndSize(buf+off, buf_len-off) finally: PyBuffer_Release(&view); if ret == 1: obj = unpack_data(&ctx) - if off < buf_len: - raise ExtraData(obj, PyBytes_FromStringAndSize(buf+off, buf_len-off)) + if extra is not None: + raise ExtraData(obj, extra) return obj unpack_clear(&ctx) diff --git a/test/test_memoryview.py b/test/test_memoryview.py index 3f6a39d4..fd124dce 100644 --- a/test/test_memoryview.py +++ b/test/test_memoryview.py @@ -2,7 +2,9 @@ from array import array -from msgpack import packb, unpackb +from pytest import raises + +from msgpack import ExtraData, packb, unpackb def make_array(f, data): @@ -109,3 +111,20 @@ def test_unpack_noncontiguous_memoryview(): noncont = memoryview(bytes(padded))[::2] assert not noncont.c_contiguous assert unpackb(noncont) == 2**32 + + +def test_unpack_noncontiguous_memoryview_extra_data(): + # See https://github.com/msgpack/msgpack-python/issues/720 + # ExtraData.extra must be copied out of the temporary contiguous copy + # before that copy is released. + packed = packb(0) + b"extra" + padded = bytearray() + for byte in packed: + padded.append(byte) + padded.append(0) + noncont = memoryview(bytes(padded))[::2] + assert not noncont.c_contiguous + with raises(ExtraData) as excinfo: + unpackb(noncont) + assert excinfo.value.unpacked == 0 + assert excinfo.value.extra == b"extra" From 58b6fd9faebfed9aa327d3ccf7447487a609c7c2 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 5 Aug 2026 18:43:36 +0900 Subject: [PATCH 2/2] refactor --- msgpack/_unpacker.pyx | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/msgpack/_unpacker.pyx b/msgpack/_unpacker.pyx index 30ef5e8a..e2953b1e 100644 --- a/msgpack/_unpacker.pyx +++ b/msgpack/_unpacker.pyx @@ -191,19 +191,16 @@ def unpackb(object packed, *, object object_hook=None, object list_hook=None, use_list, raw, timestamp, strict_map_key, cerr, 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 and 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. - extra = PyBytes_FromStringAndSize(buf+off, buf_len-off) + if ret == 1: + obj = unpack_data(&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: PyBuffer_Release(&view); - if ret == 1: - obj = unpack_data(&ctx) - if extra is not None: - raise ExtraData(obj, extra) - return obj - unpack_clear(&ctx) if ret == 0: raise ValueError("Unpack failed: incomplete input")