diff --git a/Lib/test/test_marshal.py b/Lib/test/test_marshal.py index 2e8e0ea388639a8..d917e9339995547 100644 --- a/Lib/test/test_marshal.py +++ b/Lib/test/test_marshal.py @@ -562,6 +562,29 @@ def test_unmarshallable(self): with self.assertRaisesRegex(ValueError, "unmarshallable object"): marshal.dumps((arg, memoryview(b''))) + 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) + LARGE_SIZE = 2**31 pointer_size = 8 if sys.maxsize > 0xFFFFFFFF else 4 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 d5107c4975ca11b..a05305f72cf51af 100644 --- a/Python/marshal.c +++ b/Python/marshal.c @@ -104,6 +104,7 @@ module marshal #define WFERR_NESTEDTOODEEP 2 #define WFERR_NOMEMORY 3 #define WFERR_CODE_NOT_ALLOWED 4 +#define WFERR_SET_CHANGED_SIZE 5 typedef struct { FILE *fp; @@ -635,9 +636,19 @@ w_complex_object(PyObject *v, char flag, WFILE *p) p->error = WFERR_NOMEMORY; 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) { @@ -659,7 +670,11 @@ w_complex_object(PyObject *v, char flag, WFILE *p) Py_DECREF(pairs); return; } - assert(i == n); + if (changed || i != n) { + p->error = WFERR_SET_CHANGED_SIZE; + Py_DECREF(pairs); + return; + } if (PyList_Sort(pairs)) { p->error = WFERR_NOMEMORY; Py_DECREF(pairs); @@ -1909,6 +1924,10 @@ _PyMarshal_WriteObjectToString(PyObject *x, int version, int allow_code) PyErr_SetString(PyExc_ValueError, "marshalling code objects is disallowed"); break; + case WFERR_SET_CHANGED_SIZE: + PyErr_SetString(PyExc_RuntimeError, + "set changed size during iteration"); + break; default: case WFERR_UNMARSHALLABLE: PyErr_SetString(PyExc_ValueError,