[3.14] gh-159185: Fix marshalling of a set modified while its elements are marshalled (GH-159195) (#159200)

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 cd71799c47)
This commit is contained in:
Gregory P. Smith authored and GitHub committed 2026-10-11 14:52:10 -07:00
1 parent 865124c69f
commit 09e1e663c9
3 files changed
+47 -1

No files matched your search

+20 -1
View File
@@ -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,