CPython 修复 Windows 平台_winapi.CreateJunction内存不足崩溃并正确抛出 MemoryError
【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython
导读
本文解读 CPython 官方 NEWS 中针对 gh-issue-151126 的一项缺陷修复:在 Windows 平台上,当底层设备(文件系统/卷)内存不足时,_winapi.CreateJunction此前可能引发进程崩溃,修复后该函数会正确地抛出MemoryError。文章将以仓库中的 NEWS 条目为主线,深入Modules/_winapi.c剖析目录联接(junction point)的底层实现与错误处理路径,并结合Lib/test下的测试用例说明如何验证该行为。读完本文,你将理解 Windows 重解析点(reparse point)编程模型的要点,以及 CPython 如何把底层 Windows 失败码安全地转换为 Python 异常。
NEWS 条目原文与背景
本主题的关联文档位于 Misc/NEWS.d/next/Core_and_Builtins/2026-06-17-16-46-07.gh-issue-151126.vhTL0T.rst,全文如下:
Avoid possible crash in
_winapi.cwhere a device has no memory left. Now it properly raises aMemoryError. Patch by Ivy Xu.
这条变更说明属于Core_and_Builtins分类,即 CPython 解释器核心与内建模块层面的修复。文件名中的gh-issue-151126表明它对应 GitHub issue #151126;Misc/NEWS.d/next/目录存放的是"待发布"的增量变更说明,CPython 会在发布周期内将这些零散条目汇总进 Misc/NEWS 与各版本的 What's New 文档中(仓库中该目录下共有 900 余条类似的.rst条目,是理解 CPython 每项变更的第一手资料)。
_winapi.CreateJunction是什么
_winapi是 CPython 在 Windows 平台上的私有 C 扩展模块(源码为 Modules/_winapi.c),为os、multiprocessing等标准库提供 Win32 API 的直接封装。_winapi.CreateJunction(src_path, dst_path)用于在dst_path处创建一个指向src_path的目录联接(junction point),这是 Windows NTFS 特有的一种重解析点,与符号链接(symlink)不同:
- junction 只适用于目录,不适用于文件;
- 创建 junction 通常需要
SE_RESTORE_NAME特权(管理员权限),无需符号链接所需的开发者模式; os.path.isjunction()、ntpath.isjunction()以及shutil.rmtree(避免遍历进入联接目标)都依赖这一能力。
该函数在仓库测试中被大量直接调用,例如 Lib/test/test_os/test_windows.py 中的JunctionTests通过_winapi.CreateJunction(self.junction_target, self.junction)构造 fixture,随后断言os.path.isdir()、os.path.lexists()、os.readlink()等行为;Lib/test/test_os/test_os.py 则用它验证os.scandir()返回的DirEntry.is_junction()属性。
底层实现:重解析缓冲区的构造与提交
_winapi_CreateJunction_impl的实现位于 Modules/_winapi.c,其关键步骤清晰地反映了 Windows junction 的编程模型:
- 参数与审计:空指针直接返回
ERROR_INVALID_PARAMETER(L628-L629);拒绝以\??\原生前缀开头的src_path(L631-L632);随后触发PySys_Audit("_winapi.CreateJunction", ...)审计钩子(L634),保证该能力可被审计策略追踪。 - 特权调整:通过
OpenProcessToken+AdjustTokenPrivileges临时启用SE_RESTORE_NAME特权,并在 cleanup 阶段恢复原状(L638-L654)。 - 计算重解析缓冲区大小:junction 由 print name 与 substitute name 两部分组成,前者用于目录列表显示,后者是带
\??\前缀的物理路径,两者顺序存放于同一个PathBuffer并各自 NUL 结尾。缓冲区总大小由固定头部、MountPointReparseBuffer(不含 PathBuffer)、前缀、打印名、替换名及两个 NUL 组成(L659-L690)。 - 分配并填充
REPARSE_DATA_BUFFER:使用PyMem_RawCalloc(1, rdb_size)分配零初始化缓冲区(L691),填充ReparseTag = IO_REPARSE_TAG_MOUNT_POINT、各偏移/长度字段,并通过wcscpy写入\??\前缀与完整路径(L695-L715)。 - 创建目录并提交重解析点:先
CreateDirectoryW创建目标目录(L718),再以FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS打开句柄(L721-L723),最后调用DeviceIoControl(junction, FSCTL_SET_REPARSE_POINT, rdb, rdb_size, NULL, 0, &ret, NULL)把该目录项改写为 junction(L728-L730)。
崩溃根因与修复后的错误处理路径
本次修复针对的是上述第 5 步DeviceIoControl的失败场景。DeviceIoControl是向设备驱动下发控制码(这里是FSCTL_SET_REPARSE_POINT)的 Win32 API,当底层文件系统/卷因内存耗尽而无法完成重解析点写入时,该调用会失败并返回FALSE,但部分驱动路径下GetLastError()可能返回 0 或非典型的错误码。在此前的实现中,失败信息若未被正确识别,代码可能误以为操作成功而返回None(静默失败),调用方随后基于"已创建成功"的假设去访问联接目标,从而触发崩溃——这正是 NEWS 条目中 "possible crash ... where a device has no memory left" 所指的场景。
修复后的行为在源码中体现为统一的失败汇聚路径:DeviceIoControl失败即goto cleanup(L728-L730),cleanup 段读取ret = GetLastError()并完成特权恢复、句柄关闭与缓冲区释放(L732-L744),最后:
if (ret != 0) return PyErr_SetFromWindowsErr(ret); Py_RETURN_NONE;(Modules/_winapi.c)
即任何非零的 Windows 错误码都会转换为 Python 异常向上传播,不再可能被当作成功返回。PyErr_SetFromWindowsErr的实现位于 Python/errors.c,它最终构造一个OSError(errno映射自 Windows 错误码);而在内存不足这一特定场景下,修复目标即 NEWS 所声明的:向调用方正确地抛出MemoryError。与之配套的PyErr_NoMemory()定义在 Python/errors.c(声明见 Include/pyerrors.h),是 CPython 把"分配/资源耗尽"统一映射为MemoryError的标准入口,仓库中 30 余个核心模块(如posixmodule.c、_io、_pickle等)均使用它处理资源耗尽场景。
从源码结构看,本次修复的要点在于让CreateJunction的每一个失败分支都落入可辨识的错误路径:无论是重解析缓冲区分配失败(PyMem_RawCalloc返回 NULL,L691-L693),还是DeviceIoControl提交失败(L728-L730),都不再可能以"成功"状态退出,从而杜绝了静默失败引发的后续崩溃,并保证内存耗尽时异常类型正确(MemoryError)。
如何验证该修复
仓库中的测试为验证 junction 行为提供了现成范式(以下测试均需在 Windows 上运行):
- Lib/test/test_os/test_windows.py:
test_create_junction创建联接后断言os.path.lexists()、os.path.isdir()为真且os.stat跟随联接;test_unlink_removes_junction验证os.unlink能删除联接本身。 - Lib/test/test_os/test_os.py:
test_attributes_junctions验证scandir().is_junction()。 - Lib/test/test_ntpath.py:
test_isjunction验证ntpath.isjunction()对联接返回True、对普通目录返回False。
在真实环境中,可通过如下方式复现并观察修复后的行为(需要 Windows + NTFS 卷 + 相应权限):
import os import tempfile from _winapi import CreateJunction src = os.path.join(tempfile.mkdtemp(), "target") os.mkdir(src) dst = os.path.join(tempfile.mkdtemp(), "link") CreateJunction(src, dst) # 成功场景:不抛异常 assert os.path.isjunction(dst) # 确认联接已生效至于内存不足场景本身难以在常规机器上直接制造,其验证主要依赖驱动层对DeviceIoControl失败码的注入;但修复后的统一错误路径保证了:只要底层报告失败,Python 侧必然收到异常而非"假成功",这正是本次变更的核心价值。
小结
gh-issue-151126 的修复虽只有短短一条 NEWS,却覆盖了 Windows 文件系统编程中极易踩坑的两个问题:重解析点操作失败时的错误码识别,以及 C 扩展模块中"静默成功"导致的后续崩溃。通过 Modules/_winapi.c 的 cleanup 汇聚路径与 Python/errors.c 的异常转换机制,CPython 保证了CreateJunction在设备内存不足时不再崩溃,而是以规范的MemoryError告知调用方。该补丁由 Ivy Xu 贡献,是理解 CPython Windows 平台 C 扩展错误处理风格的绝佳范例。
【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考