fix: add reentrant guard to Unpacker.feed() (#704)

fix ##695
This commit is contained in:
Inada Naoki 2026-06-24 15:13:45 +09:00 committed by GitHub
parent 7082130739
commit cf3fd2b061
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 60 additions and 28 deletions

View file

@ -305,6 +305,8 @@ cdef class Unpacker:
Raises ``OutOfData`` when *packed* is incomplete.
Raises ``FormatError`` when *packed* is not valid msgpack.
Raises ``StackError`` when *packed* contains too nested.
Raises ``RuntimeError`` when ``feed()`` is called while unpacking
is in progress (e.g. from a hook).
Other exceptions can be raised during unpacking.
"""
cdef unpack_context ctx
@ -318,6 +320,7 @@ cdef class Unpacker:
cdef object unicode_errors
cdef Py_ssize_t max_buffer_size
cdef uint64_t stream_offset
cdef bint _unpacking
def __dealloc__(self):
unpack_clear(&self.ctx)
@ -381,6 +384,7 @@ cdef class Unpacker:
self.buf_head = 0
self.buf_tail = 0
self.stream_offset = 0
self._unpacking = False
if unicode_errors is not None:
self.unicode_errors = unicode_errors
@ -398,6 +402,11 @@ cdef class Unpacker:
cdef char* buf
cdef Py_ssize_t buf_len
if self._unpacking:
raise RuntimeError(
"Unpacker.feed() cannot be called while unpacking is in progress"
)
if self.file_like is not None:
raise AssertionError(
"unpacker.feed() is not be able to use with `file_like`.")
@ -465,36 +474,40 @@ cdef class Unpacker:
cdef object obj
cdef Py_ssize_t prev_head
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
else:
ret = 0
if ret == 1:
obj = unpack_data(&self.ctx)
unpack_init(&self.ctx)
return obj
if ret == 0:
if self.file_like is not None:
self.read_from_file()
continue
if iter:
raise StopIteration("No more data to unpack.")
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
else:
raise OutOfData("No more data to unpack.")
ret = 0
unpack_clear(&self.ctx)
if ret == -2:
raise FormatError
elif ret == -3:
raise StackError
elif PyErr_Occurred():
raise
else:
raise ValueError("Unpack failed: error = %d" % (ret,))
if ret == 1:
obj = unpack_data(&self.ctx)
unpack_init(&self.ctx)
return obj
if ret == 0:
if self.file_like is not None:
self.read_from_file()
continue
if iter:
raise StopIteration("No more data to unpack.")
else:
raise OutOfData("No more data to unpack.")
unpack_clear(&self.ctx)
if ret == -2:
raise FormatError
elif ret == -3:
raise StackError
elif PyErr_Occurred():
raise
else:
raise ValueError("Unpack failed: error = %d" % (ret,))
finally:
self._unpacking = False
@cython.critical_section
def read_bytes(self, Py_ssize_t nbytes):

View file

@ -123,3 +123,22 @@ def test_unpacker_reinit_clears_partial_state():
unpacker.feed(packb({"a": 1}))
assert unpacker.unpack() == {"a": 1}
@mark.skipif(
Unpacker.__module__ == "msgpack.fallback",
reason="reentrant guard is implemented in C extension only",
)
def test_unpacker_reentrant_feed():
import struct
def ext_hook(code, data):
# re-entrant feed on the SAME unpacker, large enough to force a buffer realloc
up.feed(b"\xc0" * 100)
return 0
up = Unpacker(ext_hook=ext_hook, max_buffer_size=64 * 1024 * 1024)
# array(11): [ ExtType(code=5, data=b'A') (fires the re-entrant hook), then 10 more elements ]
up.feed(b"\xdc" + struct.pack(">H", 11) + b"\xd4\x05A" + b"\x2a" * 10)
with raises(RuntimeError):
up.unpack()