Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions Lib/test/test_marshal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
21 changes: 20 additions & 1 deletion Python/marshal.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
Expand Down Expand Up @@ -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,
Expand Down
Loading