-
Notifications
You must be signed in to change notification settings - Fork 600
Description
Overview
When an async generator is finalized under uvloop debug mode, uvloop’s extract_stack() calls traceback.walk_stack() on Python 3.14. The stack walker receives a non-frame object (e.g. _asyncio.TaskStepMethWrapper) and crashes (traceback.walk_stack_generator) or raises AttributeError. It's worth mentioning this does NOT happen with asyncio debug.
Environment
- uvloop 0.22.1
- Python 3.14.0
- macOS (Darwin 24.6.0, arm64)
- Debug mode enabled (PYTHONASYNCIODEBUG=1 or uvloop.run(debug=True))
Reproduction
- Set
PYTHONFAULTHANDLER=1 PYTHONASYNCIODEBUG=1as environment variables. - Run the following script with
python3 uvloop_python314_bug_repro.py
"""uvloop_python314_bug_repro.py
Minimal repro: uvloop 0.22.1 + Python 3.14 debug async-gen finalization crash.
This creates a nested async-generator chain and breaks on a "[DONE]" sentinel
without closing the inner iterator. Under uvloop debug, async generator
finalization schedules aclose() via call_soon_threadsafe, which calls
extract_stack(). On Python 3.14, traceback.walk_stack sees a non-frame object
(for example, _asyncio.TaskStepMethWrapper) and raises AttributeError. The
unpatched build can segfault in traceback.walk_stack_generator.
Run:
uv run --active --no-sync python uvloop_python314_bug_repro.py
"""
from __future__ import annotations
import asyncio
import gc
import os
import sys
from typing import AsyncIterator
import uvloop
os.environ["PYTHONASYNCIODEBUG"] = "1"
REPEATS = 1
class FakeResponse:
async def aiter_raw(self) -> AsyncIterator[bytes]:
payloads = [
b"data: one\n\n",
b"data: [DONE]\n\n",
b"data: trailing\n\n",
]
for payload in payloads:
yield payload
await asyncio.sleep(0)
async def aclose(self) -> None:
await asyncio.sleep(0)
class Decoder:
async def _aiter_chunks(self, source: AsyncIterator[bytes]) -> AsyncIterator[bytes]:
async for chunk in source:
yield chunk
async def aiter_bytes(self, source: AsyncIterator[bytes]) -> AsyncIterator[str]:
async for chunk in self._aiter_chunks(source):
for line in chunk.splitlines():
yield line.decode("utf-8")
class AsyncStream:
def __init__(self) -> None:
self.response = FakeResponse()
self.decoder = Decoder()
self._iterator: AsyncIterator[str] = self.__stream__()
def __aiter__(self) -> AsyncIterator[str]:
return self._iterator
async def __stream__(self) -> AsyncIterator[str]:
iterator = self._iter_events()
try:
async for event in iterator:
if event.endswith("[DONE]"):
break
yield event
finally:
await self.response.aclose()
async def _iter_events(self) -> AsyncIterator[str]:
async for event in self.decoder.aiter_bytes(self.response.aiter_raw()):
yield event
async def exercise_once() -> None:
stream = AsyncStream()
async for _ in stream:
pass
del stream
gc.collect()
await asyncio.sleep(0)
async def main() -> None:
loop = asyncio.get_running_loop()
print(f"Python: {sys.version}")
print(f"Loop: {type(loop).__name__}")
print(f"Debug: {loop.get_debug()}")
for _ in range(REPEATS):
await exercise_once()
await asyncio.sleep(0.1)
if __name__ == "__main__":
print(f"uvloop: {uvloop.__version__}")
uvloop.run(main(), debug=True)Expected
Clean exit (debug stack capture should never crash the process).
Actual
Fatal Python error: Segmentation fault
File ".../traceback.py", line 393 in walk_stack_generator
...Ideas
Python 3.14 changed frame introspection behavior. During async generator finalization, uvloop's debug stack capture (cbhandles.pyx:extract_stack) calls traceback.walk_stack(). The stack can contain non-frame objects like _asyncio.TaskStepMethWrapper which lack the f_back attribute that walk_stack_generator expects.
The crash path:
- Async generator breaks early (e.g., on
[DONE]sentinel) - GC finalizes the generator
- Finalizer schedules
aclose()viacall_soon_threadsafe - uvloop debug captures stack →
traceback.walk_stack()→ crash
This happens during async generator finalization; the finalizer schedules aclose() via call_soon_threadsafe, which triggers uvloop’s debug stack capture. A defensive guard before calling traceback.walk_stack (e.g. require a real frame, or swallow AttributeError/TypeError) avoids the crash.
Workaround
Explicitly close/break async generator chains before they go out of scope, or disable uvloop debug mode on Python 3.14.