From 5e8536a32c5a2eefe40172e4776c683cda85e184 Mon Sep 17 00:00:00 2001 From: Christian Aurich Zanettini Martins Date: Mon, 12 Oct 2026 23:18:22 -0300 Subject: [PATCH] gh-159283: Keep the propagating exception in PyObject_ClearManagedDict() When an object whose __dict__ still pointed at its inline values was freed while an exception was propagating, a failed allocation while detaching the dict called PyErr_NoMemory(), which replaced that exception, and PyErr_FormatUnraisable() then cleared it. A debug build aborted in py_dealloc() and a release build went on with no exception set, which ended in SystemError. Save the current exception before detaching the dict and restore it afterwards. --- Lib/test/test_class.py | 35 +++++++++++++++++++ ...-10-12-23-18-16.gh-issue-159283.VdOijg.rst | 4 +++ Objects/dictobject.c | 4 +++ 3 files changed, 43 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-10-12-23-18-16.gh-issue-159283.VdOijg.rst diff --git a/Lib/test/test_class.py b/Lib/test/test_class.py index 0ba3956be065a8d..ad872bc64386f3f 100644 --- a/Lib/test/test_class.py +++ b/Lib/test/test_class.py @@ -1047,6 +1047,41 @@ def __init__(self): self.fail("MemoryError was not raised while detaching " f"the dictionary: {seen}") + @support.nomemtest + def test_detach_materialized_dict_no_memory_keeps_exception(self): + # A MemoryError raised while detaching the dictionary must not + # replace the exception which is propagating when the object is freed. + import _testcapi + + class A: + def __init__(self): + self.a = 1 + self.b = 2 + + def f(lst): + # The object is only referenced from the evaluation stack, + # so it is freed while ZeroDivisionError propagates. + [lst.pop(), 1/0] + + seen = [] + for n in range(20): + lst = [A()] + d = lst[0].__dict__ + with support.catch_unraisable_exception() as ex: + with self.assertRaises((ZeroDivisionError, MemoryError)) as cm: + _testcapi.call_with_nomemory(n, n + 1, f, lst) + if ex.unraisable is None: + continue + err_msg = ex.unraisable.err_msg + seen.append((n, type(cm.exception), err_msg)) + if (err_msg == + 'Exception ignored while clearing an object managed dict'): + self.assertIs(type(cm.exception), ZeroDivisionError) + break + else: + self.fail("MemoryError was not raised while detaching " + f"the dictionary: {seen}") + class DefinitionOrderTests(unittest.TestCase): # PEP 520: Preserving Class Attribute Definition Order diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-12-23-18-16.gh-issue-159283.VdOijg.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-12-23-18-16.gh-issue-159283.VdOijg.rst new file mode 100644 index 000000000000000..81cb2be1f22c84a --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-12-23-18-16.gh-issue-159283.VdOijg.rst @@ -0,0 +1,4 @@ +Fix a :exc:`SystemError` (a fatal error in a debug build) when an object +whose :attr:`~object.__dict__` has been accessed is freed while an exception +propagates and detaching the dictionary fails with :exc:`MemoryError`. The +propagating exception is now kept. Patch by Christian Aurich Zanettini Martins. diff --git a/Objects/dictobject.c b/Objects/dictobject.c index eed9bd92c55bf2b..c798f5db7f242ac 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -8018,6 +8018,9 @@ PyObject_ClearManagedDict(PyObject *obj) // We have a materialized object which points at the inline // values. We need to materialize the keys. Nothing can modify // this object, but we need to lock the dictionary. + // The object can be freed while an exception is propagating: + // don't let a MemoryError from detaching the dict replace it. + PyObject *exc = PyErr_GetRaisedException(); int err; Py_BEGIN_CRITICAL_SECTION(dict); err = detach_dict_from_object(dict, obj); @@ -8038,6 +8041,7 @@ PyObject_ClearManagedDict(PyObject *obj) clear_inline_values(_PyObject_InlineValues(obj)); Py_END_CRITICAL_SECTION(); } + PyErr_SetRaisedException(exc); } } Py_CLEAR(_PyObject_ManagedDictPointer(obj)->dict);