From 379943fbaeec9f7194ccd7d16942e57079ad2a9e Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Sun, 11 Oct 2026 20:01:34 +0000 Subject: [PATCH] gh-159185: Fix marshalling of a set modified while its elements are marshalled Marshalling a set element can run Python code (__buffer__, an audit hook) which changes the set. The number of elements was read before the loop, so a set which grew was written past the end of the list and one which shrank left NULL items in the list which was then sorted. Raise RuntimeError("set changed size during iteration") instead. --- Lib/test/test_marshal.py | 24 +++++++++++++++++++ ...-10-11-08-40-00.gh-issue-159185.Sq7cZd.rst | 4 ++++ Python/marshal.c | 16 ++++++++++++- 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-11-08-40-00.gh-issue-159185.Sq7cZd.rst diff --git a/Lib/test/test_marshal.py b/Lib/test/test_marshal.py index 449704d1c347b60..5b4d02c232f4a64 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 000000000000000..493cd78d6cf015d --- /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 6aa99bb4693117a..b6a50627854eaf9 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);