MagicStack / MagicStack/uvloop

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

未关闭
#715 4 条评论 2 个 reaction 已指派 0 人 在 GitHub 查看

还没有人认领这个 Issue。

主要语言
Cython
星标
11.9k
派生
616
PR 合并指标
30 天内没有已合并 PR

描述

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.

贡献指南

这个仓库没有索引到贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

调研方向

从 cbhandles.pyx:extract_stack 和 Python 3.14 中 traceback.walk_stack 的行为入手,然后在启用调试模式的情况下使用 uvloop_python314_bug_repro.py 重现该故障。跟踪异步生成器通过 call_soon_threadsafe 进行最终化的过程,并验证重现过程能够正常退出,且不会出现 AttributeError 或段错误。

由索引模型根据 Issue 内容生成。

评估

技术栈
python
领域
backend
Issue 类型
缺陷
难度
4/5
预计耗时
3-5 天
活跃度
冷清
描述清晰度
基本清楚
新手友好度
48/100

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。