news 2026/9/11 16:38:46

CPython 修复 musl 环境(如 Alpine Linux)下的栈限制检查:由链接器栈大小驱动的递归保护

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CPython 修复 musl 环境(如 Alpine Linux)下的栈限制检查:由链接器栈大小驱动的递归保护

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.

拆解为三个要点:

  1. 问题场景:Python 链接到 musl libc(例如 Alpine Linux 的默认工具链)时,栈限制检查存在缺陷;
  2. 修复手段:改用链接器(linker)设置的栈大小来计算栈限制,而不是依赖运行时对栈边界的估算;
  3. 影响范围:属于 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 MiB
musl(Alpine Linux)128 kB≥ 1 MiB

当 Python 需要执行深层次递归时,128 kB 的默认栈远不够用,同时栈边界本身也比 glibc 场景小得多——任何基于"猜测"或"硬编码默认值"的栈限制计算,在 musl 上都可能严重偏离真实边界,导致两种典型故障:要么过早触发RecursionError(栈其实还有余量),要么检查形同虚设(接近真实溢出处才被拦截,甚至来不及抛出异常)。

修复方案:configure 检测 + 链接器显式设定栈大小

本次修复的整体思路是:不在运行时猜测栈有多大,而是先探测平台默认栈大小,若不足则用链接器参数把主线程栈显式设置为 1 MiB,并把该值编译进 Python,让栈限制计算与之严格对齐。

第一步:configure 探测线程栈大小

在 configure.ac 中,Linux 且非交叉编译环境下会编译并运行一段探针程序,核心逻辑是:

  1. 调用pthread_attr_initpthread_attr_getstacksize取得系统默认线程栈大小;
  2. 若大小小于 1 MiB(1024 × 1024 字节),探针返回 1,ac_cv_thread_stack_size记为1048576
  3. 若大小足够,返回 0,记为default
  4. 编译或运行失败,记为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 的栈限制要么基于过小的估算、要么基于与其他平台相同的默认值,深递归程序可能被错误地判定为栈溢出,或在栈真正耗尽前得不到有效保护。修复后:

  1. configure 探测到 musl 默认 128 kB 栈后自动追加-Wl,-z,stack-size=1048576,主线程栈被链接器提升到 1 MiB;
  2. _Py_LINKER_THREAD_STACK_SIZEPy_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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 16:37:41

Anki 如何用过滤牌组(Filtered Deck)按指定顺序临时复习卡片?

Anki 如何用过滤牌组&#xff08;Filtered Deck&#xff09;按指定顺序临时复习卡片&#xff1f; 【免费下载链接】anki Anki is a smart spaced repetition flashcard program 项目地址: https://gitcode.com/GitHub_Trending/an/anki 当你需要临时离开日常计划——比如…

作者头像 李华
网站建设 2026/9/11 16:33:51

LeetCode 1515题解:Weiszfeld算法求解服务中心最佳位置

1. 问题背景与理解这道LeetCode困难题1515"服务中心的最佳位置"描述了一个典型的设施选址优化问题。题目给定平面上的一组客户点坐标&#xff0c;要求找到一个服务中心的位置&#xff0c;使得该中心到所有客户点的欧几里得距离之和最小。这在实际应用中非常常见&…

作者头像 李华
网站建设 2026/9/11 16:31:16

Android图形架构演进:BufferQueue与BLASTBufferQueue深度解析

1. 项目概述&#xff1a;Android图像显示框架的核心演进在Android图形系统的演进历程中&#xff0c;BufferQueue始终扮演着连接生产者和消费者的关键角色。随着Android 15的发布&#xff0c;BLASTBufferQueue的引入标志着图形架构的又一次重大革新。作为长期从事Android底层开发…

作者头像 李华
网站建设 2026/9/11 16:30:59

SAP重分类在月末结账中的核心作用与技术实现

1. SAP重分类在月末结账中的核心作用 在SAP财务模块的实际操作中&#xff0c;重分类&#xff08;Reclassification&#xff09;是每个会计期末必须执行的标准化操作流程。作为资深SAP顾问&#xff0c;我处理过上百家企业的月结流程&#xff0c;可以明确地说&#xff1a;重分类的…

作者头像 李华
网站建设 2026/9/11 16:30:42

单通道脑电睡眠分期实战:从Sleep-EDF数据到GRU模型

简介&#xff1a;这套源码实现了基于单通道脑电信号的自动睡眠分期&#xff0c;面向计算机、数学、电子信息等专业学生及时序分类初学者&#xff0c;可作为课程设计、毕业设计或入门神经网络的参考项目。项目基于Sleep-EDF公开数据集&#xff0c;覆盖153条整晚睡眠记录&#xf…

作者头像 李华