MagicStack / MagicStack/uvloop

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

Đang mở
#715 4 bình luận 2 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

Ngôn ngữ chính
Cython
Star
11.9k
Fork
616
Chỉ số merge pull request
Không có pull request nào được merge trong 30 ngày

Mô tả

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

Hướng dẫn đóng góp

Chưa lập chỉ mục được hướng dẫn đóng góp cho kho mã nguồn này

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Hướng nghiên cứu

Bắt đầu với cbhandles.pyx:extract_stack và hành vi của traceback.walk_stack trong Python 3.14, sau đó tái hiện lỗi bằng uvloop_python314_bug_repro.py với chế độ debug được bật. Theo dõi quá trình hoàn tất của async-generator thông qua call_soon_threadsafe và xác minh rằng quá trình tái hiện kết thúc sạch sẽ mà không có AttributeError hoặc lỗi phân đoạn.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
python
Lĩnh vực
backend
Loại issue
Lỗi
Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức độ hoạt động
Ít trao đổi
Độ rõ ràng
Khá rõ ràng
Mức phù hợp với người mới
48/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.