Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions Lib/test/test_marshal.py
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,30 @@ def test_error_in_set_item(self):
with self.assertRaisesRegex(TypeError, "'<' not supported"):
marshal.dumps({complex(nan, 0), complex(nan, 0)})

def test_set_changed_size(self):
# A set is modified while its items are being marshalled.
class Item:
def __hash__(self):
return 0
def __buffer__(self, flags):
change(sample)
return memoryview(b'abc')
def grow(sample):
sample.update(range(1000, 1400))
def shrink(sample):
sample.difference_update(range(1, 60))
for change in grow, shrink:
for version in range(marshal.version + 1):
with self.subTest(change=change.__name__, version=version):
# The first item of the set modifies it.
sample = {Item()}
sample.update(range(1, 60))
self.assertIsInstance(next(iter(sample)), Item)
with self.assertRaisesRegex(RuntimeError,
"set changed size"):
marshal.dumps(sample, version)


def test_error_in_buffer(self):
# The BufferError raised for a non-contiguous buffer is not replaced
# with a generic error.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix a crash in :func:`marshal.dumps` and :func:`marshal.dump` when a set is
modified while its elements are serialized, for example by the
:meth:`~object.__buffer__` method of one of its elements.
:exc:`RuntimeError` is now raised.
16 changes: 15 additions & 1 deletion Python/marshal.c
Original file line number Diff line number Diff line change
Expand Up @@ -681,9 +681,19 @@ w_complex_object(PyObject *v, char flag, WFILE *p)
p->error = true;
return;
}
// Marshalling an element can run arbitrary code (an audit hook, or
// the __buffer__() method of the element), which can add elements
// to the set or remove some: n is not trusted to still be the size
// of the set.
Py_ssize_t i = 0;
bool changed = false;
Py_BEGIN_CRITICAL_SECTION(v);
while (_PySet_NextEntryRef(v, &pos, &value, &hash)) {
if (i == n) {
changed = true;
Py_DECREF(value);
break;
}
PyObject *dump = _PyMarshal_WriteObjectToString(value,
p->version, p->allow_code);
if (dump == NULL) {
Expand All @@ -699,11 +709,15 @@ w_complex_object(PyObject *v, char flag, WFILE *p)
PyList_SET_ITEM(pairs, i++, pair);
}
Py_END_CRITICAL_SECTION();
if (!p->error && (changed || i != n)) {
PyErr_SetString(PyExc_RuntimeError,
"set changed size during iteration");
p->error = true;
}
if (p->error) {
Py_DECREF(pairs);
return;
}
assert(i == n);
if (PyList_Sort(pairs)) {
p->error = true;
Py_DECREF(pairs);
Expand Down
Loading