Crash report
marshal.loads() crashes (SIGSEGV on a release build, an assertion abort on a debug build) when it loads a back-referenced dictionary — one marshalled with FLAG_REF, e.g. a dict that appears more than once or under a referenced parent — and the interpreter is under memory pressure. A single allocation failure inside marshal's reference-tracking turns into a NULL dereference instead of a clean, catchable MemoryError, so a program that correctly handles MemoryError can still be crashed (or left with a corrupted interpreter) by a marshal stream loaded under low memory.
Reproduction (debug build)
import _testcapi, marshal
# TYPE_DICT|FLAG_REF payload that decodes to {1: 2}; FLAG_REF makes
# marshal reference-track the dict, which is what triggers the bug.
payload = b'\xfbi\x01\x00\x00\x00i\x02\x00\x00\x000'
assert marshal.loads(payload) == {1: 2}
_testcapi.set_nomemory(0, 1) # fail the next allocation
try:
marshal.loads(payload) # -> SIGSEGV / assertion abort
finally:
_testcapi.remove_mem_hooks()
$ ./python crash.py
Segmentation fault
faulthandler / lldb pin the crash to PyDict_SetItem() being called with a NULL dict, from r_object() in Python/marshal.c. The same fault fires under a genuine MemoryError (no test API) when marshal's reference-tracking allocation is the one that fails. Loading a non-back-referenced dict, or a back-referenced tuple/list/set, under the same single-allocation failure raises a clean MemoryError — so the crash is specific to the dict path.
Root cause
In Python/marshal.c, r_object(), the TYPE_DICT branch registers the new dict for back-references with R_REF(v):
v = PyDict_New();
if (v == NULL) {
break;
}
if (type == TYPE_DICT) {
R_REF(v); // can set v = NULL (r_ref() -> PyList_Append() fails)
}
...
for (;;) {
...
if (PyDict_SetItem(v, key, val) < 0) { // v may be NULL here -> crash
R_REF(v) expands to if (flag) v = r_ref(v, flag, p);, and r_ref() returns NULL (after decref'ing its argument) when its internal PyList_Append() fails. Unlike the sibling container branches — TYPE_TUPLE, TYPE_LIST, TYPE_FROZENDICT, and the set/frozenset branches all follow R_REF(v) with an if (v == NULL) break; (or if (idx < 0)) guard — the TYPE_DICT branch has none, so it falls through to PyDict_SetItem(NULL, ...). It is the only unguarded R_REF(v) in r_object().
Suggested fix
Add the same if (v == NULL) break; guard after R_REF(v) in the TYPE_DICT branch, mirroring the sibling branches. r_ref() has already decref'd the dict on failure, so the early break propagates the MemoryError cleanly.
Affected versions
main (3.16) and 3.15 only — this is a regression. The if (v == NULL) break; guard was present in the TYPE_DICT branch until 2e37d83 (gh-148653, "Fix some marshal errors related to recursive immutable objects"), which refactored the TYPE_DICT/TYPE_FROZENDICT branches to share code and dropped the guard from the dict path. It first shipped in v3.15.0b1, so no released version is affected; 3.13 and 3.14 still have the guard. The fix should be backported to 3.15.
Note
marshal is documented as not intended for use with untrusted data, so this is a robustness / crash bug rather than a security vulnerability.
Linked PR
A PR follows.
Linked PRs
Crash report
marshal.loads()crashes (SIGSEGV on a release build, an assertion abort on a debug build) when it loads a back-referenced dictionary — one marshalled withFLAG_REF, e.g. a dict that appears more than once or under a referenced parent — and the interpreter is under memory pressure. A single allocation failure inside marshal's reference-tracking turns into aNULLdereference instead of a clean, catchableMemoryError, so a program that correctly handlesMemoryErrorcan still be crashed (or left with a corrupted interpreter) by a marshal stream loaded under low memory.Reproduction (debug build)
faulthandler / lldb pin the crash to
PyDict_SetItem()being called with aNULLdict, fromr_object()inPython/marshal.c. The same fault fires under a genuineMemoryError(no test API) when marshal's reference-tracking allocation is the one that fails. Loading a non-back-referenced dict, or a back-referenced tuple/list/set, under the same single-allocation failure raises a cleanMemoryError— so the crash is specific to the dict path.Root cause
In
Python/marshal.c,r_object(), theTYPE_DICTbranch registers the new dict for back-references withR_REF(v):R_REF(v)expands toif (flag) v = r_ref(v, flag, p);, andr_ref()returnsNULL(after decref'ing its argument) when its internalPyList_Append()fails. Unlike the sibling container branches —TYPE_TUPLE,TYPE_LIST,TYPE_FROZENDICT, and the set/frozenset branches all followR_REF(v)with anif (v == NULL) break;(orif (idx < 0)) guard — theTYPE_DICTbranch has none, so it falls through toPyDict_SetItem(NULL, ...). It is the only unguardedR_REF(v)inr_object().Suggested fix
Add the same
if (v == NULL) break;guard afterR_REF(v)in theTYPE_DICTbranch, mirroring the sibling branches.r_ref()has already decref'd the dict on failure, so the earlybreakpropagates theMemoryErrorcleanly.Affected versions
main(3.16) and3.15only — this is a regression. Theif (v == NULL) break;guard was present in theTYPE_DICTbranch until 2e37d83 (gh-148653, "Fix some marshal errors related to recursive immutable objects"), which refactored theTYPE_DICT/TYPE_FROZENDICTbranches to share code and dropped the guard from the dict path. It first shipped in v3.15.0b1, so no released version is affected; 3.13 and 3.14 still have the guard. The fix should be backported to 3.15.Note
marshalis documented as not intended for use with untrusted data, so this is a robustness / crash bug rather than a security vulnerability.Linked PR
A PR follows.
Linked PRs