Skip to content

Commit aaf38ec

Browse files
committed
Refuse to resume a partial array/map with the other of skip()/unpack()
skip() and unpack() share one Unpacker._unpack() state machine and the same context, but a skip() call never populates a container frame's object slot, since it has no Python object to build there. If skip() leaves an array or map open because the buffer ran out mid-container, and the next call resumes it through unpack() instead, unpack() treats that slot as if it already held a real list or dict and crashes trying to append into it. Track which of the two last made progress on an in-progress container and reject switching to the other one with a clear ValueError instead of segfaulting. Resuming with the same method that started the parse still works exactly as before. Fixes #734
1 parent 9f4909c commit aaf38ec

2 files changed

Lines changed: 58 additions & 0 deletions

File tree

msgpack/_unpacker.pyx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ cdef extern from "unpack.h":
4444
msgpack_user user
4545
PyObject* obj
4646
Py_ssize_t count
47+
unsigned int top
4748

4849
ctypedef int (*execute_fn)(unpack_context* ctx, const char* data,
4950
Py_ssize_t len, Py_ssize_t* off) except -1
@@ -320,6 +321,10 @@ cdef class Unpacker:
320321
cdef Py_ssize_t max_buffer_size
321322
cdef uint64_t stream_offset
322323
cdef bint _unpacking
324+
# Which of unpack_construct/unpack_skip left an array or map open on
325+
# the last call, so a later call can refuse to resume it with the
326+
# other one instead of touching stack entries it never populated.
327+
cdef execute_fn _resume_execute
323328

324329
def __dealloc__(self):
325330
unpack_clear(&self.ctx)
@@ -473,6 +478,14 @@ cdef class Unpacker:
473478
cdef object obj
474479
cdef Py_ssize_t prev_head
475480

481+
if (self.ctx.top != 0 and self._resume_execute != NULL
482+
and self._resume_execute != execute):
483+
raise ValueError(
484+
"unpack() and skip() cannot be mixed while an array or "
485+
"map is still incomplete; finish it with the same method "
486+
"that started it"
487+
)
488+
476489
self._unpacking = True
477490
try:
478491
while 1:
@@ -486,8 +499,10 @@ cdef class Unpacker:
486499
if ret == 1:
487500
obj = unpack_data(&self.ctx)
488501
unpack_init(&self.ctx)
502+
self._resume_execute = NULL
489503
return obj
490504
if ret == 0:
505+
self._resume_execute = execute
491506
if self.file_like is not None:
492507
self.read_from_file()
493508
continue
@@ -497,6 +512,7 @@ cdef class Unpacker:
497512
raise OutOfData("No more data to unpack.")
498513

499514
unpack_clear(&self.ctx)
515+
self._resume_execute = NULL
500516
if ret == -2:
501517
raise FormatError
502518
elif ret == -3:

test/test_sequnpack.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,48 @@ def test_foobar_skip():
6363
unpacker.unpack()
6464

6565

66+
def test_skip_then_unpack_across_incomplete_container():
67+
# skip() opens the array's stack frame without ever populating its
68+
# object slot (it has nothing to build), so resuming with unpack()
69+
# used to reuse that slot as if it held a real list and crash. See
70+
# GH #734.
71+
unpacker = Unpacker()
72+
unpacker.feed(b"\x91")
73+
with raises(OutOfData):
74+
unpacker.skip()
75+
unpacker.feed(b"\x00")
76+
with raises(ValueError):
77+
unpacker.unpack()
78+
79+
80+
def test_unpack_then_skip_across_incomplete_container():
81+
unpacker = Unpacker()
82+
unpacker.feed(b"\x91")
83+
with raises(OutOfData):
84+
unpacker.unpack()
85+
unpacker.feed(b"\x00")
86+
with raises(ValueError):
87+
unpacker.skip()
88+
89+
90+
def test_skip_then_skip_across_incomplete_container_still_works():
91+
unpacker = Unpacker()
92+
unpacker.feed(b"\x91")
93+
with raises(OutOfData):
94+
unpacker.skip()
95+
unpacker.feed(b"\x00")
96+
assert unpacker.skip() is None
97+
98+
99+
def test_unpack_then_unpack_across_incomplete_container_still_works():
100+
unpacker = Unpacker()
101+
unpacker.feed(b"\x91")
102+
with raises(OutOfData):
103+
unpacker.unpack()
104+
unpacker.feed(b"\x00")
105+
assert unpacker.unpack() == [0]
106+
107+
66108
def test_maxbuffersize():
67109
with raises(ValueError):
68110
Unpacker(read_size=5, max_buffer_size=3)

0 commit comments

Comments
 (0)