Skip to content
Merged
26 changes: 26 additions & 0 deletions Lib/test/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,32 @@ def testMemoryErrorBigSource(self, size):
with self.assertRaisesRegex(OverflowError, "Parser column offset overflow"):
compile(src, '<fragment>', 'exec')

@cpython_only
# 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.
user_input = dedent("""
import atexit
import _testcapi

def callback():
pass

atexit.register(callback)
# Simulate low memory condition
_testcapi.set_nomemory(0)
""")
with SuppressCrashReport():
with script_helper.spawn_python('-c', user_input) as p:
p.wait()
output = p.stdout.read()

# The key point is that the process should exit (not hang)
self.assertIn(p.returncode, (0, 1))

@cpython_only
def testSettingException(self):
# test that setting an exception at the C level works even if the
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix: ``atexit_callfuncs`` need to atexit_cleanup the state when copy is NULL
to avoid the low memory error then recursive error.
2 changes: 2 additions & 0 deletions Modules/atexitmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ atexit_callfuncs(struct atexit_state *state)
{
PyErr_FormatUnraisable("Exception ignored while "
"copying atexit callbacks");
// gh-140080: need to cleanup to prevent recursive when low memory
atexit_cleanup(state);
return;
}

Expand Down
Loading