MagicStack / MagicStack/uvloop

uvloop debug mode crashes on Python 3.14 during async generator finalization (traceback.walk_stack sees non-frame object)

Aperta
#715 4 commenti 2 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Lingua principale
Cython
Stelle
11.9k
Fork
616
Metriche di merge delle PR
Nessuna PR unita negli ultimi 30g

Descrizione

## 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

1. Set `PYTHONFAULTHANDLER=1 PYTHONASYNCIODEBUG=1` as environment variables.
2. Run the following script with `python3 uvloop_python314_bug_repro.py`

```python
"""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

```console
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:

1. Async generator breaks early (e.g., on `[DONE]` sentinel)
2. GC finalizes the generator
3. Finalizer schedules `aclose()` via `call_soon_threadsafe`
4. 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.

Guida per i contributori

Nessuna guida per i contributori indicizzata per questo repository

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Direzione di ricerca

Inizia da cbhandles.pyx:extract_stack e dal comportamento di traceback.walk_stack in Python 3.14, quindi riproduci il problema usando uvloop_python314_bug_repro.py con la modalità di debug abilitata. Traccia la finalizzazione dei generatori asincroni attraverso call_soon_threadsafe e verifica che la riproduzione termini correttamente senza un AttributeError o un segmentation fault.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Valutazione

Stack tecnologico
python
Ambito
backend
Tipo di issue
Bug
Difficoltà
4/5
Tempo stimato
3-5 giorni
Stato di attività
Tranquilla
Chiarezza
Abbastanza chiara
Idoneità per principianti
48/100

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.