From 5e5297640f54c3c44271b7563ea6a39fca2662be Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Fri, 3 Jul 2026 22:35:30 +0300 Subject: [PATCH 1/2] gh-152083: Fix crash clearing a managed dict under low memory --- Lib/test/test_class.py | 30 +++++++++++++++++++ ...-07-03-22-32-58.gh-issue-152083.nSAuhO.rst | 2 ++ Objects/dictobject.c | 4 +++ 3 files changed, 36 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-07-03-22-32-58.gh-issue-152083.nSAuhO.rst diff --git a/Lib/test/test_class.py b/Lib/test/test_class.py index e401941c6d69700..51f99f9b0d492c9 100644 --- a/Lib/test/test_class.py +++ b/Lib/test/test_class.py @@ -1040,5 +1040,35 @@ def __init__(self): self.assertFalse(out, msg=out.decode('utf-8')) self.assertFalse(err, msg=err.decode('utf-8')) + @support.nomemtest + def test_clear_managed_dict_no_memory_keeps_exception(self): + # gh-152083: an exception may already be set when the managed dict is + # cleared under low memory. PyErr_FormatUnraisable() must not clear it. + code = """if 1: + import _testcapi + + class A: + def __init__(self): + self.a = 1 + self.b = 2 + + def f(): + a = A() + a.__dict__ + return [None] * 1000 + + for start in range(120): + _testcapi.set_nomemory(start) + try: + f() + except BaseException: + pass + _testcapi.remove_mem_hooks() + """ + rc, out, err = script_helper.assert_python_ok("-c", code) + self.assertEqual(rc, 0) + self.assertFalse(out, msg=out.decode('utf-8')) + self.assertFalse(err, msg=err.decode('utf-8')) + if __name__ == '__main__': unittest.main() diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-03-22-32-58.gh-issue-152083.nSAuhO.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-03-22-32-58.gh-issue-152083.nSAuhO.rst new file mode 100644 index 000000000000000..b8d3149f7a5cd98 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-03-22-32-58.gh-issue-152083.nSAuhO.rst @@ -0,0 +1,2 @@ +Fix a crash when an object's managed dictionary is cleared under low memory +while an exception is set. diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 6029205eac8b20f..eb559fdc6b373ef 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -7944,6 +7944,9 @@ PyObject_ClearManagedDict(PyObject *obj) // values. We need to materialize the keys. Nothing can modify // this object, but we need to lock the dictionary. int err; + // gh-152083: an exception may already be set, so keep it. + // The OOM path below reports via PyErr_FormatUnraisable() which clears it. + PyObject *exc = PyErr_GetRaisedException(); Py_BEGIN_CRITICAL_SECTION(dict); err = detach_dict_from_object(dict, obj); Py_END_CRITICAL_SECTION(); @@ -7963,6 +7966,7 @@ PyObject_ClearManagedDict(PyObject *obj) clear_inline_values(_PyObject_InlineValues(obj)); Py_END_CRITICAL_SECTION(); } + PyErr_SetRaisedException(exc); } } Py_CLEAR(_PyObject_ManagedDictPointer(obj)->dict); From a3d2714b0fbf84e2f7d259a13c3ae45f715f8420 Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Sun, 13 Sep 2026 14:25:27 +0300 Subject: [PATCH 2/2] rewrite test to @isolation.runInSubprocess() --- Lib/test/test_class.py | 48 +++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/Lib/test/test_class.py b/Lib/test/test_class.py index 7ba009e6f41bb4f..bd7b78b188ed9a4 100644 --- a/Lib/test/test_class.py +++ b/Lib/test/test_class.py @@ -1113,34 +1113,30 @@ def test_prepare_preserves_order(self): @support.nomemtest + @isolation.runInSubprocess() def test_clear_managed_dict_no_memory_keeps_exception(self): # gh-152083: an exception may already be set when the managed dict is - # cleared under low memory. PyErr_FormatUnraisable() must not clear it. - code = """if 1: - import _testcapi - - class A: - def __init__(self): - self.a = 1 - self.b = 2 - - def f(): - a = A() - a.__dict__ - return [None] * 1000 - - for start in range(120): - _testcapi.set_nomemory(start) - try: - f() - except BaseException: - pass - _testcapi.remove_mem_hooks() - """ - rc, out, err = script_helper.assert_python_ok("-c", code) - self.assertEqual(rc, 0) - self.assertFalse(out, msg=out.decode('utf-8')) - self.assertFalse(err, msg=err.decode('utf-8')) + # cleared under low memory. PyErr_FormatUnraisable() must not clear it. + import _testcapi + + class A: + def __init__(self): + self.a = 1 + self.b = 2 + + def f(): + a = A() + a.__dict__ + return [None] * 1000 + + for start in range(120): + _testcapi.set_nomemory(start) + try: + f() + except BaseException: + pass + _testcapi.remove_mem_hooks() + if __name__ == '__main__': unittest.main()