|
1 | | -# Segfault in `instrument_hooks_deinit` during Python shutdown |
| 1 | +# Segfault in instrument hooks during Python shutdown |
2 | 2 |
|
3 | 3 | ## Issue |
4 | 4 |
|
5 | 5 | CI job `tests (valgrind, 3.13, >=8.1.1)` crashes with exit code 139 (SIGSEGV) after all tests pass. |
6 | | -The segfault occurs during Python's shutdown/GC phase, not during test execution. |
7 | 6 |
|
8 | 7 | Failing CI run: https://github.com/CodSpeedHQ/pytest-codspeed/actions/runs/23649460934/job/68890119732 |
9 | 8 |
|
10 | | -## Root cause (suspected) |
| 9 | +## Root cause |
11 | 10 |
|
12 | | -`InstrumentHooks.__del__` calls `instrument_hooks_deinit()` via cffi during Python shutdown. |
13 | | -When multiple `InstrumentHooks` instances are created (e.g. the xdist test loops 5 times, each creating a new instance), their destructors fire during GC at exit and crash — likely because the native library or cffi runtime is already partially torn down. |
| 11 | +Calling `sys.activate_stack_trampoline("perf")` multiple times after loading a cffi native library causes a segfault at process exit on CPython 3.13.12 (works fine on 3.13.11). This is a CPython regression. |
14 | 12 |
|
15 | | -## Stack trace (from CI) |
| 13 | +`InstrumentHooks.__init__` unconditionally calls `sys.activate_stack_trampoline("perf")`. When multiple instances are created (e.g. the xdist test loops 5 times inprocess), the repeated activation triggers the crash. |
16 | 14 |
|
17 | | -``` |
18 | | -Fatal Python error: Segmentation fault |
19 | | - File "plugin.py", line 148 in pytest_configure |
20 | | - ... |
21 | | - File "test_pytest_plugin_cpu_instrumentation.py", line 116 in test_pytest_xdist_concurrency_compatibility |
22 | | -``` |
| 15 | +## Fix |
23 | 16 |
|
24 | | -## Minimal repro |
| 17 | +Guard the trampoline activation with `sys.is_stack_trampoline_active()` in `InstrumentHooks.__init__`: |
25 | 18 |
|
26 | 19 | ```python |
27 | | -# repro.py |
28 | | -import os |
29 | | -os.environ["CODSPEED_ENV"] = "1" |
30 | | - |
31 | | -from pytest_codspeed.instruments.hooks import InstrumentHooks |
32 | | - |
33 | | -for i in range(5): |
34 | | - h = InstrumentHooks() |
35 | | - print(f"Created instance {i}: {h.instance}") |
36 | | - |
37 | | -del h |
38 | | -print("Exiting...") |
39 | | -``` |
40 | | - |
41 | | -``` |
42 | | -uv run python repro.py |
43 | | -# Segfaults at exit with code 139 |
| 20 | +if SUPPORTS_PERF_TRAMPOLINE and not sys.is_stack_trampoline_active(): |
| 21 | + sys.activate_stack_trampoline("perf") |
44 | 22 | ``` |
45 | 23 |
|
46 | | -## Key files |
| 24 | +## Verification |
47 | 25 |
|
48 | | -- `src/pytest_codspeed/instruments/hooks/__init__.py` — `InstrumentHooks.__del__` calls `instrument_hooks_deinit` |
49 | | -- `src/pytest_codspeed/instruments/hooks/build.py` — cffi bindings for the native library |
50 | | -- `src/pytest_codspeed/instruments/analysis.py` — creates `InstrumentHooks` in `__init__` |
51 | | -- `tests/test_pytest_plugin_cpu_instrumentation.py::test_pytest_xdist_concurrency_compatibility` — triggers the bug by creating 5 successive instances |
| 26 | +- `uv run --python 3.13.12 python repro.py` → exit 0 (was exit 139) |
| 27 | +- `test_pytest_xdist_concurrency_compatibility` → PASSED, exit 0 (was exit 139) |
0 commit comments