From 63662b91786d654c389aa065080d46c0ec78156f Mon Sep 17 00:00:00 2001 From: Cajetan Rodrigues Date: Fri, 5 Dec 2025 10:29:15 +0100 Subject: [PATCH 1/4] gh-141786 Fix missing parent executor during trace An executor's trace can point to another executor, forming a graph of traces / executors. Sometimes it is possible while recording a trace that the parent executor is freed / invalidated halfway. This leads to the bug described in the issue gh-141786, which if left unfixed could cause a memory leak. This patch checks for the validity of the parent executor as well as allowing JIT from the cold executor. While the cold executor is not linked, it is the executor responsible for creating side traces and we still want to JIT from it. --- Python/ceval.c | 4 +++- Python/optimizer.c | 9 ++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Python/ceval.c b/Python/ceval.c index 46bf644106ac39..a43cc60a9e4f0f 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1408,7 +1408,9 @@ stop_tracing_and_jit(PyThreadState *tstate, _PyInterpreterFrame *frame) // Likewise, we hold a strong reference to the executor containing this exit, so the exit is guaranteed // to be valid to access. if (err <= 0) { - exit->temperature = restart_backoff_counter(exit->temperature); + if (exit->executor->vm_data.linked || exit->executor->vm_data.valid) { + exit->temperature = restart_backoff_counter(exit->temperature); + } } else { exit->temperature = initial_temperature_backoff_counter(); diff --git a/Python/optimizer.c b/Python/optimizer.c index 9db894f0bf054a..1cbb1ffd16d5d1 100644 --- a/Python/optimizer.c +++ b/Python/optimizer.c @@ -140,6 +140,14 @@ _PyOptimizer_Optimize( } assert(!interp->compiling); assert(_tstate->jit_tracer_state.initial_state.stack_depth >= 0); + _PyExitData *exit = _tstate->jit_tracer_state.initial_state.exit; + _PyExecutorObject *cold_executor = _PyExecutor_GetColdExecutor(); + if (exit != NULL && + (!exit->executor->vm_data.linked || !exit->executor->vm_data.valid) && + exit->executor != cold_executor) { + // gh-141786 Parent executor is either unlinked or invalid - cannot optimize. + return 0; + } #ifndef Py_GIL_DISABLED assert(_tstate->jit_tracer_state.initial_state.func != NULL); interp->compiling = true; @@ -185,7 +193,6 @@ _PyOptimizer_Optimize( else { executor->vm_data.code = NULL; } - _PyExitData *exit = _tstate->jit_tracer_state.initial_state.exit; if (exit != NULL) { exit->executor = executor; } From 36a625246871d80330feb4714486e981fc0830fb Mon Sep 17 00:00:00 2001 From: Cajetan Rodrigues Date: Mon, 8 Dec 2025 10:13:46 +0100 Subject: [PATCH 2/4] gh-141786 Add a test case --- Lib/test/test_capi/test_opt.py | 138 +++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/Lib/test/test_capi/test_opt.py b/Lib/test/test_capi/test_opt.py index 51234a2e40f54f..ecb9e7c9354a29 100644 --- a/Lib/test/test_capi/test_opt.py +++ b/Lib/test/test_capi/test_opt.py @@ -2695,6 +2695,144 @@ def recursive_wrapper_4569(): pass """)) + def test_missing_parent_executor_does_not_crash(self): + script_helper.assert_python_ok("-c", textwrap.dedent(""" + import random + + rand = random.Random(38720) + import sys + + def b(): + for c in range(23): + rand.random() + rand.random() + if rand.random() > 0.1: + rand.random() + rand.random() + rand.random() + for c in range(4): + rand.random() + if rand.random(): + 0 + if rand.random() > 0.1: + rand.random() + rand.random() + if rand.random(): + rand.random() + if rand.random(): + rand.random() + rand.random() + if rand.random(): + rand.random() + rand.random() + if rand.random(): + if rand.random() > 0.1: + rand.random() + if rand.random() < rand.random(): + ... + if rand.random() < rand.random(): + ... + if rand.random(): + ... + if rand.random() > 0.1: + rand.random() + rand.random() + for c in range(12): + rand.random() + + def random(B): + if rand.random() < 0.1: + random(B) + + def Random(B): + if rand.random(): + random(B) + + def b(B): + if rand.random(): + Random(B) + + def c(B): + if rand.random() < 0.1: + b(B) + + def a(B): + if rand.random() > 0.1: + c(B) + + def A(B): + if rand.random() > 0.1: + a(B) + + def C(B): + if rand.random() > 0.1: + A(B) + + def B(B): + if rand.random() < 0.1: + C(B) + + for D in range(200): + try: + B(D) + except: + ... + + class A: + def __del__(C): + b = sys._getframe(1) + exec("D=0", b.f_globals, b.f_locals) + + B = A() + if rand.random() < 0.1: + if rand.random() < 0.1: + if rand.random() < 0.1: + for D in range(D): + if rand.random() < 0.1: + if rand.random() > 0.1: + if rand.random(): + if rand.random() > rand.random(): + C + if rand.random() < 0.1: + if rand.random() < 0.1: + if rand.random() < 0.1: + del B + if rand.random() < 0.1: + if rand.random() < 0.1: + if rand.random(): + 0 + if rand.random() > 0.1: + if rand.random() > 0.1: + rand.random() + rand.random() + if rand.random() > 0.1: + rand.random() + rand.random() + if rand.random() > 0.1: + if rand.random() > 0.1: + rand.random() + rand.random() + rand.random() + rand.random() + if rand.random() > 0.1: + if rand.random() > 0.1: + rand.random() + if rand.random() > rand.random(): + 0 + if rand.random(): + C + if rand.random() > 0.1: + rand.random() + rand.random() + for c in range(3): + if rand.random(): + 0 + for c in range(2): + rand.random() + + for D in range(5064): + b() + """)) def global_identity(x): return x From ade01a26e83f0f690cd28097ac304c0a415104a8 Mon Sep 17 00:00:00 2001 From: Cajetan Rodrigues Date: Mon, 8 Dec 2025 15:41:55 +0100 Subject: [PATCH 3/4] gh-141786 Fix an incorrect condition --- Python/ceval.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/ceval.c b/Python/ceval.c index a43cc60a9e4f0f..06500ea09fbe87 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1408,7 +1408,7 @@ stop_tracing_and_jit(PyThreadState *tstate, _PyInterpreterFrame *frame) // Likewise, we hold a strong reference to the executor containing this exit, so the exit is guaranteed // to be valid to access. if (err <= 0) { - if (exit->executor->vm_data.linked || exit->executor->vm_data.valid) { + if (exit->executor->vm_data.linked && exit->executor->vm_data.valid) { exit->temperature = restart_backoff_counter(exit->temperature); } } From 7ab1b35fd743403f3daacba44dd3c1ab3651d5a3 Mon Sep 17 00:00:00 2001 From: Cajetan Rodrigues Date: Mon, 8 Dec 2025 16:14:43 +0100 Subject: [PATCH 4/4] gh-141786 Fix test name to be more precise --- Lib/test/test_capi/test_opt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_capi/test_opt.py b/Lib/test/test_capi/test_opt.py index ecb9e7c9354a29..0f67f19a115d83 100644 --- a/Lib/test/test_capi/test_opt.py +++ b/Lib/test/test_capi/test_opt.py @@ -2695,7 +2695,7 @@ def recursive_wrapper_4569(): pass """)) - def test_missing_parent_executor_does_not_crash(self): + def test_executor_invalidation_does_not_crash(self): script_helper.assert_python_ok("-c", textwrap.dedent(""" import random