Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

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

オープン
#715 コメント 4 件 リアクション 2 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
48/100
issue の種類
バグ
明瞭さ
おおむね明確
活発さ
静か
技術スタック
python
領域
backend

調査の方向性

cbhandles.pyx:extract_stack と Python 3.14 における traceback.walk_stack の動作から始め、次にデバッグモードを有効にして uvloop_python314_bug_repro.py を使い、失敗を再現します。call_soon_threadsafe を通じた非同期ジェネレーターのファイナライズを追跡し、AttributeError やセグメンテーションフォルトが発生せずに再現処理が正常終了することを確認します。

索引モデルが issue の本文から書いたものです。

説明

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
"""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:

  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.

主要言語
Cython
スター
11.9k
フォーク
615
PR マージ指標
30日以内にマージされた PR はありません

コントリビューションガイド

このリポジトリのコントリビューションガイドは索引されていません

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

MagicStack/uvloop のほかの issue

MagicStack/uvloop の issue をすべて見る

似ている issue

Backend & API Design の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。