-
-
Notifications
You must be signed in to change notification settings - Fork 34.1k
Description
Bug description
A tight loop in asyncio.wait_for() causes a segmentation fault when an asyncio.Event is already set but an outer condition prevents the caller from returning. The crash occurs inside the garbage collector during _Py_HandlePending, triggered by the rapid allocation of TimerHandle objects.
The bug is nondeterministic and requires pytest-asyncio's per-test asyncio.Runner lifecycle to reproduce. It crashes approximately 1 in 7 runs.
Mechanism
The following wait_for pattern creates a busy-loop that floods the GC:
async def wait(self):
while True:
if event.is_set() and vpn_event.is_set():
return
try:
# BUG: event IS set, so event.wait() returns instantly.
# But vpn_event is NOT set, so the while-loop continues.
# Each iteration allocates a new TimerHandle via wait_for().
await asyncio.wait_for(event.wait(), timeout=1.0)
except asyncio.TimeoutError:
passWith 3 concurrent tasks running this loop:
- Thousands of
TimerHandleobjects are allocated per second - Gen0 GC threshold (2000) is hit every ~600 iterations
- After enough gen0 collections, a cascading gen0 -> gen1 -> gen2 collection triggers
- The GC segfaults during traversal/finalization of the unreachable set
Faulthandler output
Fatal Python error: Segmentation fault
Current thread 0x00007fab8ee3f100 (most recent call first):
Garbage-collecting
File "/usr/lib/python3.13/asyncio/events.py", line 113 in __init__
File "/usr/lib/python3.13/asyncio/base_events.py", line 816 in call_at
File "/usr/lib/python3.13/asyncio/timeouts.py", line 71 in reschedule
File "/usr/lib/python3.13/asyncio/timeouts.py", line 94 in __aenter__
File "/usr/lib/python3.13/asyncio/tasks.py", line 506 in wait_for
SIGSEGV handler details (custom handler installed via conftest.py)
Frame: /usr/lib/python3.13/asyncio/events.py:36 in __init__
GC counts: (15, 0, 0)
GC counts (15, 0, 0) confirms gen1 and gen2 were both just collected (counts reset to 0). The crash happens during or immediately after a full gen2 collection.
C-level backtrace (from GDB post-mortem on core dump)
#0 kill () <- SIGSEGV handler re-raise
#1 os_kill.lto_priv.0
#2 cfunction_vectorcall_FASTCALL
#3 PyObject_Vectorcall
#4 _PyEval_EvalFrameDefault
#7 _Py_HandlePending <- GC triggered here
#8 _PyEval_EvalFrameDefault <- interrupted by GC
#9 slot_tp_init.lto_priv.0 <- Handle.__init__ tp_init slot
#10 _PyObject_MakeTpCall
#12 gen_send_ex2.lto_priv.0 <- coroutine.send()
#13-14 _asyncio.cpython-313 .so <- task_step_impl (C accelerator)
Reproducer
Requires: pytest, pytest-asyncio>=1.3.0
"""Reproducer: save as test_gh_XXXXX.py and run with pytest."""
import asyncio
import pytest
class Gate:
def __init__(self):
self._target_events = {}
self._vpn_event = asyncio.Event()
self._vpn_event.set()
def register(self, ip):
if ip not in self._target_events:
self._target_events[ip] = asyncio.Event()
self._target_events[ip].set()
def mark_vpn_down(self):
self._vpn_event.clear()
def mark_vpn_up(self):
self._vpn_event.set()
async def wait_buggy(self, ip):
while True:
ev = self._target_events[ip]
if ev.is_set() and self._vpn_event.is_set():
return
try:
await asyncio.wait_for(ev.wait(), timeout=1.0)
except asyncio.TimeoutError:
pass
# 14 padding tests to accumulate GC gen1/gen2 pressure
class TestPadding:
@pytest.mark.asyncio
async def test_01(self):
g = Gate(); g.register("10.0.0.1")
await asyncio.wait_for(g.wait_buggy("10.0.0.1"), timeout=1.0)
@pytest.mark.asyncio
async def test_02(self):
g = Gate(); g.register("10.0.0.2")
await asyncio.wait_for(g.wait_buggy("10.0.0.2"), timeout=1.0)
@pytest.mark.asyncio
async def test_03(self):
g = Gate(); g.register("10.0.0.3")
g.mark_vpn_down(); g.mark_vpn_up()
await asyncio.wait_for(g.wait_buggy("10.0.0.3"), timeout=1.0)
@pytest.mark.asyncio
async def test_04(self): await asyncio.sleep(0.01)
@pytest.mark.asyncio
async def test_05(self): await asyncio.sleep(0.01)
@pytest.mark.asyncio
async def test_06(self): await asyncio.sleep(0.01)
@pytest.mark.asyncio
async def test_07(self): await asyncio.sleep(0.01)
@pytest.mark.asyncio
async def test_08(self): await asyncio.sleep(0.01)
@pytest.mark.asyncio
async def test_09(self): await asyncio.sleep(0.01)
@pytest.mark.asyncio
async def test_10(self): await asyncio.sleep(0.01)
@pytest.mark.asyncio
async def test_11(self): await asyncio.sleep(0.01)
@pytest.mark.asyncio
async def test_12(self): await asyncio.sleep(0.01)
@pytest.mark.asyncio
async def test_13(self): await asyncio.sleep(0.01)
@pytest.mark.asyncio
async def test_14(self): await asyncio.sleep(0.01)
class TestCrash:
@pytest.mark.asyncio
async def test_busyloop_gc_segfault(self):
"""Crashes ~1 in 7 runs."""
gate = Gate()
gate.register("10.8.0.3")
gate.mark_vpn_down()
unblocked = []
async def _waiter(n):
await gate.wait_buggy("10.8.0.3")
unblocked.append(n)
tasks = [asyncio.create_task(_waiter(i)) for i in range(3)]
await asyncio.sleep(0.05)
assert len(unblocked) == 0
gate.mark_vpn_up()
await asyncio.wait_for(asyncio.gather(*tasks), timeout=3.0)
assert len(unblocked) == 3Run with:
# Crashes nondeterministically. Run in a loop:
for i in $(seq 1 10); do
timeout 60 python3 -m pytest test_gh_XXXXX.py -x -q 2>&1 | tail -3
[ $? -eq 139 ] && echo "SEGFAULT on run $i" && break
doneKey observations
- Does not crash outside pytest. The
asyncio.Runnerper-test lifecycle (new loop per test, GC between tests) is required. - Does not crash with
asyncio.run()alone, even with aggressivegc.set_threshold(10, 2, 2). - The padding tests are required -- removing them eliminates the crash (insufficient gen2 pressure).
- The crash site (
Handle.__init__line 36) is the function def line itself, meaning the SIGSEGV happens during Python frame entry when_Py_HandlePendingtriggers GC.
Possibly related issues
- Use-after-free in
asyncioTask deallocation via re-registering task incall_exception_handler#142556 -- UAF inTaskObj_deallocviacall_exception_handlerre-registration (same asyncio GC finalization code path) - Segmentation fault during garbage collection of the asyncio event loop #113566 -- Segfault during GC of asyncio event loop (NULL deref in
FutureObj_reprduring finalization) - Segfault in gc while finalizing #135115 -- Segfault in
gc_get_refsduringmove_unreachableon Python 3.13
Your environment
- CPython version: 3.13.5 (main, Jun 25 2025, 18:55:22) [GCC 14.2.0]
- Operating system and architecture: Linux 6.17.13+2-amd64, x86_64
- pytest 8.3.5, pytest-asyncio 1.3.0
- GIL: enabled (standard build, not free-threaded)
Metadata
Metadata
Assignees
Labels
Projects
Status