Skip to content

perf_trampoline crash in the fork child process #144766

@yilei

Description

@yilei

Crash report

What happened?

After upgrading from 3.13.11 to 3.13.12, we saw a consistent SIGSEGV crash in perf_trampoline from the fork child process when perf support is enabled. Here is a minimal example:

import gc
import os
import sys


def main():
    if not sys.is_stack_trampoline_active():
        try:
            sys.activate_stack_trampoline("perf")
        except ValueError:
            print("Perf trampoline not supported on this platform")
            return

    # Execute functions to create trampolined code objects so
    # trampoline_refcount > 1 at fork time. Keep references alive.
    funcs = []
    ns = {}
    for i in range(50):
        exec(compile(f"def _f{i}(): return {i}", f"<gen-{i}>", "exec"), ns)
        f = ns[f"_f{i}"]
        f()  # Execute to create trampoline
        funcs.append(f)

    pid = os.fork()

    if pid == 0:
        # Child: _PyPerfTrampoline_AfterFork_Child has run, leaving two
        # active code watchers. We need to create NEW code objects that get
        # trampolines at the NEW extra_code_index, so both watchers see
        # them and double-decrement trampoline_refcount.
        ns2 = {}
        for i in range(50):
            exec(compile(f"def _g{i}(): return {i}", f"<child-{i}>", "exec"), ns2)
            ns2[f"_g{i}"]()  # Execute to create trampoline (increments refcount)

        # Destroy them — both watchers fire, double-decrement -> SIGSEGV
        del ns2
        gc.collect()
        os._exit(0)
    else:
        _, status = os.waitpid(pid, 0)
        if os.WIFSIGNALED(status):
            sig = os.WTERMSIG(status)
            print(f"FAIL: Child killed by signal {sig} (SIGSEGV=11)")
            sys.exit(1)
        else:
            exit_code = os.WEXITSTATUS(status)
            if exit_code == 0:
                print("OK: Child exited normally (bug not triggered)")
            else:
                print(f"FAIL: Child exited with code {exit_code}")
                sys.exit(1)


if __name__ == "__main__":
    main()

Compiling CPython 3.13.12 with:

./configure CFLAGS="-g -O0" && make -j $(nproc)

Running in gdb shows the crash is here:

❯ PYTHONPERFSUPPORT=1 gdb --args ./python ~/perf_trampoline_crash.py
...
(gdb) set follow-fork-mode child
(gdb) run
...

Thread 2.1 "python" received signal SIGSEGV, Segmentation fault.
[Switching to process 753038]
compile_trampoline () at Python/perf_trampoline.c:411
411	    size_t total_code_size = round_up(perf_code_arena->code_size + trampoline_api.code_padding, 16);

After some digging, I believe this is a regression from the bugfix for #143228. Reverting de34f6d makes the crash go away.

This crash is also reproducible at main branch.

CPython versions tested on:

3.13, CPython main branch

Operating systems tested on:

Linux

Output from running 'python -VV' on the command line:

No response

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    interpreter-core(Objects, Python, Grammar, and Parser dirs)type-crashA hard crash of the interpreter, possibly with a core dump

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions