]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-144766: Fix a crash in fork child process when perf support is enabled. (#144795)
authorYilei <hi@mangoumbrella.com>
Sat, 14 Feb 2026 11:41:28 +0000 (03:41 -0800)
committerGitHub <noreply@github.com>
Sat, 14 Feb 2026 11:41:28 +0000 (11:41 +0000)
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 66348619073909ca74a8e290a594712bc8bb0960..597e6599352049134aa09819acc0c8956eb4924d 100644 (file)
@@ -170,6 +170,47 @@ 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):
         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 (file)
index 0000000..d9613c9
--- /dev/null
@@ -0,0 +1 @@
+Fix a crash in fork child process when perf support is enabled.
index c0dc1f7a49bdca9d952ca303b0329c6a7fff7fdb..0d835f3b7f56a9c7d95ebea708f453de3706864e 100644 (file)
@@ -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);
         }
     }