Skip to content

Commit 8546c04

Browse files
committed
fix: add reentrant guard to Unpacker.feed()
1 parent 7082130 commit 8546c04

2 files changed

Lines changed: 61 additions & 29 deletions

File tree

msgpack/_unpacker.pyx

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,8 @@ cdef class Unpacker:
305305
Raises ``OutOfData`` when *packed* is incomplete.
306306
Raises ``FormatError`` when *packed* is not valid msgpack.
307307
Raises ``StackError`` when *packed* contains too nested.
308+
Raises ``RuntimeError`` when ``feed()`` is called while unpacking
309+
is in progress (e.g. from a hook).
308310
Other exceptions can be raised during unpacking.
309311
"""
310312
cdef unpack_context ctx
@@ -318,6 +320,7 @@ cdef class Unpacker:
318320
cdef object unicode_errors
319321
cdef Py_ssize_t max_buffer_size
320322
cdef uint64_t stream_offset
323+
cdef bint _unpacking
321324

322325
def __dealloc__(self):
323326
unpack_clear(&self.ctx)
@@ -381,6 +384,7 @@ cdef class Unpacker:
381384
self.buf_head = 0
382385
self.buf_tail = 0
383386
self.stream_offset = 0
387+
self._unpacking = False
384388

385389
if unicode_errors is not None:
386390
self.unicode_errors = unicode_errors
@@ -398,6 +402,11 @@ cdef class Unpacker:
398402
cdef char* buf
399403
cdef Py_ssize_t buf_len
400404

405+
if self._unpacking:
406+
raise RuntimeError(
407+
"Unpacker.feed() cannot be called while unpacking is in progress"
408+
)
409+
401410
if self.file_like is not None:
402411
raise AssertionError(
403412
"unpacker.feed() is not be able to use with `file_like`.")
@@ -465,36 +474,40 @@ cdef class Unpacker:
465474
cdef object obj
466475
cdef Py_ssize_t prev_head
467476

468-
while 1:
469-
prev_head = self.buf_head
470-
if prev_head < self.buf_tail:
471-
ret = execute(&self.ctx, self.buf, self.buf_tail, &self.buf_head)
472-
self.stream_offset += self.buf_head - prev_head
473-
else:
474-
ret = 0
475-
476-
if ret == 1:
477-
obj = unpack_data(&self.ctx)
478-
unpack_init(&self.ctx)
479-
return obj
480-
if ret == 0:
481-
if self.file_like is not None:
482-
self.read_from_file()
483-
continue
484-
if iter:
485-
raise StopIteration("No more data to unpack.")
477+
self._unpacking = True
478+
try:
479+
while 1:
480+
prev_head = self.buf_head
481+
if prev_head < self.buf_tail:
482+
ret = execute(&self.ctx, self.buf, self.buf_tail, &self.buf_head)
483+
self.stream_offset += self.buf_head - prev_head
486484
else:
487-
raise OutOfData("No more data to unpack.")
488-
489-
unpack_clear(&self.ctx)
490-
if ret == -2:
491-
raise FormatError
492-
elif ret == -3:
493-
raise StackError
494-
elif PyErr_Occurred():
495-
raise
496-
else:
497-
raise ValueError("Unpack failed: error = %d" % (ret,))
485+
ret = 0
486+
487+
if ret == 1:
488+
obj = unpack_data(&self.ctx)
489+
unpack_init(&self.ctx)
490+
return obj
491+
if ret == 0:
492+
if self.file_like is not None:
493+
self.read_from_file()
494+
continue
495+
if iter:
496+
raise StopIteration("No more data to unpack.")
497+
else:
498+
raise OutOfData("No more data to unpack.")
499+
500+
unpack_clear(&self.ctx)
501+
if ret == -2:
502+
raise FormatError
503+
elif ret == -3:
504+
raise StackError
505+
elif PyErr_Occurred():
506+
raise
507+
else:
508+
raise ValueError("Unpack failed: error = %d" % (ret,))
509+
finally:
510+
self._unpacking = False
498511

499512
@cython.critical_section
500513
def read_bytes(self, Py_ssize_t nbytes):

test/test_unpack.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,22 @@ def hook(code, data):
123123

124124
unpacker.feed(packb({"a": 1}))
125125
assert unpacker.unpack() == {"a": 1}
126+
127+
128+
@mark.skipif(
129+
Unpacker.__module__ == "msgpack.fallback",
130+
reason="reentrant guard is implemented in C extension only",
131+
)
132+
def test_unpacker_reentrant_feed():
133+
import struct
134+
135+
def ext_hook(code, data):
136+
# re-entrant feed on the SAME unpacker, large enough to force a buffer realloc
137+
up.feed(b"\xc0" * 100)
138+
return 0
139+
140+
up = Unpacker(ext_hook=ext_hook, max_buffer_size=64 * 1024 * 1024)
141+
# array(11): [ ExtType(code=5, data=b'A') (fires the re-entrant hook), then 10 more elements ]
142+
up.feed(b"\xdc" + struct.pack(">H", 11) + b"\xd4\x05A" + b"\x2a" * 10)
143+
with raises(RuntimeError):
144+
up.unpack()

0 commit comments

Comments
 (0)