From 92a4d354f7ef9a3a53fe3435f7cfa563665752a4 Mon Sep 17 00:00:00 2001 From: Hai Zhu Date: Thu, 13 Aug 2026 19:23:45 +0800 Subject: [PATCH 1/2] gh-154701: prevent executor self-links in JIT cold exits (GH-155323) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * prevent executor self-links in JIT cold exits * 📜🤖 Added by blurb_it. * fix windows ci --------- (cherry picked from commit 716cbae06c7d9d641626dfdb783f3959edf470a4) Co-authored-by: Hai Zhu Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com> --- Include/internal/pycore_optimizer.h | 2 +- Include/internal/pycore_uop_metadata.h | 2 +- Lib/test/test_capi/test_opt.py | 24 ++++++++++++++++++- ...-08-07-10-14-56.gh-issue-154701.zulh2S.rst | 1 + Python/bytecodes.c | 4 ++++ Python/executor_cases.c.h | 8 +++++++ 6 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-08-07-10-14-56.gh-issue-154701.zulh2S.rst diff --git a/Include/internal/pycore_optimizer.h b/Include/internal/pycore_optimizer.h index 8c35c4416fe3c8c..25e04171f4d5189 100644 --- a/Include/internal/pycore_optimizer.h +++ b/Include/internal/pycore_optimizer.h @@ -206,7 +206,7 @@ typedef struct _PyExecutorObject { PyAPI_FUNC(_PyExecutorObject*) _Py_GetExecutor(PyCodeObject *code, int offset); int _Py_ExecutorInit(_PyExecutorObject *, const _PyBloomFilter *); -void _Py_ExecutorDetach(_PyExecutorObject *); +PyAPI_FUNC(void) _Py_ExecutorDetach(_PyExecutorObject *); PyAPI_FUNC(void) _Py_Executor_DependsOn(_PyExecutorObject *executor, void *obj); /* We use a bloomfilter with k = 6, m = 256 diff --git a/Include/internal/pycore_uop_metadata.h b/Include/internal/pycore_uop_metadata.h index 6713e9bc95f942d..55b7f5160201683 100644 --- a/Include/internal/pycore_uop_metadata.h +++ b/Include/internal/pycore_uop_metadata.h @@ -413,7 +413,7 @@ const uint32_t _PyUop_Flags[MAX_UOP_ID+1] = { [_ERROR_POP_N] = HAS_ARG_FLAG | HAS_SYNC_SP_FLAG, [_SPILL_OR_RELOAD] = 0, [_TIER2_RESUME_CHECK] = HAS_PERIODIC_FLAG, - [_COLD_EXIT] = HAS_SYNC_SP_FLAG, + [_COLD_EXIT] = HAS_ESCAPES_FLAG | HAS_SYNC_SP_FLAG, [_COLD_DYNAMIC_EXIT] = HAS_SYNC_SP_FLAG, [_GUARD_CODE_VERSION__PUSH_FRAME] = HAS_EXIT_FLAG, [_GUARD_CODE_VERSION_YIELD_VALUE] = HAS_EXIT_FLAG, diff --git a/Lib/test/test_capi/test_opt.py b/Lib/test/test_capi/test_opt.py index b2439dfdeb7a221..121e46859cc6360 100644 --- a/Lib/test/test_capi/test_opt.py +++ b/Lib/test/test_capi/test_opt.py @@ -11,7 +11,7 @@ from test.support import (script_helper, requires_specialization, import_helper, Py_GIL_DISABLED, requires_jit_enabled, - reset_code) + reset_code, SHORT_TIMEOUT, isolation) _testinternalcapi = import_helper.import_module("_testinternalcapi") @@ -6200,6 +6200,28 @@ def __exit__(self, e, v, t): ... f1() """), PYTHON_JIT="1") + @isolation.runInSubprocess(timeout=SHORT_TIMEOUT) + def test_for_iter_side_exit_does_not_self_link(self): + def exhaust(iterator): + for _ in iterator: + pass + + values = range(TIER2_THRESHOLD) + # After the initial trace, MAX_CHAIN_DEPTH side exits cause the final + # executor to be installed at FOR_ITER. + warmup_iterators = ( + iter(set(values)), + iter(dict.fromkeys(values)), + iter(values), + enumerate(values), + zip(values, values), + ) + for iterator in warmup_iterators: + exhaust(iterator) + + # A different iterator type must not link that executor to itself. + exhaust(map(bool, values)) + def global_identity(x): return x diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-07-10-14-56.gh-issue-154701.zulh2S.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-07-10-14-56.gh-issue-154701.zulh2S.rst new file mode 100644 index 000000000000000..c32803e71cbf993 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-07-10-14-56.gh-issue-154701.zulh2S.rst @@ -0,0 +1 @@ +Fix an infinite loop in JIT when a ``FOR_ITER`` side exit links an executor back to itself. diff --git a/Python/bytecodes.c b/Python/bytecodes.c index 4f0d0227cc2eb04..f841e12793a8001 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -6308,6 +6308,10 @@ dummy_func( if (target->op.code == ENTER_EXECUTOR) { PyCodeObject *code = _PyFrame_GetCode(frame); executor = code->co_executors->executors[target->op.arg]; + if (executor == _PyExecutor_FromExit(exit)) { + _Py_ExecutorDetach(executor); + GOTO_TIER_ONE(target); + } Py_INCREF(executor); assert(tstate->jit_exit == exit); exit->executor = executor; diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index 7644c2d38d7f068..6b347948ef9db5f 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -23676,6 +23676,14 @@ if (target->op.code == ENTER_EXECUTOR) { PyCodeObject *code = _PyFrame_GetCode(frame); executor = code->co_executors->executors[target->op.arg]; + if (executor == _PyExecutor_FromExit(exit)) { + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyFrame_StackPointerValidate(frame); + _Py_ExecutorDetach(executor); + _PyFrame_StackPointerInvalidate(frame); + SET_CURRENT_CACHED_VALUES(0); + GOTO_TIER_ONE(target); + } Py_INCREF(executor); assert(tstate->jit_exit == exit); exit->executor = executor; From 323a0c12b3a9555f6b3aeaf6d110f069e89a4d86 Mon Sep 17 00:00:00 2001 From: cocolato Date: Sat, 15 Aug 2026 18:24:27 +0800 Subject: [PATCH 2/2] gh-154701: adapt JIT cold exit backport to 3.15 --- Lib/test/test_capi/test_opt.py | 42 +++++++++++++++++++--------------- Python/executor_cases.c.h | 3 +-- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/Lib/test/test_capi/test_opt.py b/Lib/test/test_capi/test_opt.py index 121e46859cc6360..06271a0e2732754 100644 --- a/Lib/test/test_capi/test_opt.py +++ b/Lib/test/test_capi/test_opt.py @@ -5,13 +5,14 @@ import unittest import gc import os +import subprocess import types import _opcode from test.support import (script_helper, requires_specialization, import_helper, Py_GIL_DISABLED, requires_jit_enabled, - reset_code, SHORT_TIMEOUT, isolation) + reset_code, SHORT_TIMEOUT) _testinternalcapi = import_helper.import_module("_testinternalcapi") @@ -6200,27 +6201,30 @@ def __exit__(self, e, v, t): ... f1() """), PYTHON_JIT="1") - @isolation.runInSubprocess(timeout=SHORT_TIMEOUT) def test_for_iter_side_exit_does_not_self_link(self): - def exhaust(iterator): - for _ in iterator: - pass + subprocess.run([sys.executable, "-c", textwrap.dedent(""" + from _testinternalcapi import TIER2_THRESHOLD - values = range(TIER2_THRESHOLD) - # After the initial trace, MAX_CHAIN_DEPTH side exits cause the final - # executor to be installed at FOR_ITER. - warmup_iterators = ( - iter(set(values)), - iter(dict.fromkeys(values)), - iter(values), - enumerate(values), - zip(values, values), - ) - for iterator in warmup_iterators: - exhaust(iterator) + def exhaust(iterator): + for _ in iterator: + pass + + values = range(TIER2_THRESHOLD) + # After the initial trace, MAX_CHAIN_DEPTH side exits cause the final + # executor to be installed at FOR_ITER. + warmup_iterators = ( + iter(set(values)), + iter(dict.fromkeys(values)), + iter(values), + enumerate(values), + zip(values, values), + ) + for iterator in warmup_iterators: + exhaust(iterator) - # A different iterator type must not link that executor to itself. - exhaust(map(bool, values)) + # A different iterator type must not link that executor to itself. + exhaust(map(bool, values)) + """)], check=True, timeout=SHORT_TIMEOUT) def global_identity(x): return x diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index 6b347948ef9db5f..dc5c68794e8a5f5 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -23678,9 +23678,8 @@ executor = code->co_executors->executors[target->op.arg]; if (executor == _PyExecutor_FromExit(exit)) { _PyFrame_SetStackPointer(frame, stack_pointer); - _PyFrame_StackPointerValidate(frame); _Py_ExecutorDetach(executor); - _PyFrame_StackPointerInvalidate(frame); + stack_pointer = _PyFrame_GetStackPointer(frame); SET_CURRENT_CACHED_VALUES(0); GOTO_TIER_ONE(target); }