python / python/cpython

RemoteUnwinder.get_async_stack_trace() crashes with a C stack overflow on a deeply nested awaited_by graph instead of raising

未关闭
#151,535 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

还没有人认领这个 Issue。

interpreter-core topic-profiling type-bug
主要语言
Python
星标
77.2k
派生
35.9k
PR 合并指标
PR 指标待抓取

描述

Bug description

_remote_debugging.RemoteUnwinder.get_async_stack_trace() reconstructs the async
call graph of a target process by recursing up the awaited_by relation. The
recursion is a three-function cycle with no depth limit, cycle detection, or
_Py_EnterRecursiveCall guard:

process_task_and_waitersprocess_task_awaited_byprocess_waiter_task
process_task_and_waiters (…)

(On main these are in Modules/_remote_debugging/asyncio.c; on 3.14 they are in
the single-file Modules/_remote_debugging_module.c.) Each level also stack-allocates
a char task_obj[SIZEOF_TASK_OBJ] (SIZEOF_TASK_OBJ == 4096), so ~1900 levels
exhaust a default 8 MiB stack. When the target's running task sits at the bottom of
a sufficiently deep awaited_by chain, the debugger process (the one calling
get_async_stack_trace()) overflows its C stack and dies with SIGSEGV.

This is asymmetric with the iterative sibling path: get_all_awaited_by /
append_awaited_by_for_thread bounds its walk with MAX_ITERATIONS = 2 << 15.
Only the recursive get_async_stack_trace path is unbounded. The module already
treats the target's tables as untrusted input (debug_offsets_validation.h) and the
thread-list walk already has explicit "corrupted remote memory" cycle detection, so
bounding this traversal is consistent with the module's existing invariants.

The same pattern (C recursion converted to RecursionError instead of a segfault)
was treated as a bug in https://github.com/python/cpython/issues/137894.

Reproducer

A target with a deep linear awaited_by chain whose leaf is the running task; a
second process attaches and calls get_async_stack_trace():

# target.py <N>:  tN await t(N-1) await ... await leaf;  leaf busy-spins (= running task)
# attacker.py <pid>:
from _remote_debugging import RemoteUnwinder
RemoteUnwinder(int(pid)).get_async_stack_trace()
  • N = 10 → returns a stack trace cleanly (exit 0).
  • N >= ~2000 → attacker process SIGSEGV (exit 139). gdb shows ~1884 stacked
    process_task_awaited_by frames terminating at a guard-page fault.

(Full PoC scripts available on request.)

Reproduced on
  • CPython 3.14.6 (GA, python:3.14 image, aarch64 Linux) — crashes.
  • CPython 3.16 main (local --with-pydebug build) — same unguarded recursion in source.

Cross-process attach uses the normal Linux ptrace/process_vm_readv path
(--cap-add=SYS_PTRACE); the crash is in the debugger, driven by the target's
graph shape. A privileged profiler/observability tool attaching to an untrusted (or
just legitimately deeply nested) workload is the realistic setting.

Expected behavior

A bounded traversal — raise/propagate an error (as the iterative path does on hitting
its limit), not crash the debugger process.

Proposed fix

Bound the process_task_and_waitersprocess_waiter_task recursion, matching the
iterative sibling. I have a PR ready that adds an explicit recursion-depth cap
(MAX_TASK_AWAITED_BY_DEPTH, mirroring the existing MAX_ITERATIONS /
MAX_SET_TABLE_SIZE constants in the module) and raises a RuntimeError on
overflow, which also handles a cyclic awaited_by graph from corrupted remote
memory. Happy to open it.

Linked PRs
  • gh-151536

贡献指南

打开贡献指南

从这里开始

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

调研方向

从 Modules/_remote_debugging/asyncio.c 以及 process_task_and_waiters、process_task_awaited_by 和 process_waiter_task 入口点开始;在 3.14 上则使用 Modules/_remote_debugging_module.c。将此递归路径与 get_all_awaited_by 和 append_awaited_by_for_thread 进行比较,包括它们的 MAX_ITERATIONS 上限。完成的标准是:深度嵌套或循环的 awaited_by 图会引发错误,而不是导致调试器进程崩溃。

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

评估

技术栈
c, python
领域
devtools
Issue 类型
缺陷
难度
4/5
预计耗时
3-5 天
活跃度
停滞
描述清晰度
描述清楚
新手友好度
25/100

把新 issue 发到你的邮箱

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