From 5922149a5033ec1151320864e605adf88f53f280 Mon Sep 17 00:00:00 2001 From: Yilei Date: Sat, 14 Feb 2026 03:41:28 -0800 Subject: [PATCH 1/2] gh-144766: Fix a crash in fork child process when perf support is enabled. (#144795) --- Lib/test/test_perf_profiler.py | 41 +++++++++++++++++++ ...-02-13-18-30-59.gh-issue-144766.JGu3x3.rst | 1 + Python/perf_trampoline.c | 6 +++ 3 files changed, 48 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-02-13-18-30-59.gh-issue-144766.JGu3x3.rst diff --git a/Lib/test/test_perf_profiler.py b/Lib/test/test_perf_profiler.py index 66348619073909..597e6599352049 100644 --- a/Lib/test/test_perf_profiler.py +++ b/Lib/test/test_perf_profiler.py @@ -170,6 +170,47 @@ def baz(): self.assertNotIn(f"py::bar:{script}", child_perf_file_contents) self.assertNotIn(f"py::baz:{script}", child_perf_file_contents) + @unittest.skipIf(support.check_bolt_optimized(), "fails on BOLT instrumented binaries") + def test_trampoline_works_after_fork_with_many_code_objects(self): + code = """if 1: + import gc, os, sys, signal + + # Create many code objects so trampoline_refcount > 1 + for i in range(50): + exec(compile(f"def _dummy_{i}(): pass", f"", "exec")) + + pid = os.fork() + if pid == 0: + # Child: create and destroy new code objects, + # then collect garbage. If the old code watcher + # survived the fork, the double-decrement of + # trampoline_refcount will cause a SIGSEGV. + for i in range(50): + exec(compile(f"def _child_{i}(): pass", f"", "exec")) + gc.collect() + os._exit(0) + else: + _, status = os.waitpid(pid, 0) + if os.WIFSIGNALED(status): + print(f"FAIL: child killed by signal {os.WTERMSIG(status)}", file=sys.stderr) + sys.exit(1) + sys.exit(os.WEXITSTATUS(status)) + """ + with temp_dir() as script_dir: + script = make_script(script_dir, "perftest", code) + env = {**os.environ, "PYTHON_JIT": "0"} + with subprocess.Popen( + [sys.executable, "-Xperf", script], + text=True, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + env=env, + ) as process: + stdout, stderr = process.communicate() + + self.assertEqual(process.returncode, 0, stderr) + self.assertEqual(stderr, "") + @unittest.skipIf(support.check_bolt_optimized(), "fails on BOLT instrumented binaries") def test_sys_api(self): for define_eval_hook in (False, True): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-02-13-18-30-59.gh-issue-144766.JGu3x3.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-13-18-30-59.gh-issue-144766.JGu3x3.rst new file mode 100644 index 00000000000000..d9613c95af1915 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-13-18-30-59.gh-issue-144766.JGu3x3.rst @@ -0,0 +1 @@ +Fix a crash in fork child process when perf support is enabled. diff --git a/Python/perf_trampoline.c b/Python/perf_trampoline.c index c0dc1f7a49bdca..0d835f3b7f56a9 100644 --- a/Python/perf_trampoline.c +++ b/Python/perf_trampoline.c @@ -618,6 +618,12 @@ _PyPerfTrampoline_AfterFork_Child(void) int was_active = _PyIsPerfTrampolineActive(); _PyPerfTrampoline_Fini(); if (was_active) { + // After fork, Fini may leave the old code watcher registered + // if trampolined code objects from the parent still exist + // (trampoline_refcount > 0). Clear it unconditionally before + // Init registers a new one, to prevent two watchers sharing + // the same globals and double-decrementing trampoline_refcount. + perf_trampoline_reset_state(); _PyPerfTrampoline_Init(1); } } From 14cbd0e6afa98355bdc6749b8230fed4c9b21bd6 Mon Sep 17 00:00:00 2001 From: Chris Eibl <138194463+chris-eibl@users.noreply.github.com> Date: Sat, 14 Feb 2026 15:09:01 +0100 Subject: [PATCH 2/2] remove unused _PyFunction_LookupByVersion (GH-144814) --- Include/internal/pycore_function.h | 1 - Objects/funcobject.c | 26 -------------------------- Python/optimizer.c | 1 - 3 files changed, 28 deletions(-) diff --git a/Include/internal/pycore_function.h b/Include/internal/pycore_function.h index 522e03c6696993..9c2121f59a4a0c 100644 --- a/Include/internal/pycore_function.h +++ b/Include/internal/pycore_function.h @@ -30,7 +30,6 @@ _PyFunction_IsVersionValid(uint32_t version) extern uint32_t _PyFunction_GetVersionForCurrentState(PyFunctionObject *func); PyAPI_FUNC(void) _PyFunction_SetVersion(PyFunctionObject *func, uint32_t version); void _PyFunction_ClearCodeByVersion(uint32_t version); -PyFunctionObject *_PyFunction_LookupByVersion(uint32_t version, PyObject **p_code); extern PyObject *_Py_set_function_type_params( PyThreadState* unused, PyObject *func, PyObject *type_params); diff --git a/Objects/funcobject.c b/Objects/funcobject.c index 8f4ff4e42392c2..ee0c46a95b9708 100644 --- a/Objects/funcobject.c +++ b/Objects/funcobject.c @@ -373,32 +373,6 @@ _PyFunction_ClearCodeByVersion(uint32_t version) #endif } -PyFunctionObject * -_PyFunction_LookupByVersion(uint32_t version, PyObject **p_code) -{ -#ifdef Py_GIL_DISABLED - return NULL; -#else - PyInterpreterState *interp = _PyInterpreterState_GET(); - struct _func_version_cache_item *slot = get_cache_item(interp, version); - if (slot->code) { - assert(PyCode_Check(slot->code)); - PyCodeObject *code = (PyCodeObject *)slot->code; - if (code->co_version == version) { - *p_code = slot->code; - } - } - else { - *p_code = NULL; - } - if (slot->func && slot->func->func_version == version) { - assert(slot->func->func_code == slot->code); - return slot->func; - } - return NULL; -#endif -} - uint32_t _PyFunction_GetVersionForCurrentState(PyFunctionObject *func) { diff --git a/Python/optimizer.c b/Python/optimizer.c index bf5d8a28264635..12ef7c3fc0adf5 100644 --- a/Python/optimizer.c +++ b/Python/optimizer.c @@ -8,7 +8,6 @@ #include "pycore_bitutils.h" // _Py_popcount32() #include "pycore_ceval.h" // _Py_set_eval_breaker_bit #include "pycore_code.h" // _Py_GetBaseCodeUnit -#include "pycore_function.h" // _PyFunction_LookupByVersion() #include "pycore_interpframe.h" #include "pycore_object.h" // _PyObject_GC_UNTRACK() #include "pycore_opcode_metadata.h" // _PyOpcode_OpName[]