From a2c2a782aac93bda13b37cabb0f42bad4e965511 Mon Sep 17 00:00:00 2001 From: Mark Shannon Date: Tue, 12 Aug 2025 15:01:20 +0100 Subject: [PATCH 1/4] Check C stack depth before stack allocating JIT optimizer struct --- Lib/test/test_call.py | 1 + Lib/test/test_capi/test_opt.py | 34 +++++++++++++++++++++++++++++++++- Python/optimizer_analysis.c | 5 +++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_call.py b/Lib/test/test_call.py index 1c73aaafb71fd5..f5ce51fc58c0ea 100644 --- a/Lib/test/test_call.py +++ b/Lib/test/test_call.py @@ -1075,6 +1075,7 @@ def c_py_recurse(m): c_py_recurse(100_000) + class TestFunctionWithManyArgs(unittest.TestCase): def test_function_with_many_args(self): for N in (10, 500, 1000): diff --git a/Lib/test/test_capi/test_opt.py b/Lib/test/test_capi/test_opt.py index c796b0dd4b5b7e..d635940813a37b 100644 --- a/Lib/test/test_capi/test_opt.py +++ b/Lib/test/test_capi/test_opt.py @@ -10,11 +10,12 @@ from test.support import (script_helper, requires_specialization, import_helper, Py_GIL_DISABLED, requires_jit_enabled, - reset_code) + reset_code, infinite_recursion) _testinternalcapi = import_helper.import_module("_testinternalcapi") from _testinternalcapi import TIER2_THRESHOLD +from _testcapi import pyobject_vectorcall @contextlib.contextmanager @@ -2502,6 +2503,37 @@ def testfunc(n): self.assertIn("_BINARY_OP", uops) +@requires_jit_enabled +class TestStackUse(unittest.TestCase): + + def test_jit_at_depth(self): + + def run_at_depth(n, func): + if n: + pyobject_vectorcall(run_at_depth, (n-1, func), None) + else: + func() + + def find_stack_limit(): + nonlocal depth + depth += 1 + pyobject_vectorcall(find_stack_limit, (), None) + + with infinite_recursion(): + depth = 0 + try: + find_stack_limit() + except RecursionError: + pass + + def loop_func(): + r = 0 + for i in range(TIER2_THRESHOLD): + r += i + return r + run_at_depth(depth-1, loop_func) + + def global_identity(x): return x diff --git a/Python/optimizer_analysis.c b/Python/optimizer_analysis.c index dd3e49b83d9971..e202a2000a5147 100644 --- a/Python/optimizer_analysis.c +++ b/Python/optimizer_analysis.c @@ -695,6 +695,11 @@ _Py_uop_analyze_and_optimize( ) { OPT_STAT_INC(optimizer_attempts); + /* Make sure we have enough C stack space for the optimizer */ + int margin = 1 + sizeof(JitOptContext)/_PyOS_STACK_MARGIN_BYTES; + if (_Py_ReachedRecursionLimitWithMargin(_PyThreadState_GET(), margin)) { + return 0; + } int err = remove_globals(frame, buffer, length, dependencies); if (err <= 0) { From ef273958f217b070fabedefc246d9279650cf73a Mon Sep 17 00:00:00 2001 From: Mark Shannon Date: Tue, 12 Aug 2025 15:02:33 +0100 Subject: [PATCH 2/4] Add news --- .../2025-08-12-15-02-24.gh-issue-137573.9J6xfX.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-08-12-15-02-24.gh-issue-137573.9J6xfX.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-08-12-15-02-24.gh-issue-137573.9J6xfX.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-12-15-02-24.gh-issue-137573.9J6xfX.rst new file mode 100644 index 00000000000000..c86e7dc68cdb6e --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-12-15-02-24.gh-issue-137573.9J6xfX.rst @@ -0,0 +1 @@ +Guard against C stack overflow when JIT compiling. From 0a889dfd4d213368fb9150a42f6a0c9c370f6882 Mon Sep 17 00:00:00 2001 From: Mark Shannon Date: Wed, 13 Aug 2025 11:44:43 +0100 Subject: [PATCH 3/4] Update Lib/test/test_call.py Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com> --- Lib/test/test_call.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_call.py b/Lib/test/test_call.py index f5ce51fc58c0ea..1c73aaafb71fd5 100644 --- a/Lib/test/test_call.py +++ b/Lib/test/test_call.py @@ -1075,7 +1075,6 @@ def c_py_recurse(m): c_py_recurse(100_000) - class TestFunctionWithManyArgs(unittest.TestCase): def test_function_with_many_args(self): for N in (10, 500, 1000): From be99bd445018d5d6ef08d240caac9489200ea654 Mon Sep 17 00:00:00 2001 From: Mark Shannon Date: Wed, 13 Aug 2025 17:50:51 +0100 Subject: [PATCH 4/4] Account for trace buffer as well --- Python/optimizer.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Python/optimizer.c b/Python/optimizer.c index f06df9644a1614..03e90dc2c9d5c2 100644 --- a/Python/optimizer.c +++ b/Python/optimizer.c @@ -114,6 +114,12 @@ _PyOptimizer_Optimize( _PyInterpreterFrame *frame, _Py_CODEUNIT *start, _PyExecutorObject **executor_ptr, int chain_depth) { + /* Make sure we have enough C stack space for the buffer */ + int margin = 1 + sizeof(_PyUOpInstruction) * UOP_MAX_TRACE_LENGTH / _PyOS_STACK_MARGIN_BYTES; + if (_Py_ReachedRecursionLimitWithMargin(_PyThreadState_GET(), margin)) { + return 0; + } + _PyStackRef *stack_pointer = frame->stackpointer; assert(_PyInterpreterState_GET()->jit); // The first executor in a chain and the MAX_CHAIN_DEPTH'th executor *must*