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 需求glibc8 MiB≥ 1 MiBmuslAlpine Linux128 kB≥ 1 MiB当 Python 需要执行深层次递归时128 kB 的默认栈远不够用同时栈边界本身也比 glibc 场景小得多——任何基于猜测或硬编码默认值的栈限制计算在 musl 上都可能严重偏离真实边界导致两种典型故障要么过早触发RecursionError栈其实还有余量要么检查形同虚设接近真实溢出处才被拦截甚至来不及抛出异常。修复方案configure 检测 链接器显式设定栈大小本次修复的整体思路是不在运行时猜测栈有多大而是先探测平台默认栈大小若不足则用链接器参数把主线程栈显式设置为 1 MiB并把该值编译进 Python让栈限制计算与之严格对齐。第一步configure 探测线程栈大小在 configure.ac 中Linux 且非交叉编译环境下会编译并运行一段探针程序核心逻辑是调用pthread_attr_init与pthread_attr_getstacksize取得系统默认线程栈大小若大小小于 1 MiB1024 × 1024 字节探针返回 1ac_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-size1048576告知 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_nppthread_attr_getguardsizepthread_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-size1048576主线程栈被链接器提升到 1 MiB_Py_LINKER_THREAD_STACK_SIZE让Py_C_STACK_SIZE与该值一致栈限制计算与实际布局对齐。相关扩展 API仓库还提供了可供嵌入方显式接管栈保护的稳定 API定义于 Python/ceval.cPyUnstable_ThreadState_SetStackProtection(tstate, stack_start_addr, stack_size)手动指定栈起始地址与大小需 ≥_PyOS_MIN_STACK_SIZE否则抛ValueErrorPyUnstable_ThreadState_ResetStackProtection(tstate)恢复到初始化时记录的栈边界或在未记录时重新初始化。这对于自行管理线程栈的宿主应用如绿色线程、协程调度器尤其有用。验证建议在 Alpine Linuxmusl上从源码构建 CPython观察 configure 输出中checking for thread stack size的结果以及 LDFLAGS 是否包含-Wl,-z,stack-size1048576在生成的头文件中确认_Py_LINKER_THREAD_STACK_SIZE已定义并在 Python/ceval.c 处确认Py_C_STACK_SIZE的取值运行深递归脚本如递归计算斐波那契数列直至抛出RecursionError确认异常在RecursionError层面被捕获而非进程以Unrecoverable stack overflow直接崩溃仓库自带的递归限制测试位于 Lib/test/test_sys.pytest_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),仅供参考