Skip to content

Commit 0151abc

Browse files
pablogsalyilei
andauthored
[3.13] gh-144766: Fix a crash in fork child process when perf support is enabled. (GH-144795) (#144818)
(cherry picked from commit 5922149) Co-authored-by: Yilei <hi@mangoumbrella.com>
1 parent 48dd852 commit 0151abc

File tree

3 files changed

+49
-0
lines changed

3 files changed

+49
-0
lines changed

Lib/test/test_perf_profiler.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,48 @@ def baz():
167167
self.assertNotIn(f"py::bar:{script}", child_perf_file_contents)
168168
self.assertNotIn(f"py::baz:{script}", child_perf_file_contents)
169169

170+
@unittest.skipIf(support.check_bolt_optimized(), "fails on BOLT instrumented binaries")
171+
def test_trampoline_works_after_fork_with_many_code_objects(self):
172+
code = """if 1:
173+
import gc, os, sys, signal
174+
175+
# Create many code objects so trampoline_refcount > 1
176+
for i in range(50):
177+
exec(compile(f"def _dummy_{i}(): pass", f"<test{i}>", "exec"))
178+
179+
pid = os.fork()
180+
if pid == 0:
181+
# Child: create and destroy new code objects,
182+
# then collect garbage. If the old code watcher
183+
# survived the fork, the double-decrement of
184+
# trampoline_refcount will cause a SIGSEGV.
185+
for i in range(50):
186+
exec(compile(f"def _child_{i}(): pass", f"<child{i}>", "exec"))
187+
gc.collect()
188+
os._exit(0)
189+
else:
190+
_, status = os.waitpid(pid, 0)
191+
if os.WIFSIGNALED(status):
192+
print(f"FAIL: child killed by signal {os.WTERMSIG(status)}", file=sys.stderr)
193+
sys.exit(1)
194+
sys.exit(os.WEXITSTATUS(status))
195+
"""
196+
with temp_dir() as script_dir:
197+
script = make_script(script_dir, "perftest", code)
198+
env = {**os.environ, "PYTHON_JIT": "0"}
199+
with subprocess.Popen(
200+
[sys.executable, "-Xperf", script],
201+
text=True,
202+
stderr=subprocess.PIPE,
203+
stdout=subprocess.PIPE,
204+
env=env,
205+
) as process:
206+
stdout, stderr = process.communicate()
207+
208+
self.assertEqual(process.returncode, 0, stderr)
209+
self.assertEqual(stderr, "")
210+
211+
@unittest.skipIf(support.check_bolt_optimized(), "fails on BOLT instrumented binaries")
170212
def test_sys_api(self):
171213
code = """if 1:
172214
import sys
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a crash in fork child process when perf support is enabled.

Python/perf_trampoline.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,12 @@ _PyPerfTrampoline_AfterFork_Child(void)
620620
int was_active = _PyIsPerfTrampolineActive();
621621
_PyPerfTrampoline_Fini();
622622
if (was_active) {
623+
// After fork, Fini may leave the old code watcher registered
624+
// if trampolined code objects from the parent still exist
625+
// (trampoline_refcount > 0). Clear it unconditionally before
626+
// Init registers a new one, to prevent two watchers sharing
627+
// the same globals and double-decrementing trampoline_refcount.
628+
perf_trampoline_reset_state();
623629
_PyPerfTrampoline_Init(1);
624630
}
625631
}

0 commit comments

Comments
 (0)