https://github.com/python/cpython/commit/4c2433f310b19460b6b69ceb05100a07c3154fc1
commit: 4c2433f310b19460b6b69ceb05100a07c3154fc1
branch: 3.15
author: Gregory P. Smith <[email protected]>
committer: gpshead <[email protected]>
date: 2026-10-11T21:55:14Z
summary:
[3.15] gh-159185: Fix marshalling of a set modified while its elements are
marshalled (GH-159195) (#159199)
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.
(cherry picked from commit cd71799c47802f87e1469cc97cc99f5ce6cea27c)
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 378b38ee05ca7b..96e3aa056992ab 100644
--- a/Lib/test/test_marshal.py
+++ b/Lib/test/test_marshal.py
@@ -582,6 +582,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 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 fcd0e58647684a..d2d25b5d012918 100644
--- a/Python/marshal.c
+++ b/Python/marshal.c
@@ -106,6 +106,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;
@@ -651,9 +652,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) {
@@ -673,7 +684,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);
@@ -1941,6 +1956,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,
_______________________________________________
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]