CPython 修复 musl 环境(如 Alpine Linux)下的栈限制检查:由链接器栈大小驱动的递归保护
【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython
本篇技术指南围绕 CPython 仓库中的一条核心修复记录(Misc/NEWS.d/next/Core_and_Builtins/2026-06-16-17-23-37.gh-issue-151546.LhiaZz.rst,对应 gh-issue-151546,由 Victor Stinner 提交)展开:当 Python 链接到 musl libc(典型如 Alpine Linux)时,栈限制检查(stack limit check)曾被错误触发,修复方式改为使用链接器设置的线程栈大小来计算栈限制。读完本文,你将理解 CPython 递归保护中"软/硬栈限制"的完整机制、musl 与 glibc 在默认线程栈大小上的关键差异,以及 configure 检测与链接器参数如何协同修正这一平台缺陷。
修复内容速览
该 NEWS 条目原文只有两句话,但信息密度很高:
Fix the stack limit check if Python is linked to musl (ex: Alpine Linux). Use the stack size set by the linker to compute the stack limits. Patch by Victor Stinner.
拆解为三个要点:
- 问题场景:Python 链接到 musl libc(例如 Alpine Linux 的默认工具链)时,栈限制检查存在缺陷;
- 修复手段:改用链接器(linker)设置的栈大小来计算栈限制,而不是依赖运行时对栈边界的估算;
- 影响范围:属于 Core and Builtins 类别,即解释器核心的 C 实现层面改动。
问题根源:musl 与 glibc 的默认线程栈大小差异
要理解这次修复,必须先弄清两个 libc 在默认线程栈大小上的巨大差异。CPython 的 configure.ac 中有一段非常明确的注释:
On Linux, check the thread stack size. musl (ex: Alpine Linux) uses a default thread stack size of 128 kB, whereas the glibc uses 8 MiB. Python needs at least 1 MiB.
也就是说:
| libc | 默认线程栈大小 | Python 需求 |
|---|---|---|
| glibc | 8 MiB | ≥ 1 MiB |
| musl(Alpine Linux) | 128 kB | ≥ 1 MiB |
当 Python 需要执行深层次递归时,128 kB 的默认栈远不够用,同时栈边界本身也比 glibc 场景小得多——任何基于"猜测"或"硬编码默认值"的栈限制计算,在 musl 上都可能严重偏离真实边界,导致两种典型故障:要么过早触发RecursionError(栈其实还有余量),要么检查形同虚设(接近真实溢出处才被拦截,甚至来不及抛出异常)。
修复方案:configure 检测 + 链接器显式设定栈大小
本次修复的整体思路是:不在运行时猜测栈有多大,而是先探测平台默认栈大小,若不足则用链接器参数把主线程栈显式设置为 1 MiB,并把该值编译进 Python,让栈限制计算与之严格对齐。
第一步:configure 探测线程栈大小
在 configure.ac 中,Linux 且非交叉编译环境下会编译并运行一段探针程序,核心逻辑是:
- 调用
pthread_attr_init与pthread_attr_getstacksize取得系统默认线程栈大小; - 若大小小于 1 MiB(1024 × 1024 字节),探针返回 1,
ac_cv_thread_stack_size记为1048576; - 若大小足够,返回 0,记为
default; - 编译或运行失败,记为
unknown。
第二步:通过链接器设置栈大小
当探测结果既不是default也不是unknown时,configure.ac 会做两件事:
LDFLAGS="$LDFLAGS -Wl,-z,stack-size=$ac_cv_thread_stack_size" AC_DEFINE_UNQUOTED([_Py_LINKER_THREAD_STACK_SIZE], [$ac_cv_thread_stack_size], [Thread stack size set by the linker (in bytes).])-Wl,-z,stack-size=1048576告知 GNU 链接器把可执行文件主线程的栈大小设置为 1 MiB;_Py_LINKER_THREAD_STACK_SIZE宏把同一数值(字节单位)编译进解释器,供 Python/ceval.c 使用。
这正是 NEWS 条目所说"use the stack size set by the linker to compute the stack limits"的落地方式:先由链接器把事实上的栈大小固定下来,再让栈限制计算使用同一个确定值,从而消除 musl 上"实际栈边界"与"计算假设"之间的偏差。
源码实现:栈限制如何被计算与使用
Py_C_STACK_SIZE 的取值
Python/ceval.c 定义了递归保护依赖的核心常量Py_C_STACK_SIZE:
#if defined(_Py_LINKER_THREAD_STACK_SIZE) # define Py_C_STACK_SIZE _Py_LINKER_THREAD_STACK_SIZE #elif ... # define Py_C_STACK_SIZE 320000 // 部分平台默认 # define Py_C_STACK_SIZE 1200000 # define Py_C_STACK_SIZE 1600000 # define Py_C_STACK_SIZE 2000000 # define Py_C_STACK_SIZE 4000000 #endif当 configure 探测到栈大小异常(如 musl 的 128 kB 场景)并通过_Py_LINKER_THREAD_STACK_SIZE设置 1 MiB 后,Py_C_STACK_SIZE就会精确等于链接器设定的值,栈限制计算与真实栈布局保持一致。
hardware_stack_limits:栈边界的获取策略
Python/ceval.c 中的hardware_stack_limits()按平台分三种策略:
- Windows:调用
GetCurrentThreadStackLimits直接取得系统维护的栈低/高地址; - macOS:通过
pthread_get_stackaddr_np/pthread_get_stacksize_np获取; - Linux 及其他:在 glibc(或非 Linux 平台)下走
pthread_getattr_np+pthread_attr_getguardsize+pthread_attr_getstack精确查询;否则回退到基于当前栈指针sp的估算路径:
#if _Py_STACK_GROWS_DOWN uintptr_t top_addr = _Py_SIZE_ROUND_UP(sp + 8*sizeof(void*), SYSTEM_PAGE_SIZE); *top = top_addr; *base = top_addr - Py_C_STACK_SIZE; #else ... #endif这条估算路径正是本次修复的关键所在——它在 musl 上会被选中(代码注释明确说明:musl 虽然声明支持pthread_getattr_np,但在 Alpine 上返回的栈大小远小于预期,会"impose undue limits",因此按"musl 不是 glibc"处理)。估算路径的精度完全依赖Py_C_STACK_SIZE与实际栈一致,修复后两者都被链接器参数统一为 1 MiB,估算才可靠。
tstate_set_stack:软/硬限制与边距
得到base/top后,Python/ceval.c 的tstate_set_stack()在栈向下生长(_Py_STACK_GROWS_DOWN)时设置三个关键字段:
_tstate->c_stack_top = top; _tstate->c_stack_hard_limit = base + _PyOS_STACK_MARGIN_BYTES; _tstate->c_stack_soft_limit = base + _PyOS_STACK_MARGIN_BYTES * 2;c_stack_top:栈顶(起点);c_stack_hard_limit:硬限制,越界即不可恢复;c_stack_soft_limit:软限制,越界即触发递归深度检查。
函数内部带断言校验hard_limit <= soft_limit < c_stack_top,并保证(top - base) >= _PyOS_MIN_STACK_SIZE;若启用 ThreadSanitizer(_Py_THREAD_SANITIZER),还会把可用栈减半以避免 TSan 崩溃。
_Py_CheckRecursiveCall:软硬限制的分级响应
Python/ceval.c 的_Py_CheckRecursiveCall()是栈保护的执行者(仅在栈指针越过c_stack_soft_limit时才被_Py_EnterRecursiveCallTstate调用,见 Include/internal/pycore_ceval.h 中的软限制判定宏):
- 栈指针越过硬限制:打印
Unrecoverable stack overflow (used %d kB)并调用Py_FatalError——此时栈已不足以安全抛出异常; - 栈指针位于硬限制与软限制之间:进入常规递归深度检查路径,最终抛出
RecursionError; - 若距离硬限制超过一个
_PyOS_STACK_MARGIN_BYTES边距:判定为"栈已切换"(如协程/绿色线程场景),直接放行。
这套分级机制由_Py_InitializeRecursionLimits()(Python/ceval.c)在线程状态初始化时建立,同时保存初始 base/top 供恢复使用。
修复的实际影响与验证方式
对 musl 用户的直接影响
在修复前,musl 环境(Alpine Linux 默认即此)下 Python 的栈限制要么基于过小的估算、要么基于与其他平台相同的默认值,深递归程序可能被错误地判定为栈溢出,或在栈真正耗尽前得不到有效保护。修复后:
- configure 探测到 musl 默认 128 kB 栈后自动追加
-Wl,-z,stack-size=1048576,主线程栈被链接器提升到 1 MiB; _Py_LINKER_THREAD_STACK_SIZE让Py_C_STACK_SIZE与该值一致,栈限制计算与实际布局对齐。
相关扩展 API
仓库还提供了可供嵌入方显式接管栈保护的稳定 API,定义于 Python/ceval.c:
PyUnstable_ThreadState_SetStackProtection(tstate, stack_start_addr, stack_size):手动指定栈起始地址与大小(需 ≥_PyOS_MIN_STACK_SIZE,否则抛ValueError);PyUnstable_ThreadState_ResetStackProtection(tstate):恢复到初始化时记录的栈边界,或在未记录时重新初始化。
这对于自行管理线程栈的宿主应用(如绿色线程、协程调度器)尤其有用。
验证建议
- 在 Alpine Linux(musl)上从源码构建 CPython,观察 configure 输出中
checking for thread stack size的结果,以及 LDFLAGS 是否包含-Wl,-z,stack-size=1048576; - 在生成的头文件中确认
_Py_LINKER_THREAD_STACK_SIZE已定义,并在 Python/ceval.c 处确认Py_C_STACK_SIZE的取值; - 运行深递归脚本(如递归计算斐波那契数列直至抛出
RecursionError),确认异常在RecursionError层面被捕获,而非进程以Unrecoverable stack overflow直接崩溃; - 仓库自带的递归限制测试位于 Lib/test/test_sys.py(
test_getrecursionlimit/test_setrecursionlimit),可在 musl 环境下运行以验证sys.getrecursionlimit()/sys.setrecursionlimit()行为正常。
小结
这条 NEWS 条目虽然简短,却完整记录了一次典型的"平台差异驱动的解释器核心修复":问题根因是 musl 与 glibc 默认线程栈大小相差 64 倍(128 kB vs 8 MiB),修复方案不是去猜栈边界,而是由 configure 探测、链接器设定(-Wl,-z,stack-size)、编译期宏(_Py_LINKER_THREAD_STACK_SIZE)与运行时软/硬限制检查(Python/ceval.c 中的c_stack_soft_limit/c_stack_hard_limit)四者联动,让 Python 在 Alpine Linux 等 musl 发行版上获得与 glibc 平台一致的递归保护体验。
【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考