From a05aece5438409fd76f193c564a34e2ca63e278f Mon Sep 17 00:00:00 2001 From: yihong Date: Wed, 15 Oct 2025 21:49:55 +0800 Subject: [PATCH] gh-140080: Clear `atexit` callbacks when memory allocation fails during finalization (GH-140103) This fixes a regression introduced by GH-136004, in which finalization would hang while executing atexit handlers if the system was out of memory. --------- Signed-off-by: yihong0618 Co-authored-by: Peter Bierma Co-authored-by: Victor Stinner --- Lib/test/test_atexit.py | 35 ++++++++++++++++++- ...-10-14-20-18-31.gh-issue-140080.8ROjxW.rst | 1 + Modules/atexitmodule.c | 1 + 3 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-10-14-20-18-31.gh-issue-140080.8ROjxW.rst diff --git a/Lib/test/test_atexit.py b/Lib/test/test_atexit.py index 66142a108d5..8256ff183f2 100644 --- a/Lib/test/test_atexit.py +++ b/Lib/test/test_atexit.py @@ -1,9 +1,11 @@ import atexit import os +import subprocess import textwrap import unittest from test import support -from test.support import script_helper +from test.support import SuppressCrashReport, script_helper +from test.support import os_helper from test.support import threading_helper class GeneralTest(unittest.TestCase): @@ -189,6 +191,37 @@ def callback(): self.assertEqual(os.read(r, len(expected)), expected) os.close(r) + # Python built with Py_TRACE_REFS fail with a fatal error in + # _PyRefchain_Trace() on memory allocation error. + @unittest.skipIf(support.Py_TRACE_REFS, 'cannot test Py_TRACE_REFS build') + def test_atexit_with_low_memory(self): + # gh-140080: Test that setting low memory after registering an atexit + # callback doesn't cause an infinite loop during finalization. + code = textwrap.dedent(""" + import atexit + import _testcapi + + def callback(): + print("hello") + + atexit.register(callback) + # Simulate low memory condition + _testcapi.set_nomemory(0) + """) + + with os_helper.temp_dir() as temp_dir: + script = script_helper.make_script(temp_dir, 'test_atexit_script', code) + with SuppressCrashReport(): + with script_helper.spawn_python(script, + stderr=subprocess.PIPE) as proc: + proc.wait() + stdout = proc.stdout.read() + stderr = proc.stderr.read() + + self.assertIn(proc.returncode, (0, 1)) + self.assertNotIn(b"hello", stdout) + self.assertIn(b"MemoryError", stderr) + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-10-14-20-18-31.gh-issue-140080.8ROjxW.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-14-20-18-31.gh-issue-140080.8ROjxW.rst new file mode 100644 index 00000000000..0ddcea57f9d --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-14-20-18-31.gh-issue-140080.8ROjxW.rst @@ -0,0 +1 @@ +Fix hang during finalization when attempting to call :mod:`atexit` handlers under no memory. diff --git a/Modules/atexitmodule.c b/Modules/atexitmodule.c index 4b068967a6c..4536b03fbc4 100644 --- a/Modules/atexitmodule.c +++ b/Modules/atexitmodule.c @@ -112,6 +112,7 @@ atexit_callfuncs(struct atexit_state *state) { PyErr_FormatUnraisable("Exception ignored while " "copying atexit callbacks"); + atexit_cleanup(state); return; }