https://github.com/python/cpython/commit/cd71799c47802f87e1469cc97cc99f5ce6cea27c
commit: cd71799c47802f87e1469cc97cc99f5ce6cea27c
branch: main
author: Gregory P. Smith <[email protected]>
committer: gpshead <[email protected]>
date: 2026-10-11T14:09:04-07:00
summary:
gh-159185: Fix marshalling of a set modified while its elements are marshalled
(GH-159195)
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.
files:
A Misc/NEWS.d/next/Library/2026-10-11-08-40-00.gh-issue-159185.Sq7cZd.rst
M Lib/test/test_marshal.py
M Python/marshal.c
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);
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]