Skip to content

Commit 5e8536a

Browse files
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.
1 parent 64018ac commit 5e8536a

3 files changed

Lines changed: 43 additions & 0 deletions

File tree

‎Lib/test/test_class.py‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,6 +1047,41 @@ def __init__(self):
10471047
self.fail("MemoryError was not raised while detaching "
10481048
f"the dictionary: {seen}")
10491049

1050+
@support.nomemtest
1051+
def test_detach_materialized_dict_no_memory_keeps_exception(self):
1052+
# A MemoryError raised while detaching the dictionary must not
1053+
# replace the exception which is propagating when the object is freed.
1054+
import _testcapi
1055+
1056+
class A:
1057+
def __init__(self):
1058+
self.a = 1
1059+
self.b = 2
1060+
1061+
def f(lst):
1062+
# The object is only referenced from the evaluation stack,
1063+
# so it is freed while ZeroDivisionError propagates.
1064+
[lst.pop(), 1/0]
1065+
1066+
seen = []
1067+
for n in range(20):
1068+
lst = [A()]
1069+
d = lst[0].__dict__
1070+
with support.catch_unraisable_exception() as ex:
1071+
with self.assertRaises((ZeroDivisionError, MemoryError)) as cm:
1072+
_testcapi.call_with_nomemory(n, n + 1, f, lst)
1073+
if ex.unraisable is None:
1074+
continue
1075+
err_msg = ex.unraisable.err_msg
1076+
seen.append((n, type(cm.exception), err_msg))
1077+
if (err_msg ==
1078+
'Exception ignored while clearing an object managed dict'):
1079+
self.assertIs(type(cm.exception), ZeroDivisionError)
1080+
break
1081+
else:
1082+
self.fail("MemoryError was not raised while detaching "
1083+
f"the dictionary: {seen}")
1084+
10501085
class DefinitionOrderTests(unittest.TestCase):
10511086
# PEP 520: Preserving Class Attribute Definition Order
10521087

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix a :exc:`SystemError` (a fatal error in a debug build) when an object
2+
whose :attr:`~object.__dict__` has been accessed is freed while an exception
3+
propagates and detaching the dictionary fails with :exc:`MemoryError`. The
4+
propagating exception is now kept. Patch by Christian Aurich Zanettini Martins.

‎Objects/dictobject.c‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8018,6 +8018,9 @@ PyObject_ClearManagedDict(PyObject *obj)
80188018
// We have a materialized object which points at the inline
80198019
// values. We need to materialize the keys. Nothing can modify
80208020
// this object, but we need to lock the dictionary.
8021+
// The object can be freed while an exception is propagating:
8022+
// don't let a MemoryError from detaching the dict replace it.
8023+
PyObject *exc = PyErr_GetRaisedException();
80218024
int err;
80228025
Py_BEGIN_CRITICAL_SECTION(dict);
80238026
err = detach_dict_from_object(dict, obj);
@@ -8038,6 +8041,7 @@ PyObject_ClearManagedDict(PyObject *obj)
80388041
clear_inline_values(_PyObject_InlineValues(obj));
80398042
Py_END_CRITICAL_SECTION();
80408043
}
8044+
PyErr_SetRaisedException(exc);
80418045
}
80428046
}
80438047
Py_CLEAR(_PyObject_ManagedDictPointer(obj)->dict);

0 commit comments

Comments
 (0)