From e04d3b61573f4aab16ed23e7953697c2a1257255 Mon Sep 17 00:00:00 2001 From: tonghuaroot Date: Mon, 22 Jun 2026 11:11:37 +0800 Subject: [PATCH 1/3] gh-151895: Fix marshal.loads() crash on dict reference-tracking failure Loading a reference-tracked dictionary dereferenced a NULL pointer when the allocation that registers it for back-references failed under low memory. It now raises MemoryError, matching the tuple and list paths. --- Lib/test/test_marshal.py | 28 +++++++++++++++++++ ...-06-22-11-05-56.gh-issue-151895.QQsjUQ.rst | 2 ++ Python/marshal.c | 3 ++ 3 files changed, 33 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-06-22-11-05-56.gh-issue-151895.QQsjUQ.rst diff --git a/Lib/test/test_marshal.py b/Lib/test/test_marshal.py index 9c4d91c456dc5d9..90232d12c4a9c77 100644 --- a/Lib/test/test_marshal.py +++ b/Lib/test/test_marshal.py @@ -449,6 +449,34 @@ def test_loads_abnormal_reference_loops(self): with self.subTest(data=data): self.assertRaises(ValueError, marshal.loads, data) + @support.cpython_only + @unittest.skipUnless(_testcapi, 'requires _testcapi') + @unittest.skipIf(support.Py_TRACE_REFS, + 'Py_TRACE_REFS conflicts with _testcapi.set_nomemory') + def test_loads_dict_no_memory(self): + # gh-151895: loading a reference-tracked dict used to crash when the + # allocation that registers it in the reference list failed. + data = b'\xfbi\x01\x00\x00\x00i\x02\x00\x00\x000' # {1: 2}, FLAG_REF + self.assertEqual(marshal.loads(data), {1: 2}) + # The reference-list allocation fails early; 16 is ample headroom. + for index in range(16): + with self.subTest(index=index): + # Capture the outcome before touching memory again: any + # allocation made by an assertion would also fail while the + # nomemory hook is active. + result = error = None + _testcapi.set_nomemory(index, index + 1) + try: + result = marshal.loads(data) + except MemoryError as exc: + error = exc + finally: + _testcapi.remove_mem_hooks() + if error is None: + self.assertEqual(result, {1: 2}) + # The interpreter must still be healthy after the sweep. + self.assertEqual(marshal.loads(data), {1: 2}) + def test_exact_type_match(self): # Former bug: # >>> class Int(int): pass diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-22-11-05-56.gh-issue-151895.QQsjUQ.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-22-11-05-56.gh-issue-151895.QQsjUQ.rst new file mode 100644 index 000000000000000..e17b340348b06b1 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-22-11-05-56.gh-issue-151895.QQsjUQ.rst @@ -0,0 +1,2 @@ +Fixed a crash in :func:`marshal.loads` when an allocation failed while +loading a reference-tracked dictionary; it now raises :exc:`MemoryError`. diff --git a/Python/marshal.c b/Python/marshal.c index 9688d426419c2fa..bb936efa7fa5834 100644 --- a/Python/marshal.c +++ b/Python/marshal.c @@ -1471,6 +1471,9 @@ r_object(RFILE *p) } if (type == TYPE_DICT) { R_REF(v); + if (v == NULL) { + break; + } } else { idx = r_ref_reserve(flag, p); From 90ba4df2809e962f416a982f7e586044ed94ffe7 Mon Sep 17 00:00:00 2001 From: tonghuaroot Date: Mon, 22 Jun 2026 11:29:40 +0800 Subject: [PATCH 2/3] Trim test comments --- Lib/test/test_marshal.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_marshal.py b/Lib/test/test_marshal.py index 90232d12c4a9c77..54260e53cd9678a 100644 --- a/Lib/test/test_marshal.py +++ b/Lib/test/test_marshal.py @@ -458,12 +458,10 @@ def test_loads_dict_no_memory(self): # allocation that registers it in the reference list failed. data = b'\xfbi\x01\x00\x00\x00i\x02\x00\x00\x000' # {1: 2}, FLAG_REF self.assertEqual(marshal.loads(data), {1: 2}) - # The reference-list allocation fails early; 16 is ample headroom. for index in range(16): with self.subTest(index=index): - # Capture the outcome before touching memory again: any - # allocation made by an assertion would also fail while the - # nomemory hook is active. + # Capture the outcome first: an assertion would itself + # allocate and fail while the nomemory hook is active. result = error = None _testcapi.set_nomemory(index, index + 1) try: From a178dace6e5cbfeaf1b9327f8b4286f4180065d8 Mon Sep 17 00:00:00 2001 From: tonghuaroot Date: Mon, 22 Jun 2026 20:14:54 +0800 Subject: [PATCH 3/3] gh-151895: Drop the no-memory regression test Per review: CPython does not usually add tests for unchecked-malloc failure paths. Keep just the r_object() TYPE_DICT guard. --- Lib/test/test_marshal.py | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/Lib/test/test_marshal.py b/Lib/test/test_marshal.py index 54260e53cd9678a..9c4d91c456dc5d9 100644 --- a/Lib/test/test_marshal.py +++ b/Lib/test/test_marshal.py @@ -449,32 +449,6 @@ def test_loads_abnormal_reference_loops(self): with self.subTest(data=data): self.assertRaises(ValueError, marshal.loads, data) - @support.cpython_only - @unittest.skipUnless(_testcapi, 'requires _testcapi') - @unittest.skipIf(support.Py_TRACE_REFS, - 'Py_TRACE_REFS conflicts with _testcapi.set_nomemory') - def test_loads_dict_no_memory(self): - # gh-151895: loading a reference-tracked dict used to crash when the - # allocation that registers it in the reference list failed. - data = b'\xfbi\x01\x00\x00\x00i\x02\x00\x00\x000' # {1: 2}, FLAG_REF - self.assertEqual(marshal.loads(data), {1: 2}) - for index in range(16): - with self.subTest(index=index): - # Capture the outcome first: an assertion would itself - # allocate and fail while the nomemory hook is active. - result = error = None - _testcapi.set_nomemory(index, index + 1) - try: - result = marshal.loads(data) - except MemoryError as exc: - error = exc - finally: - _testcapi.remove_mem_hooks() - if error is None: - self.assertEqual(result, {1: 2}) - # The interpreter must still be healthy after the sweep. - self.assertEqual(marshal.loads(data), {1: 2}) - def test_exact_type_match(self): # Former bug: # >>> class Int(int): pass