diff --git a/Lib/test/test_marshal.py b/Lib/test/test_marshal.py index 449704d1c347b6..5b4d02c232f4a6 100644 --- a/Lib/test/test_marshal.py +++ b/Lib/test/test_marshal.py @@ -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. diff --git a/Misc/NEWS.d/next/Library/2026-10-11-08-40-00.gh-issue-159185.Sq7cZd.rst b/Misc/NEWS.d/next/Library/2026-10-11-08-40-00.gh-issue-159185.Sq7cZd.rst new file mode 100644 index 00000000000000..493cd78d6cf015 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-11-08-40-00.gh-issue-159185.Sq7cZd.rst @@ -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. diff --git a/Python/marshal.c b/Python/marshal.c index 6aa99bb4693117..b6a50627854eaf 100644 --- a/Python/marshal.c +++ b/Python/marshal.c @@ -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) { @@ -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);