From 1202ec664ca94200d2968be3d632033c0dcaacd4 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Thu, 4 Sep 2025 09:55:29 +0800 Subject: [PATCH 01/29] fix: exec('_testcapi.set_nomemory(0)') hang in new repl 3.13 Signed-off-by: yihong0618 --- Python/ceval.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Python/ceval.c b/Python/ceval.c index 8c0cb29863cc60..f9d809314dffcc 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -912,7 +912,14 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int int frame_lasti = _PyInterpreterFrame_LASTI(frame); PyObject *lasti = PyLong_FromLong(frame_lasti); if (lasti == NULL) { - goto exception_unwind; + // gh-134163 + // If we can't allocate memory for lasti during exception handling, + // this likely means we're in a severe memory shortage situation. + // Instead of going back to exception_unwind (which would cause + // infinite recursion), directly exit to let the original exception + // propagate up and hopefully be handled at a higher level. + _PyFrame_SetStackPointer(frame, stack_pointer); + goto exit_unwind; } PUSH(lasti); } From 4cd38e8c87d7ffe38b6ca3a9980bd6c25369f698 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Thu, 4 Sep 2025 11:52:38 +0800 Subject: [PATCH 02/29] chore: add news for it Signed-off-by: yihong0618 --- .../2025-09-04-11-52-23.gh-issue-134163.EqKyn8.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-09-04-11-52-23.gh-issue-134163.EqKyn8.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-09-04-11-52-23.gh-issue-134163.EqKyn8.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-09-04-11-52-23.gh-issue-134163.EqKyn8.rst new file mode 100644 index 00000000000000..917cdc0e405f85 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-09-04-11-52-23.gh-issue-134163.EqKyn8.rst @@ -0,0 +1 @@ +fix in default repl exec('_testcapi.set_nomemory(0)') hang forever. From e55701780b2108e2010838502236c82f4dd73d4b Mon Sep 17 00:00:00 2001 From: yihong Date: Thu, 4 Sep 2025 10:34:20 +0800 Subject: [PATCH 03/29] Update Python/ceval.c Co-authored-by: Peter Bierma --- Python/ceval.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Python/ceval.c b/Python/ceval.c index f9d809314dffcc..b2fe62b89d16e8 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -912,8 +912,7 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int int frame_lasti = _PyInterpreterFrame_LASTI(frame); PyObject *lasti = PyLong_FromLong(frame_lasti); if (lasti == NULL) { - // gh-134163 - // If we can't allocate memory for lasti during exception handling, + // gh-134163: If we can't allocate memory for lasti during exception handling, // this likely means we're in a severe memory shortage situation. // Instead of going back to exception_unwind (which would cause // infinite recursion), directly exit to let the original exception From 53f713cfb427a249c734459d1dee24b3212a817f Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Thu, 4 Sep 2025 15:59:32 +0800 Subject: [PATCH 04/29] fix: add unittest for it Signed-off-by: yihong0618 --- Lib/test/test_repl.py | 65 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index 47e041ef8273a1..390efcd5750ee2 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -98,6 +98,71 @@ def test_no_memory(self): # Exit code 120: Py_FinalizeEx() failed to flush stdout and stderr. self.assertIn(p.returncode, (1, 120)) + @cpython_only + def test_issue134163_exec_hang(self): + # Complete reproduction code that simulates REPL exec() behavior + # note these prints are used can not drop + user_input = dedent(""" + exec(''' + def test_repl_hanging(): + print("=" * 60) + print("Reproducing gh-134163 outside REPL") + print("Simulating interactive interpreter exec() behavior") + print("=" * 60) + print() + + # First, import and set up the memory failure condition + print("Step 1: Setting up memory allocation failure...") + print("Step 2: Preparing code that will trigger exception handling...") + # Create a code object that will cause exception handling + # This simulates what happens when REPL executes user input + test_code = \"\"\" + # This code will trigger the problematic code path + # by causing an exception during execution when memory is constrained + import _testcapi + _testcapi.set_nomemory(0) # This line triggers the hang condition + \"\"\" + print("Step 3: Compiling test code...") + try: + compiled_code = compile(test_code, "", "exec") + except Exception as e: + print(f"Compilation failed: {e}") + exit(1) + print("Step 4: Executing code that triggers the hang condition...") + print("BEFORE FIX: This would hang indefinitely") + print("AFTER FIX: This should exit gracefully") + print() + try: + exec(compiled_code, {"__name__": "__console__"}) + print("Code executed successfully (unexpected)") + except SystemExit: + print("SystemExit caught - re-raising") + raise + except Exception as e: + print(f"Exception caught during exec(): {type(e).__name__}: {e}") + print("This is the expected path - exception handling should work normally") + # The showtraceback() equivalent would be called here in real REPL + import traceback + traceback.print_exc() + + test_repl_hanging() + ''') + """) + p = spawn_repl() + with SuppressCrashReport(): + p.stdin.write(user_input) + output = kill_python(p) + # The test should complete without hanging and show expected output + # We expect either successful completion or controlled failure + self.assertIn(p.returncode, (0, 1, 120)) + # Verify that the simulation steps were executed or that we got the expected memory error output + # The key test is that it doesn't hang - if we get output, the test passed + # Look for either the expected reproduction output or memory error indicators + has_reproduction_output = "Reproducing gh-134163 outside REPL" in output + has_memory_error_output = "object type name: MemoryError" in output + self.assertTrue(has_reproduction_output or has_memory_error_output, + f"Expected either reproduction output or memory error output, got: {output[:500]}...") + @cpython_only def test_multiline_string_parsing(self): # bpo-39209: Multiline string tokens need to be handled in the tokenizer From 9cc89f1c9fed3be77cc334b3552cdb1926fbfb9a Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Thu, 4 Sep 2025 22:21:05 +0800 Subject: [PATCH 05/29] fix: make test info better Signed-off-by: yihong0618 --- Lib/test/test_repl.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index 390efcd5750ee2..af8377f65d33f2 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -99,9 +99,9 @@ def test_no_memory(self): self.assertIn(p.returncode, (1, 120)) @cpython_only - def test_issue134163_exec_hang(self): - # Complete reproduction code that simulates REPL exec() behavior - # note these prints are used can not drop + def test_exec_set_nomemory_hang(self): + # gh-134163 Complete reproduction code that simulates REPL exec() no memory hang + # note these print are used can not drop for trigger the malloc user_input = dedent(""" exec(''' def test_repl_hanging(): @@ -152,8 +152,7 @@ def test_repl_hanging(): with SuppressCrashReport(): p.stdin.write(user_input) output = kill_python(p) - # The test should complete without hanging and show expected output - # We expect either successful completion or controlled failure + self.assertIn(p.returncode, (0, 1, 120)) # Verify that the simulation steps were executed or that we got the expected memory error output # The key test is that it doesn't hang - if we get output, the test passed From a9b6f96f5e7b8604687508aad6ccda1e0878145e Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Fri, 5 Sep 2025 08:29:36 +0800 Subject: [PATCH 06/29] fix: better test without print Signed-off-by: yihong0618 --- Lib/test/test_repl.py | 79 +++++++++++++++---------------------------- 1 file changed, 27 insertions(+), 52 deletions(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index af8377f65d33f2..8265fb2500082d 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -100,53 +100,29 @@ def test_no_memory(self): @cpython_only def test_exec_set_nomemory_hang(self): - # gh-134163 Complete reproduction code that simulates REPL exec() no memory hang - # note these print are used can not drop for trigger the malloc + # gh-134163 Test case that triggers no memory hang condition user_input = dedent(""" - exec(''' - def test_repl_hanging(): - print("=" * 60) - print("Reproducing gh-134163 outside REPL") - print("Simulating interactive interpreter exec() behavior") - print("=" * 60) - print() - - # First, import and set up the memory failure condition - print("Step 1: Setting up memory allocation failure...") - print("Step 2: Preparing code that will trigger exception handling...") - # Create a code object that will cause exception handling - # This simulates what happens when REPL executes user input - test_code = \"\"\" - # This code will trigger the problematic code path - # by causing an exception during execution when memory is constrained - import _testcapi - _testcapi.set_nomemory(0) # This line triggers the hang condition - \"\"\" - print("Step 3: Compiling test code...") - try: - compiled_code = compile(test_code, "", "exec") - except Exception as e: - print(f"Compilation failed: {e}") - exit(1) - print("Step 4: Executing code that triggers the hang condition...") - print("BEFORE FIX: This would hang indefinitely") - print("AFTER FIX: This should exit gracefully") - print() - try: - exec(compiled_code, {"__name__": "__console__"}) - print("Code executed successfully (unexpected)") - except SystemExit: - print("SystemExit caught - re-raising") - raise - except Exception as e: - print(f"Exception caught during exec(): {type(e).__name__}: {e}") - print("This is the expected path - exception handling should work normally") - # The showtraceback() equivalent would be called here in real REPL - import traceback - traceback.print_exc() - - test_repl_hanging() - ''') + a1 = list(range(1000, 2000)) + a2 = list(range(1000, 2000)) + a3 = list(range(1000, 2000)) + a4 = list(range(1000, 2000)) + a5 = list(range(1000, 2000)) + a6 = list(range(1000, 2000)) + a7 = list(range(1000, 2000)) + a8 = list(range(1000, 2000)) + a9 = list(range(1000, 2000)) + a10 = list(range(1000, 2000)) + a11 = list(range(1000, 2000)) + a12 = list(range(1000, 2000)) + a13 = list(range(1000, 2000)) + a14 = list(range(1000, 2000)) + try: + import _testcapi + _testcapi.set_nomemory(0) + b = list(range(1000, 2000)) + except Exception as e: + import traceback + traceback.print_exc() """) p = spawn_repl() with SuppressCrashReport(): @@ -154,13 +130,12 @@ def test_repl_hanging(): output = kill_python(p) self.assertIn(p.returncode, (0, 1, 120)) - # Verify that the simulation steps were executed or that we got the expected memory error output # The key test is that it doesn't hang - if we get output, the test passed - # Look for either the expected reproduction output or memory error indicators - has_reproduction_output = "Reproducing gh-134163 outside REPL" in output - has_memory_error_output = "object type name: MemoryError" in output - self.assertTrue(has_reproduction_output or has_memory_error_output, - f"Expected either reproduction output or memory error output, got: {output[:500]}...") + # Look for either successful execution or memory error indicators + has_traceback = "Traceback" in output + has_memory_error = "MemoryError" in output + # Either we get a traceback (expected) or we complete without hanging + self.assertTrue(len(output) > 0, f"Expected some output, got: {output}") # At minimum, should not hang @cpython_only def test_multiline_string_parsing(self): From 66ec42e69339c9e180907de43e12a7c3b2b0ccfb Mon Sep 17 00:00:00 2001 From: yihong Date: Fri, 5 Sep 2025 09:24:40 +0800 Subject: [PATCH 07/29] Update Misc/NEWS.d/next/Core_and_Builtins/2025-09-04-11-52-23.gh-issue-134163.EqKyn8.rst Co-authored-by: Peter Bierma --- .../2025-09-04-11-52-23.gh-issue-134163.EqKyn8.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-09-04-11-52-23.gh-issue-134163.EqKyn8.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-09-04-11-52-23.gh-issue-134163.EqKyn8.rst index 917cdc0e405f85..24e8efda2b627d 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2025-09-04-11-52-23.gh-issue-134163.EqKyn8.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-09-04-11-52-23.gh-issue-134163.EqKyn8.rst @@ -1 +1 @@ -fix in default repl exec('_testcapi.set_nomemory(0)') hang forever. +Fix a hang when the process is out of memory inside an exception handler. From a9e6b64a153f5e22e58a3786360aed512d480fae Mon Sep 17 00:00:00 2001 From: yihong Date: Fri, 5 Sep 2025 09:24:47 +0800 Subject: [PATCH 08/29] Update Lib/test/test_repl.py Co-authored-by: Peter Bierma --- Lib/test/test_repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index 8265fb2500082d..cc9eb5b44d69fb 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -100,7 +100,7 @@ def test_no_memory(self): @cpython_only def test_exec_set_nomemory_hang(self): - # gh-134163 Test case that triggers no memory hang condition + # gh-134163: Test case that triggers no memory hang condition user_input = dedent(""" a1 = list(range(1000, 2000)) a2 = list(range(1000, 2000)) From 791ca86832019743e1b6ff801a321429d0e85ef0 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Fri, 5 Sep 2025 09:32:40 +0800 Subject: [PATCH 09/29] fix: address comments Signed-off-by: yihong0618 --- Lib/test/test_repl.py | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index cc9eb5b44d69fb..d58cd4c70a061f 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -102,20 +102,11 @@ def test_no_memory(self): def test_exec_set_nomemory_hang(self): # gh-134163: Test case that triggers no memory hang condition user_input = dedent(""" - a1 = list(range(1000, 2000)) - a2 = list(range(1000, 2000)) - a3 = list(range(1000, 2000)) - a4 = list(range(1000, 2000)) - a5 = list(range(1000, 2000)) - a6 = list(range(1000, 2000)) - a7 = list(range(1000, 2000)) - a8 = list(range(1000, 2000)) - a9 = list(range(1000, 2000)) - a10 = list(range(1000, 2000)) - a11 = list(range(1000, 2000)) - a12 = list(range(1000, 2000)) - a13 = list(range(1000, 2000)) - a14 = list(range(1000, 2000)) + # The frame_lasti need to upper 257, + # because when calling PyLong_FromLong, malloc is not invoked, + # so no MemError is triggered + # we need to warm up the memory to reproduce the issue + "a = list(range(0, 1))\n" * 20 try: import _testcapi _testcapi.set_nomemory(0) @@ -130,12 +121,7 @@ def test_exec_set_nomemory_hang(self): output = kill_python(p) self.assertIn(p.returncode, (0, 1, 120)) - # The key test is that it doesn't hang - if we get output, the test passed - # Look for either successful execution or memory error indicators - has_traceback = "Traceback" in output - has_memory_error = "MemoryError" in output - # Either we get a traceback (expected) or we complete without hanging - self.assertTrue(len(output) > 0, f"Expected some output, got: {output}") # At minimum, should not hang + self.assertGreater(len(output), 0, f"Expected some output, got: {output}") # At minimum, should not hang @cpython_only def test_multiline_string_parsing(self): From 4827680412a5e6613e84ef3df706356780ea77b6 Mon Sep 17 00:00:00 2001 From: yihong Date: Fri, 5 Sep 2025 10:03:24 +0800 Subject: [PATCH 10/29] Update Lib/test/test_repl.py Co-authored-by: Peter Bierma --- Lib/test/test_repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index d58cd4c70a061f..a5afe56f5a5e15 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -121,7 +121,7 @@ def test_exec_set_nomemory_hang(self): output = kill_python(p) self.assertIn(p.returncode, (0, 1, 120)) - self.assertGreater(len(output), 0, f"Expected some output, got: {output}") # At minimum, should not hang + self.assertGreater(len(output), 0) # At minimum, should not hang @cpython_only def test_multiline_string_parsing(self): From 8ec57694f0971ae7c647c08023f7aa7bb39c7a4f Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Fri, 5 Sep 2025 10:05:45 +0800 Subject: [PATCH 11/29] fix: follow up another comments Signed-off-by: yihong0618 --- Lib/test/test_repl.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index a5afe56f5a5e15..06438712a4b345 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -101,12 +101,12 @@ def test_no_memory(self): @cpython_only def test_exec_set_nomemory_hang(self): # gh-134163: Test case that triggers no memory hang condition - user_input = dedent(""" - # The frame_lasti need to upper 257, - # because when calling PyLong_FromLong, malloc is not invoked, - # so no MemError is triggered - # we need to warm up the memory to reproduce the issue - "a = list(range(0, 1))\n" * 20 + # The frame_lasti need to upper 257, + # because when calling PyLong_FromLong, malloc is not invoked, + # so no MemError is triggered + # we need to warm up the memory to reproduce the issue + warmup_code = "a = list(range(0, 1))\n" * 20 + user_input = warmup_code + dedent(""" try: import _testcapi _testcapi.set_nomemory(0) @@ -122,6 +122,7 @@ def test_exec_set_nomemory_hang(self): self.assertIn(p.returncode, (0, 1, 120)) self.assertGreater(len(output), 0) # At minimum, should not hang + self.assertIn("MemoryError", output) @cpython_only def test_multiline_string_parsing(self): From edcfff75caa944c24e3c51d77ae83c514b46af9e Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Sun, 7 Sep 2025 09:31:33 +0800 Subject: [PATCH 12/29] fix: should set the error when no memory Signed-off-by: yihong0618 --- Python/ceval.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Python/ceval.c b/Python/ceval.c index b2fe62b89d16e8..a9188b2a5ead15 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -914,6 +914,11 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int if (lasti == NULL) { // gh-134163: If we can't allocate memory for lasti during exception handling, // this likely means we're in a severe memory shortage situation. + if (!_PyErr_Occurred(tstate)) { + // PyLong_FromLong should have set an exception + // like _testcapi.set_nomemory(0), it might not. Ensure one is set. + PyErr_NoMemory(); + } // Instead of going back to exception_unwind (which would cause // infinite recursion), directly exit to let the original exception // propagate up and hopefully be handled at a higher level. From 81c62448a0a68b874ae1db660c4c03a76c1650be Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Thu, 4 Sep 2025 09:55:29 +0800 Subject: [PATCH 13/29] fix: exec('_testcapi.set_nomemory(0)') hang in new repl 3.13 Signed-off-by: yihong0618 --- Python/ceval.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Python/ceval.c b/Python/ceval.c index 8c0cb29863cc60..f9d809314dffcc 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -912,7 +912,14 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int int frame_lasti = _PyInterpreterFrame_LASTI(frame); PyObject *lasti = PyLong_FromLong(frame_lasti); if (lasti == NULL) { - goto exception_unwind; + // gh-134163 + // If we can't allocate memory for lasti during exception handling, + // this likely means we're in a severe memory shortage situation. + // Instead of going back to exception_unwind (which would cause + // infinite recursion), directly exit to let the original exception + // propagate up and hopefully be handled at a higher level. + _PyFrame_SetStackPointer(frame, stack_pointer); + goto exit_unwind; } PUSH(lasti); } From 847bd7bb3302262d0c94f225d3b2fa077420197f Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Thu, 4 Sep 2025 11:52:38 +0800 Subject: [PATCH 14/29] chore: add news for it Signed-off-by: yihong0618 --- .../2025-09-04-11-52-23.gh-issue-134163.EqKyn8.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-09-04-11-52-23.gh-issue-134163.EqKyn8.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-09-04-11-52-23.gh-issue-134163.EqKyn8.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-09-04-11-52-23.gh-issue-134163.EqKyn8.rst new file mode 100644 index 00000000000000..917cdc0e405f85 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-09-04-11-52-23.gh-issue-134163.EqKyn8.rst @@ -0,0 +1 @@ +fix in default repl exec('_testcapi.set_nomemory(0)') hang forever. From ff3c7f49aa0b8dccddb2167b7e22519835d8f5da Mon Sep 17 00:00:00 2001 From: yihong Date: Thu, 4 Sep 2025 10:34:20 +0800 Subject: [PATCH 15/29] Update Python/ceval.c Co-authored-by: Peter Bierma --- Python/ceval.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Python/ceval.c b/Python/ceval.c index f9d809314dffcc..b2fe62b89d16e8 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -912,8 +912,7 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int int frame_lasti = _PyInterpreterFrame_LASTI(frame); PyObject *lasti = PyLong_FromLong(frame_lasti); if (lasti == NULL) { - // gh-134163 - // If we can't allocate memory for lasti during exception handling, + // gh-134163: If we can't allocate memory for lasti during exception handling, // this likely means we're in a severe memory shortage situation. // Instead of going back to exception_unwind (which would cause // infinite recursion), directly exit to let the original exception From 175a13ea1ef13f3c3bd961c4d48fc24182e5c232 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Thu, 4 Sep 2025 15:59:32 +0800 Subject: [PATCH 16/29] fix: add unittest for it Signed-off-by: yihong0618 --- Lib/test/test_repl.py | 65 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index 47e041ef8273a1..390efcd5750ee2 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -98,6 +98,71 @@ def test_no_memory(self): # Exit code 120: Py_FinalizeEx() failed to flush stdout and stderr. self.assertIn(p.returncode, (1, 120)) + @cpython_only + def test_issue134163_exec_hang(self): + # Complete reproduction code that simulates REPL exec() behavior + # note these prints are used can not drop + user_input = dedent(""" + exec(''' + def test_repl_hanging(): + print("=" * 60) + print("Reproducing gh-134163 outside REPL") + print("Simulating interactive interpreter exec() behavior") + print("=" * 60) + print() + + # First, import and set up the memory failure condition + print("Step 1: Setting up memory allocation failure...") + print("Step 2: Preparing code that will trigger exception handling...") + # Create a code object that will cause exception handling + # This simulates what happens when REPL executes user input + test_code = \"\"\" + # This code will trigger the problematic code path + # by causing an exception during execution when memory is constrained + import _testcapi + _testcapi.set_nomemory(0) # This line triggers the hang condition + \"\"\" + print("Step 3: Compiling test code...") + try: + compiled_code = compile(test_code, "", "exec") + except Exception as e: + print(f"Compilation failed: {e}") + exit(1) + print("Step 4: Executing code that triggers the hang condition...") + print("BEFORE FIX: This would hang indefinitely") + print("AFTER FIX: This should exit gracefully") + print() + try: + exec(compiled_code, {"__name__": "__console__"}) + print("Code executed successfully (unexpected)") + except SystemExit: + print("SystemExit caught - re-raising") + raise + except Exception as e: + print(f"Exception caught during exec(): {type(e).__name__}: {e}") + print("This is the expected path - exception handling should work normally") + # The showtraceback() equivalent would be called here in real REPL + import traceback + traceback.print_exc() + + test_repl_hanging() + ''') + """) + p = spawn_repl() + with SuppressCrashReport(): + p.stdin.write(user_input) + output = kill_python(p) + # The test should complete without hanging and show expected output + # We expect either successful completion or controlled failure + self.assertIn(p.returncode, (0, 1, 120)) + # Verify that the simulation steps were executed or that we got the expected memory error output + # The key test is that it doesn't hang - if we get output, the test passed + # Look for either the expected reproduction output or memory error indicators + has_reproduction_output = "Reproducing gh-134163 outside REPL" in output + has_memory_error_output = "object type name: MemoryError" in output + self.assertTrue(has_reproduction_output or has_memory_error_output, + f"Expected either reproduction output or memory error output, got: {output[:500]}...") + @cpython_only def test_multiline_string_parsing(self): # bpo-39209: Multiline string tokens need to be handled in the tokenizer From d952d53977c49d759f4d8ee3ab572d37c9c4bc9a Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Thu, 4 Sep 2025 22:21:05 +0800 Subject: [PATCH 17/29] fix: make test info better Signed-off-by: yihong0618 --- Lib/test/test_repl.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index 390efcd5750ee2..af8377f65d33f2 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -99,9 +99,9 @@ def test_no_memory(self): self.assertIn(p.returncode, (1, 120)) @cpython_only - def test_issue134163_exec_hang(self): - # Complete reproduction code that simulates REPL exec() behavior - # note these prints are used can not drop + def test_exec_set_nomemory_hang(self): + # gh-134163 Complete reproduction code that simulates REPL exec() no memory hang + # note these print are used can not drop for trigger the malloc user_input = dedent(""" exec(''' def test_repl_hanging(): @@ -152,8 +152,7 @@ def test_repl_hanging(): with SuppressCrashReport(): p.stdin.write(user_input) output = kill_python(p) - # The test should complete without hanging and show expected output - # We expect either successful completion or controlled failure + self.assertIn(p.returncode, (0, 1, 120)) # Verify that the simulation steps were executed or that we got the expected memory error output # The key test is that it doesn't hang - if we get output, the test passed From 5fa3a1b7f43cceb04de8f7fcb8a592eb6d8bb0a3 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Fri, 5 Sep 2025 08:29:36 +0800 Subject: [PATCH 18/29] fix: better test without print Signed-off-by: yihong0618 --- Lib/test/test_repl.py | 79 +++++++++++++++---------------------------- 1 file changed, 27 insertions(+), 52 deletions(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index af8377f65d33f2..8265fb2500082d 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -100,53 +100,29 @@ def test_no_memory(self): @cpython_only def test_exec_set_nomemory_hang(self): - # gh-134163 Complete reproduction code that simulates REPL exec() no memory hang - # note these print are used can not drop for trigger the malloc + # gh-134163 Test case that triggers no memory hang condition user_input = dedent(""" - exec(''' - def test_repl_hanging(): - print("=" * 60) - print("Reproducing gh-134163 outside REPL") - print("Simulating interactive interpreter exec() behavior") - print("=" * 60) - print() - - # First, import and set up the memory failure condition - print("Step 1: Setting up memory allocation failure...") - print("Step 2: Preparing code that will trigger exception handling...") - # Create a code object that will cause exception handling - # This simulates what happens when REPL executes user input - test_code = \"\"\" - # This code will trigger the problematic code path - # by causing an exception during execution when memory is constrained - import _testcapi - _testcapi.set_nomemory(0) # This line triggers the hang condition - \"\"\" - print("Step 3: Compiling test code...") - try: - compiled_code = compile(test_code, "", "exec") - except Exception as e: - print(f"Compilation failed: {e}") - exit(1) - print("Step 4: Executing code that triggers the hang condition...") - print("BEFORE FIX: This would hang indefinitely") - print("AFTER FIX: This should exit gracefully") - print() - try: - exec(compiled_code, {"__name__": "__console__"}) - print("Code executed successfully (unexpected)") - except SystemExit: - print("SystemExit caught - re-raising") - raise - except Exception as e: - print(f"Exception caught during exec(): {type(e).__name__}: {e}") - print("This is the expected path - exception handling should work normally") - # The showtraceback() equivalent would be called here in real REPL - import traceback - traceback.print_exc() - - test_repl_hanging() - ''') + a1 = list(range(1000, 2000)) + a2 = list(range(1000, 2000)) + a3 = list(range(1000, 2000)) + a4 = list(range(1000, 2000)) + a5 = list(range(1000, 2000)) + a6 = list(range(1000, 2000)) + a7 = list(range(1000, 2000)) + a8 = list(range(1000, 2000)) + a9 = list(range(1000, 2000)) + a10 = list(range(1000, 2000)) + a11 = list(range(1000, 2000)) + a12 = list(range(1000, 2000)) + a13 = list(range(1000, 2000)) + a14 = list(range(1000, 2000)) + try: + import _testcapi + _testcapi.set_nomemory(0) + b = list(range(1000, 2000)) + except Exception as e: + import traceback + traceback.print_exc() """) p = spawn_repl() with SuppressCrashReport(): @@ -154,13 +130,12 @@ def test_repl_hanging(): output = kill_python(p) self.assertIn(p.returncode, (0, 1, 120)) - # Verify that the simulation steps were executed or that we got the expected memory error output # The key test is that it doesn't hang - if we get output, the test passed - # Look for either the expected reproduction output or memory error indicators - has_reproduction_output = "Reproducing gh-134163 outside REPL" in output - has_memory_error_output = "object type name: MemoryError" in output - self.assertTrue(has_reproduction_output or has_memory_error_output, - f"Expected either reproduction output or memory error output, got: {output[:500]}...") + # Look for either successful execution or memory error indicators + has_traceback = "Traceback" in output + has_memory_error = "MemoryError" in output + # Either we get a traceback (expected) or we complete without hanging + self.assertTrue(len(output) > 0, f"Expected some output, got: {output}") # At minimum, should not hang @cpython_only def test_multiline_string_parsing(self): From d3274fb85990dbdba134e757929405d1ea623d8f Mon Sep 17 00:00:00 2001 From: yihong Date: Fri, 5 Sep 2025 09:24:40 +0800 Subject: [PATCH 19/29] Update Misc/NEWS.d/next/Core_and_Builtins/2025-09-04-11-52-23.gh-issue-134163.EqKyn8.rst Co-authored-by: Peter Bierma --- .../2025-09-04-11-52-23.gh-issue-134163.EqKyn8.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-09-04-11-52-23.gh-issue-134163.EqKyn8.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-09-04-11-52-23.gh-issue-134163.EqKyn8.rst index 917cdc0e405f85..24e8efda2b627d 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2025-09-04-11-52-23.gh-issue-134163.EqKyn8.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-09-04-11-52-23.gh-issue-134163.EqKyn8.rst @@ -1 +1 @@ -fix in default repl exec('_testcapi.set_nomemory(0)') hang forever. +Fix a hang when the process is out of memory inside an exception handler. From c89c5b57b7a471766d0daeef56b4f6deb0d531db Mon Sep 17 00:00:00 2001 From: yihong Date: Fri, 5 Sep 2025 09:24:47 +0800 Subject: [PATCH 20/29] Update Lib/test/test_repl.py Co-authored-by: Peter Bierma --- Lib/test/test_repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index 8265fb2500082d..cc9eb5b44d69fb 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -100,7 +100,7 @@ def test_no_memory(self): @cpython_only def test_exec_set_nomemory_hang(self): - # gh-134163 Test case that triggers no memory hang condition + # gh-134163: Test case that triggers no memory hang condition user_input = dedent(""" a1 = list(range(1000, 2000)) a2 = list(range(1000, 2000)) From 63a6b7d0e9b520548e9c6c4733d29793234a9169 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Fri, 5 Sep 2025 09:32:40 +0800 Subject: [PATCH 21/29] fix: address comments Signed-off-by: yihong0618 --- Lib/test/test_repl.py | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index cc9eb5b44d69fb..d58cd4c70a061f 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -102,20 +102,11 @@ def test_no_memory(self): def test_exec_set_nomemory_hang(self): # gh-134163: Test case that triggers no memory hang condition user_input = dedent(""" - a1 = list(range(1000, 2000)) - a2 = list(range(1000, 2000)) - a3 = list(range(1000, 2000)) - a4 = list(range(1000, 2000)) - a5 = list(range(1000, 2000)) - a6 = list(range(1000, 2000)) - a7 = list(range(1000, 2000)) - a8 = list(range(1000, 2000)) - a9 = list(range(1000, 2000)) - a10 = list(range(1000, 2000)) - a11 = list(range(1000, 2000)) - a12 = list(range(1000, 2000)) - a13 = list(range(1000, 2000)) - a14 = list(range(1000, 2000)) + # The frame_lasti need to upper 257, + # because when calling PyLong_FromLong, malloc is not invoked, + # so no MemError is triggered + # we need to warm up the memory to reproduce the issue + "a = list(range(0, 1))\n" * 20 try: import _testcapi _testcapi.set_nomemory(0) @@ -130,12 +121,7 @@ def test_exec_set_nomemory_hang(self): output = kill_python(p) self.assertIn(p.returncode, (0, 1, 120)) - # The key test is that it doesn't hang - if we get output, the test passed - # Look for either successful execution or memory error indicators - has_traceback = "Traceback" in output - has_memory_error = "MemoryError" in output - # Either we get a traceback (expected) or we complete without hanging - self.assertTrue(len(output) > 0, f"Expected some output, got: {output}") # At minimum, should not hang + self.assertGreater(len(output), 0, f"Expected some output, got: {output}") # At minimum, should not hang @cpython_only def test_multiline_string_parsing(self): From 977703fc3afa5d2612c86d1f02797e312e14ee2f Mon Sep 17 00:00:00 2001 From: yihong Date: Fri, 5 Sep 2025 10:03:24 +0800 Subject: [PATCH 22/29] Update Lib/test/test_repl.py Co-authored-by: Peter Bierma --- Lib/test/test_repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index d58cd4c70a061f..a5afe56f5a5e15 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -121,7 +121,7 @@ def test_exec_set_nomemory_hang(self): output = kill_python(p) self.assertIn(p.returncode, (0, 1, 120)) - self.assertGreater(len(output), 0, f"Expected some output, got: {output}") # At minimum, should not hang + self.assertGreater(len(output), 0) # At minimum, should not hang @cpython_only def test_multiline_string_parsing(self): From 095f865616bb79ff2ce2120e8d307f1ce60ba24c Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Fri, 5 Sep 2025 10:05:45 +0800 Subject: [PATCH 23/29] fix: follow up another comments Signed-off-by: yihong0618 --- Lib/test/test_repl.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index a5afe56f5a5e15..06438712a4b345 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -101,12 +101,12 @@ def test_no_memory(self): @cpython_only def test_exec_set_nomemory_hang(self): # gh-134163: Test case that triggers no memory hang condition - user_input = dedent(""" - # The frame_lasti need to upper 257, - # because when calling PyLong_FromLong, malloc is not invoked, - # so no MemError is triggered - # we need to warm up the memory to reproduce the issue - "a = list(range(0, 1))\n" * 20 + # The frame_lasti need to upper 257, + # because when calling PyLong_FromLong, malloc is not invoked, + # so no MemError is triggered + # we need to warm up the memory to reproduce the issue + warmup_code = "a = list(range(0, 1))\n" * 20 + user_input = warmup_code + dedent(""" try: import _testcapi _testcapi.set_nomemory(0) @@ -122,6 +122,7 @@ def test_exec_set_nomemory_hang(self): self.assertIn(p.returncode, (0, 1, 120)) self.assertGreater(len(output), 0) # At minimum, should not hang + self.assertIn("MemoryError", output) @cpython_only def test_multiline_string_parsing(self): From 932c15ede0c4502f1d89e98c8c269a6d256d4e22 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Sun, 7 Sep 2025 09:31:33 +0800 Subject: [PATCH 24/29] fix: should set the error when no memory Signed-off-by: yihong0618 --- Python/ceval.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Python/ceval.c b/Python/ceval.c index b2fe62b89d16e8..a9188b2a5ead15 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -914,6 +914,11 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int if (lasti == NULL) { // gh-134163: If we can't allocate memory for lasti during exception handling, // this likely means we're in a severe memory shortage situation. + if (!_PyErr_Occurred(tstate)) { + // PyLong_FromLong should have set an exception + // like _testcapi.set_nomemory(0), it might not. Ensure one is set. + PyErr_NoMemory(); + } // Instead of going back to exception_unwind (which would cause // infinite recursion), directly exit to let the original exception // propagate up and hopefully be handled at a higher level. From bb790af621078d2f9ac394fdcbd09b451a2146bc Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Tue, 9 Sep 2025 09:58:20 +0800 Subject: [PATCH 25/29] fix: in TraceRefs it will return fatal error Signed-off-by: yihong0618 --- Lib/test/test_repl.py | 7 +++++-- Python/ceval.c | 7 ------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index 06438712a4b345..1c27752ad4c545 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -120,9 +120,12 @@ def test_exec_set_nomemory_hang(self): p.stdin.write(user_input) output = kill_python(p) - self.assertIn(p.returncode, (0, 1, 120)) + # it can exit with MemoryError or Fatal Python error when --with-trace-refs + # it return -6 here + self.assertIn(p.returncode, (0, 1, 120, -6)) self.assertGreater(len(output), 0) # At minimum, should not hang - self.assertIn("MemoryError", output) + # Can result in either MemoryError exception or fatal error + self.assertTrue("MemoryError" in output or "Fatal Python error" in output) @cpython_only def test_multiline_string_parsing(self): diff --git a/Python/ceval.c b/Python/ceval.c index a9188b2a5ead15..511e39f0c2e2dd 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -912,13 +912,6 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int int frame_lasti = _PyInterpreterFrame_LASTI(frame); PyObject *lasti = PyLong_FromLong(frame_lasti); if (lasti == NULL) { - // gh-134163: If we can't allocate memory for lasti during exception handling, - // this likely means we're in a severe memory shortage situation. - if (!_PyErr_Occurred(tstate)) { - // PyLong_FromLong should have set an exception - // like _testcapi.set_nomemory(0), it might not. Ensure one is set. - PyErr_NoMemory(); - } // Instead of going back to exception_unwind (which would cause // infinite recursion), directly exit to let the original exception // propagate up and hopefully be handled at a higher level. From 06b779ac57e5a3a336f785660b371d31d23eae06 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Tue, 9 Sep 2025 15:59:31 +0800 Subject: [PATCH 26/29] fix: use support.Py_TRACE_REFS Signed-off-by: yihong0618 --- Lib/test/test_repl.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index 1c27752ad4c545..54c0125a7a1854 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -99,7 +99,11 @@ def test_no_memory(self): self.assertIn(p.returncode, (1, 120)) @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_exec_set_nomemory_hang(self): + import_module("_testcapi") # gh-134163: Test case that triggers no memory hang condition # The frame_lasti need to upper 257, # because when calling PyLong_FromLong, malloc is not invoked, @@ -120,12 +124,10 @@ def test_exec_set_nomemory_hang(self): p.stdin.write(user_input) output = kill_python(p) - # it can exit with MemoryError or Fatal Python error when --with-trace-refs - # it return -6 here - self.assertIn(p.returncode, (0, 1, 120, -6)) + self.assertIn(p.returncode, (0, 1, 120)) self.assertGreater(len(output), 0) # At minimum, should not hang # Can result in either MemoryError exception or fatal error - self.assertTrue("MemoryError" in output or "Fatal Python error" in output) + self.assertTrue("MemoryError" in output) @cpython_only def test_multiline_string_parsing(self): From 26ce31b9ffb0a5ac13499f9320a0e633035e3bba Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Wed, 10 Sep 2025 20:48:45 +0800 Subject: [PATCH 27/29] fix: use spawn_python Signed-off-by: yihong0618 --- Lib/test/test_repl.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index 37a5e8445c6283..4c7f1f2c656755 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -14,7 +14,7 @@ SuppressCrashReport, SHORT_TIMEOUT, ) -from test.support.script_helper import kill_python +from test.support.script_helper import kill_python, spawn_python from test.support.import_helper import import_module try: @@ -119,14 +119,13 @@ def test_exec_set_nomemory_hang(self): import traceback traceback.print_exc() """) - p = spawn_repl() with SuppressCrashReport(): - p.stdin.write(user_input) + p = spawn_python('-c', user_input) output = kill_python(p) self.assertIn(p.returncode, (0, 1, 120)) self.assertGreater(len(output), 0) # At minimum, should not hang - self.assertIn("MemoryError", output) + self.assertIn(b"MemoryError", output) @cpython_only def test_multiline_string_parsing(self): From 5ab08c788902bbe678fff27dc5ebce73d48d9b85 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Wed, 10 Sep 2025 21:14:53 +0800 Subject: [PATCH 28/29] fix: address comments Signed-off-by: yihong0618 --- Lib/test/test_exceptions.py | 30 ++++++++++++++++++++++++++++++ Lib/test/test_repl.py | 31 +------------------------------ 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index c91f66629483e5..5ee514fd00cb5a 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -1843,6 +1843,36 @@ def test_memory_error_in_subinterp(self): rc, _, err = script_helper.assert_python_ok("-c", code) self.assertIn(b'MemoryError', err) + @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_exec_set_nomemory_hang(self): + import_module("_testcapi") + # gh-134163: Test case that triggers no memory hang condition + # The frame_lasti need to upper 257, + # because when calling PyLong_FromLong, malloc is not invoked, + # so no MemError is triggered + # we need to warm up the memory to reproduce the issue + warmup_code = "a = list(range(0, 1))\n" * 20 + user_input = warmup_code + dedent(""" + try: + import _testcapi + _testcapi.set_nomemory(0) + b = list(range(1000, 2000)) + except Exception as e: + import traceback + traceback.print_exc() + """) + with SuppressCrashReport(): + with script_helper.spawn_python('-c', user_input) as p: + p.wait() + output = p.stdout.read() + + self.assertIn(p.returncode, (0, 1)) + self.assertGreater(len(output), 0) # At minimum, should not hang + self.assertIn(b"MemoryError", output) + class NameErrorTests(unittest.TestCase): def test_name_error_has_name(self): diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index 4c7f1f2c656755..47e041ef8273a1 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -14,7 +14,7 @@ SuppressCrashReport, SHORT_TIMEOUT, ) -from test.support.script_helper import kill_python, spawn_python +from test.support.script_helper import kill_python from test.support.import_helper import import_module try: @@ -98,35 +98,6 @@ def test_no_memory(self): # Exit code 120: Py_FinalizeEx() failed to flush stdout and stderr. self.assertIn(p.returncode, (1, 120)) - @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_exec_set_nomemory_hang(self): - import_module("_testcapi") - # gh-134163: Test case that triggers no memory hang condition - # The frame_lasti need to upper 257, - # because when calling PyLong_FromLong, malloc is not invoked, - # so no MemError is triggered - # we need to warm up the memory to reproduce the issue - warmup_code = "a = list(range(0, 1))\n" * 20 - user_input = warmup_code + dedent(""" - try: - import _testcapi - _testcapi.set_nomemory(0) - b = list(range(1000, 2000)) - except Exception as e: - import traceback - traceback.print_exc() - """) - with SuppressCrashReport(): - p = spawn_python('-c', user_input) - output = kill_python(p) - - self.assertIn(p.returncode, (0, 1, 120)) - self.assertGreater(len(output), 0) # At minimum, should not hang - self.assertIn(b"MemoryError", output) - @cpython_only def test_multiline_string_parsing(self): # bpo-39209: Multiline string tokens need to be handled in the tokenizer From 789040ceb9a8518df760dd2c5525f999a6ce9a92 Mon Sep 17 00:00:00 2001 From: yihong Date: Wed, 10 Sep 2025 22:48:00 +0800 Subject: [PATCH 29/29] Apply suggestions from code review Co-authored-by: Peter Bierma --- Lib/test/test_exceptions.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 5ee514fd00cb5a..939783956d62d6 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -1849,11 +1849,13 @@ def test_memory_error_in_subinterp(self): @unittest.skipIf(support.Py_TRACE_REFS, 'cannot test Py_TRACE_REFS build') def test_exec_set_nomemory_hang(self): import_module("_testcapi") - # gh-134163: Test case that triggers no memory hang condition - # The frame_lasti need to upper 257, - # because when calling PyLong_FromLong, malloc is not invoked, - # so no MemError is triggered - # we need to warm up the memory to reproduce the issue + # gh-134163: A MemoryError inside code that was wrapped by a try/except + # block would lead to an infinite loop. + + # The frame_lasti needs to be greater than 257 to prevent + # PyLong_FromLong() from returning cached integers, which + # don't require a memory allocation. Prepend some dummy code + # to artificially increase the instruction index. warmup_code = "a = list(range(0, 1))\n" * 20 user_input = warmup_code + dedent(""" try: