From 3a2d34bad6bdeff24e9e8ee842798122c94ebc49 Mon Sep 17 00:00:00 2001 From: Shamil Abdulaev Date: Sat, 11 Oct 2025 15:07:14 +0300 Subject: [PATCH 1/3] Track executor object with GC --- Python/optimizer.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/optimizer.c b/Python/optimizer.c index 7b76cddeabff44..0c499953816ad0 100644 --- a/Python/optimizer.c +++ b/Python/optimizer.c @@ -1246,6 +1246,7 @@ make_executor_from_uops(_PyUOpInstruction *buffer, int length, const _PyBloomFil } sanity_check(executor); #endif + _PyObject_GC_TRACK(executor); #ifdef _Py_JIT executor->jit_code = NULL; executor->jit_size = 0; @@ -1257,7 +1258,6 @@ make_executor_from_uops(_PyUOpInstruction *buffer, int length, const _PyBloomFil return NULL; } #endif - _PyObject_GC_TRACK(executor); return executor; } From 963bcf0d8044aa16adecd7ba68ab928b72ebe680 Mon Sep 17 00:00:00 2001 From: Shamil Abdulaev Date: Sat, 11 Oct 2025 16:15:55 +0300 Subject: [PATCH 2/3] Create 2025-10-11-16-14-36.gh-issue-139549.9BGTLP.rst --- .../2025-10-11-16-14-36.gh-issue-139549.9BGTLP.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-10-11-16-14-36.gh-issue-139549.9BGTLP.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-10-11-16-14-36.gh-issue-139549.9BGTLP.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-11-16-14-36.gh-issue-139549.9BGTLP.rst new file mode 100644 index 00000000000000..ea77251755e927 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-11-16-14-36.gh-issue-139549.9BGTLP.rst @@ -0,0 +1 @@ +Fix a memory leak of a JIT executor that occurred when JIT compilation failed. From db344b2f464845e481f2e239855862a3a164a6b5 Mon Sep 17 00:00:00 2001 From: Shamil Abdulaev Date: Sat, 11 Oct 2025 21:27:20 +0300 Subject: [PATCH 3/3] Fix memory leak in JIT executor on compilation failure The executor object wasn't decremented if Tier 2 code returned with an exception set but not a fatal error. This change moves the Py_DECREF call inside the TIER1_TO_TIER2 macro. This ensures the executor's reference count is always decremented. --- .../2025-10-11-16-14-36.gh-issue-139549.9BGTLP.rst | 10 +++++++++- Python/bytecodes.c | 1 + Python/ceval.c | 5 +++++ Python/ceval_macros.h | 5 ++--- Python/generated_cases.c.h | 6 ++++++ Python/optimizer.c | 3 ++- Python/pystate.c | 3 +++ 7 files changed, 28 insertions(+), 5 deletions(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-10-11-16-14-36.gh-issue-139549.9BGTLP.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-11-16-14-36.gh-issue-139549.9BGTLP.rst index ea77251755e927..70e42165ef4b94 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2025-10-11-16-14-36.gh-issue-139549.9BGTLP.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-11-16-14-36.gh-issue-139549.9BGTLP.rst @@ -1 +1,9 @@ -Fix a memory leak of a JIT executor that occurred when JIT compilation failed. +Fix memory leak in JIT executor on compilation failure + +The executor object passed to the TIER1_TO_TIER2 macro was not +decremented if the Tier 2 code returned with an exception set but +without a fatal error (i.e., not returning NULL). + +This change moves the Py_DECREF call inside the TIER1_TO_TIER2 +macro to ensure the executor's reference count is always decremented +after its execution, regardless of the outcome. diff --git a/Python/bytecodes.c b/Python/bytecodes.c index f9f14322df0a5e..e5fea02448b767 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -2972,6 +2972,7 @@ dummy_func( assert(tstate->current_executor == NULL); assert(executor != tstate->interp->cold_executor); tstate->jit_exit = NULL; + tstate->current_executor = (PyObject *)executor; TIER1_TO_TIER2(executor); } } diff --git a/Python/ceval.c b/Python/ceval.c index 1b52128c858ecb..ab77ebace4d901 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1146,6 +1146,11 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int assert(frame->owner == FRAME_OWNED_BY_INTERPRETER); /* Restore previous frame and exit */ tstate->current_frame = frame->previous; +#ifdef _Py_TIER2 + if (tstate->current_executor != NULL) { + tstate->current_executor = NULL; + } +#endif return NULL; } #ifdef _Py_TIER2 diff --git a/Python/ceval_macros.h b/Python/ceval_macros.h index 4ed03b7fb01bdf..1d7b704c8ce5d2 100644 --- a/Python/ceval_macros.h +++ b/Python/ceval_macros.h @@ -351,12 +351,12 @@ _PyFrame_SetStackPointer(frame, stack_pointer) /* Tier-switching macros. */ -#define TIER1_TO_TIER2(EXECUTOR) \ +#define TIER1_TO_TIER2(EXECUTOR) \ do { \ - OPT_STAT_INC(traces_executed); \ next_instr = _Py_jit_entry((EXECUTOR), frame, stack_pointer, tstate); \ frame = tstate->current_frame; \ stack_pointer = _PyFrame_GetStackPointer(frame); \ + Py_DECREF(EXECUTOR); \ if (next_instr == NULL) { \ next_instr = frame->instr_ptr; \ JUMP_TO_LABEL(error); \ @@ -413,4 +413,3 @@ check_periodics(PyThreadState *tstate) { } return 0; } - diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index 79328a7b725613..6ca7aae1caa618 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -7668,7 +7668,13 @@ assert(tstate->current_executor == NULL); assert(executor != tstate->interp->cold_executor); tstate->jit_exit = NULL; + tstate->current_executor = (PyObject *)executor; + _PyFrame_SetStackPointer(frame, stack_pointer); + stack_pointer = _PyFrame_GetStackPointer(frame); TIER1_TO_TIER2(executor); + _PyFrame_SetStackPointer(frame, stack_pointer); + Py_DECREF(executor); + stack_pointer = _PyFrame_GetStackPointer(frame); } } else { diff --git a/Python/optimizer.c b/Python/optimizer.c index 0c499953816ad0..97cde1c1fdf8b3 100644 --- a/Python/optimizer.c +++ b/Python/optimizer.c @@ -1202,6 +1202,7 @@ make_executor_from_uops(_PyUOpInstruction *buffer, int length, const _PyBloomFil if (executor == NULL) { return NULL; } + _PyObject_GC_TRACK(executor); /* Initialize exits */ _PyExecutorObject *cold = _PyExecutor_GetColdExecutor(); @@ -1246,7 +1247,6 @@ make_executor_from_uops(_PyUOpInstruction *buffer, int length, const _PyBloomFil } sanity_check(executor); #endif - _PyObject_GC_TRACK(executor); #ifdef _Py_JIT executor->jit_code = NULL; executor->jit_size = 0; @@ -1507,6 +1507,7 @@ _PyExecutor_GetColdExecutor(void) if (cold == NULL) { Py_FatalError("Cannot allocate core JIT code"); } + _PyObject_GC_TRACK(cold); ((_PyUOpInstruction *)cold->trace)->opcode = _COLD_EXIT; #ifdef _Py_JIT cold->jit_code = NULL; diff --git a/Python/pystate.c b/Python/pystate.c index dbed609f29aa07..5076f11575c61c 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -1717,6 +1717,9 @@ PyThreadState_Clear(PyThreadState *tstate) Py_CLEAR(tstate->async_gen_finalizer); Py_CLEAR(tstate->context); +#ifdef _Py_TIER2 + Py_CLEAR(tstate->current_executor); +#endif #ifdef Py_GIL_DISABLED // Each thread should clear own freelists in free-threading builds.