]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.13] gh-144766: Fix a crash in fork child process when perf support is enabled...
authorPablo Galindo Salgado <Pablogsal@gmail.com>
Sat, 14 Feb 2026 17:52:42 +0000 (17:52 +0000)
committerGitHub <noreply@github.com>
Sat, 14 Feb 2026 17:52:42 +0000 (17:52 +0000)
(cherry picked from commit 5922149a5033ec1151320864e605adf88f53f280)

Co-authored-by: Yilei <hi@mangoumbrella.com>
Lib/test/test_perf_profiler.py
Misc/NEWS.d/next/Core and Builtins/2026-02-13-18-30-59.gh-issue-144766.JGu3x3.rst [new file with mode: 0644]
Python/perf_trampoline.c

index 3c845419a3e17438b5e56d719207e0ee08581cf0..ee417371a5d121b18c0801c54f2e29c0dcfc789b 100644 (file)
@@ -167,6 +167,48 @@ class TestPerfTrampoline(unittest.TestCase):
         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"<test{i}>", "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"<child{i}>", "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):
         code = """if 1:
                 import sys
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 (file)
index 0000000..d9613c9
--- /dev/null
@@ -0,0 +1 @@
+Fix a crash in fork child process when perf support is enabled.
index 5589ec1c36232f81d09173b646045b700a952aa2..7ef9eca11f5052029599ac601c4339e6048669c9 100644 (file)
@@ -620,6 +620,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);
         }
     }