Skip to content

Commit b08617d

Browse files
committed
Refuse to export the Packer buffer during pack
The C Packer accepted getbuffer() from a default callback, from dict.items(), and from pack_map_pairs() while it was still writing. Growing the buffer then freed the exported memory. Those methods now reject the export until they return. pytest -v test: 181 passed, 1 skipped. MSGPACK_PUREPYTHON=1 pytest -v test: 178 passed, 4 skipped. The pure Python buffer is not freed when it grows.
1 parent 8a88e3d commit b08617d

2 files changed

Lines changed: 112 additions & 29 deletions

File tree

‎msgpack/_packer.pyx‎

Lines changed: 51 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ cdef class Packer:
106106
cdef object _berrors
107107
cdef const char *unicode_errors
108108
cdef size_t exports # number of exported buffers
109+
cdef int _packing # public pack methods currently running
109110
cdef bint strict_types
110111
cdef bint use_float
111112
cdef bint autoreset
@@ -118,6 +119,7 @@ cdef class Packer:
118119
self.pk.buf_size = buf_size
119120
self.pk.length = 0
120121
self.exports = 0
122+
self._packing = 0
121123

122124
def __dealloc__(self):
123125
PyMem_Free(self.pk.buf)
@@ -273,29 +275,40 @@ cdef class Packer:
273275
def pack(self, object obj):
274276
cdef int ret
275277
self._check_exports()
278+
# User code runs inside _pack (default, items(), iteration) and can
279+
# export this buffer. Hold the flag until every write from this call
280+
# has finished, including a nested pack.
281+
self._packing += 1
276282
try:
277-
ret = self._pack(obj, DEFAULT_RECURSE_LIMIT)
278-
except:
279-
self.pk.length = 0
280-
raise
281-
if ret: # should not happen.
282-
raise RuntimeError("internal error")
283-
if self.autoreset:
284-
buf = PyBytes_FromStringAndSize(self.pk.buf, self.pk.length)
285-
self.pk.length = 0
286-
return buf
283+
try:
284+
ret = self._pack(obj, DEFAULT_RECURSE_LIMIT)
285+
except:
286+
self.pk.length = 0
287+
raise
288+
if ret: # should not happen.
289+
raise RuntimeError("internal error")
290+
if self.autoreset:
291+
buf = PyBytes_FromStringAndSize(self.pk.buf, self.pk.length)
292+
self.pk.length = 0
293+
return buf
294+
finally:
295+
self._packing -= 1
287296

288297
@cython.critical_section
289298
def pack_ext_type(self, typecode, data):
290299
self._check_exports()
291-
if len(data) > ITEM_LIMIT:
292-
raise ValueError("ext data too large")
293-
msgpack_pack_ext(&self.pk, typecode, len(data))
294-
msgpack_pack_raw_body(&self.pk, data, len(data))
295-
if self.autoreset:
296-
buf = PyBytes_FromStringAndSize(self.pk.buf, self.pk.length)
297-
self.pk.length = 0
298-
return buf
300+
self._packing += 1
301+
try:
302+
if len(data) > ITEM_LIMIT:
303+
raise ValueError("ext data too large")
304+
msgpack_pack_ext(&self.pk, typecode, len(data))
305+
msgpack_pack_raw_body(&self.pk, data, len(data))
306+
if self.autoreset:
307+
buf = PyBytes_FromStringAndSize(self.pk.buf, self.pk.length)
308+
self.pk.length = 0
309+
return buf
310+
finally:
311+
self._packing -= 1
299312

300313
@cython.critical_section
301314
def pack_array_header(self, long long size):
@@ -332,17 +345,21 @@ cdef class Packer:
332345
(`len(pairs)` and `for k, v in pairs:` should be supported.)
333346
"""
334347
self._check_exports()
335-
size = len(pairs)
336-
if size > ITEM_LIMIT:
337-
raise ValueError("map too large")
338-
msgpack_pack_map(&self.pk, size)
339-
for k, v in pairs:
340-
self._pack(k)
341-
self._pack(v)
342-
if self.autoreset:
343-
buf = PyBytes_FromStringAndSize(self.pk.buf, self.pk.length)
344-
self.pk.length = 0
345-
return buf
348+
self._packing += 1
349+
try:
350+
size = len(pairs)
351+
if size > ITEM_LIMIT:
352+
raise ValueError("map too large")
353+
msgpack_pack_map(&self.pk, size)
354+
for k, v in pairs:
355+
self._pack(k)
356+
self._pack(v)
357+
if self.autoreset:
358+
buf = PyBytes_FromStringAndSize(self.pk.buf, self.pk.length)
359+
self.pk.length = 0
360+
return buf
361+
finally:
362+
self._packing -= 1
346363

347364
@cython.critical_section
348365
def reset(self):
@@ -362,11 +379,16 @@ cdef class Packer:
362379
"""Return memoryview of internal buffer.
363380
364381
Note: Packer now supports buffer protocol. You can use memoryview(packer).
382+
Raises BufferError while a pack method is still running.
365383
"""
366384
return memoryview(self)
367385

368386
@cython.critical_section
369387
def __getbuffer__(self, Py_buffer *buffer, int flags):
388+
if self._packing:
389+
raise BufferError(
390+
"Packer cannot export its buffer while packing is in progress"
391+
)
370392
PyBuffer_FillInfo(buffer, self, self.pk.buf, self.pk.length, 1, flags)
371393
self.exports += 1
372394

‎test/test_buffer.py‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,64 @@ def test_packer_getbuffer():
4747
buffer.release()
4848
packer.pack(42)
4949
assert bytes(packer) == b"\x92*\xa5hello*"
50+
51+
52+
def _c_extension():
53+
return Packer.__module__ == "msgpack._cmsgpack"
54+
55+
56+
def test_getbuffer_during_default_is_rejected():
57+
# The default callback runs in the middle of pack(). Exporting the
58+
# internal buffer there lets a later realloc free the view.
59+
if not _c_extension():
60+
return
61+
62+
class Unsupported:
63+
pass
64+
65+
def default(obj):
66+
packer.getbuffer()
67+
return b"ok"
68+
69+
packer = Packer(default=default, autoreset=False)
70+
with raises(BufferError):
71+
packer.pack(Unsupported())
72+
packer.pack(b"ok")
73+
assert bytes(packer.getbuffer()) == b"\xc4\x02ok"
74+
75+
76+
def test_getbuffer_during_dict_items_is_rejected():
77+
if not _c_extension():
78+
return
79+
80+
packer = Packer(autoreset=False)
81+
82+
class EvilDict(dict):
83+
def items(self):
84+
packer.getbuffer()
85+
return [(b"k", b"v")]
86+
87+
with raises(BufferError):
88+
packer.pack(EvilDict({b"x": 1}))
89+
assert bytes(packer.getbuffer()) == b""
90+
91+
92+
def test_getbuffer_during_map_pairs_is_rejected():
93+
if not _c_extension():
94+
return
95+
96+
packer = Packer(autoreset=False)
97+
98+
class EvilPairs:
99+
def __len__(self):
100+
return 1
101+
102+
def __iter__(self):
103+
packer.getbuffer()
104+
yield (b"k", b"v")
105+
106+
with raises(BufferError):
107+
packer.pack_map_pairs(EvilPairs())
108+
packer.reset()
109+
packer.pack(b"ok")
110+
assert bytes(packer.bytes()) == b"\xc4\x02ok"

0 commit comments

Comments
 (0)