CPython 修复 `types.GenericAlias` 参数缓存后动态挂载 `__typing_subst__` 导致的崩溃

发布时间:2026/9/10 9:22:44
CPython 修复 `types.GenericAlias` 参数缓存后动态挂载 `__typing_subst__` 导致的崩溃 CPython 修复types.GenericAlias参数缓存后动态挂载__typing_subst__导致的崩溃【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython本篇文章基于 CPython 仓库中的修复记录Misc/NEWS.d/next/Core_and_Builtins/2026-08-13-13-50-00.gh-issue-155752.Rp7K2x.rst展开深入剖析types.GenericAlias的参数缓存机制、__typing_subst__钩子的作用以及参数在缓存之后才获得钩子这一竞态场景是如何从一次原生崩溃演变为清晰报错的。读完本文你将理解泛型别名底层替换的调用链掌握如何阅读与验证这类 Core and Builtins 级别的崩溃修复。背景types.GenericAlias与参数缓存机制types.GenericAlias是 PEP 585 引入的运行时泛型表示例如list[int]、dict[str, T]在运行时的真实类型。它的 C 实现位于 Objects/genericaliasobject.c内部结构gaobject保存了四个核心字段origin原始泛型类型如list、dictargs下标参数元组如(int,)或(str, T)parameters从args中提取出的类型变量元组如(T,)首次访问后即被缓存starred是否为星号展开形式如*tuple[int]。其中parameters的懒计算与缓存是本次修复的核心。看 Objects/genericaliasobject.c 中的ga_parameters_lock_heldstatic PyObject * ga_parameters_lock_held(PyObject *self) { _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self); gaobject *alias (gaobject *)self; if (alias-parameters NULL) { alias-parameters _Py_make_parameters(alias-args); if (alias-parameters NULL) { return NULL; } } return Py_NewRef(alias-parameters); }可以看到parameters仅在首次访问如读取alias.__parameters__或执行下标操作时通过_Py_make_parameters计算一次此后一直复用缓存值。_Py_make_parameters的完整逻辑见 Objects/genericaliasobject.c它遍历args中的每一项通过PyObject_HasAttrWithError(t, _Py_ID(__typing_subst__))判断该项是否自带替换钩子从而决定是将其直接收入parameters还是继续向下钻取其__parameters__。__typing_subst__钩子的职责__typing_subst__是 typing 体系内部使用的私有协议方法负责把某个类型变量替换为具体实参。在 CPython 中TypeVar、ParamSpec、TypeVarTuple三种类型变量都在 C 层暴露了该钩子见 Objects/typevarobject.ctypevar.__typing_subst__、Objects/typevarobject.cparamspec.__typing_subst__与 Objects/typevarobject.ctypevartuple.__typing_subst__。标准库 Lib/typing.py 的_collect_type_parameters同样依赖该钩子来收集泛型参数elif hasattr(t, __typing_subst__): if t not in parameters: ... parameters.append(t)而 Lib/typing.py 的_make_substitution则在替换阶段调用它substfunc getattr(old_arg, __typing_subst__, None) if substfunc: new_arg substfunc(new_arg_by_param[old_arg])对应地C 层的_Py_subs_parametersObjects/genericaliasobject.c在替换时也是先通过PyObject_GetOptionalAttr(arg, _Py_ID(__typing_subst__), subst)取钩子存在则调用PyObject_CallOneArg(subst, argitems[iparam])完成替换。Bug 本质缓存先行钩子后到本次修复gh-issue-155752针对的正是缓存与钩子出现的时间差创建types.GenericAlias(dict, (first, late))时两个参数对象都还没有__typing_subst__首次访问alias.__parameters__触发_Py_make_parameters由于当时两个参数都没有钩子计算出的缓存元组是(first,)late未被收录之后late对象动态获得了__typing_subst__钩子再次对别名执行下标操作alias[0]时_Py_subs_parameters发现late已经带钩子但在已缓存的parameters元组中却找不到late。修复前这一参数在缓存之后才拥有钩子的状态会使 C 层在tuple_index()返回 -1 后继续以 -1 作为下标访问argitems[iparam]属于越界读取可能直接导致解释器崩溃crash这正是 NEWS 条目中所说的 Fix a crash。修复实现把越界崩溃变为清晰的 TypeError修复落在 Objects/genericaliasobject.c 的_Py_subs_parameters中核心是对找不到参数的情况先做显式检查再报错if (subst) { Py_ssize_t iparam tuple_index(parameters, nparams, arg); if (iparam 0) { // __parameters__ may be stale if an argument gained // __typing_subst__ after the tuple was computed. PyErr_Format(PyExc_TypeError, argument %R with __typing_subst__ was not found in __parameters__, arg); arg NULL; } else { arg PyObject_CallOneArg(subst, argitems[iparam]); } Py_DECREF(subst); }源码注释直接点明了该场景__parameters__may be stale if an argument gained__typing_subst__after the tuple was computed.如果某个参数在元组计算之后才获得__typing_subst__那么__parameters__可能是过期的。当tuple_index返回 -1 时现在会抛出TypeError: argument ... with __typing_subst__ was not found in __parameters__而不再对argitems[-1]进行越界访问。上层调用方ga_getitemObjects/genericaliasobject.c在_Py_subs_parameters返回 NULL 时直接把该异常传递给用户异常处理链完整闭合。测试验证从复现到回归仓库测试 Lib/test/test_typing.py 中的test_parameter_added_after_parameters_cached精确复现并锁定了这一场景def test_parameter_added_after_parameters_cached(self): # gh-155752: GenericAlias parameters are cached before substitution, so # an argument can gain __typing_subst__ after the tuple is calculated. class Parameter: pass first Parameter() first.__typing_subst__ lambda value: value late Parameter() alias types.GenericAlias(dict, (first, late)) self.assertEqual(alias.__parameters__, (first,)) late.__typing_subst__ lambda value: value with self.assertRaisesRegex(TypeError, not found in __parameters__): alias[0]测试流程与 bug 触发条件一一对应先缓存参数、再挂载钩子、最后断言抛出包含 not found inparameters 的TypeError。值得注意的是测试注释中引用的gh-155752正是本 NEWS 条目2026-08-13提交文件名即 issue 编号对应的 issue 编号测试与修复记录相互印证。同族缺陷__typing_subst__返回非元组的崩溃与本修复同属__typing_subst__钩子引发的崩溃族Misc/NEWS.d/3.15.0a1.rst第 4451-4457 行记录了 gh-issue-138479当泛型对象的__typing_subst__返回的不是tuple时同样会崩溃修复后抛出TypeError: expected __typing_subst__ of ... to return a tuple, not ...。该场景的 C 层保护同样位于 Objects/genericaliasobject.c当参数被标记为已解包的 TypeVarTupleunpack为真时要求__typing_subst__的返回值必须是元组否则报错。对应的测试test_return_non_tuple_while_unpackingLib/test/test_typing.py构造了一个恶意类型变量EvilTypeVar其__typing_subst__返回42并通过类型别名type type_alias[*_] 0触发替换最终断言抛出TypeError且消息包含__typing_subst__、tuple、int。两个修复共同反映了同一个设计原则对不可信的动态钩子尤其是私有协议钩子的返回值与查找结果C 层必须做防御性校验把未定义行为转化为可诊断的TypeError。如何复现与验证直接运行回归测试在仓库根目录执行./python -m test test_typing -m test_parameter_added_after_parameters_cached需先完成源码构建可验证修复是否生效手工复现按上述测试代码先访问__parameters__再给参数对象挂载__typing_subst__最后执行alias[0]在修复后的解释器上应得到TypeError: argument ... with __typing_subst__ was not found in __parameters__而非进程崩溃阅读源码沿着ga_getitem→ga_parameters_lock_held→_Py_make_parameters→_Py_subs_parameters的调用链均位于 Objects/genericaliasobject.c可以完整理解从参数收集、缓存到替换的每一个环节。小结本次修复解决的是types.GenericAlias在参数元组已缓存、而某个参数对象随后才获得__typing_subst__钩子这一竞态场景下的越界崩溃问题。修复方案没有推翻缓存设计而是在替换路径上增加了显式的钩子存在但参数不在缓存中检查将不可预测的原生崩溃转换为语义清晰的TypeError并配套了可复现的回归测试。对于 CPython 核心开发者而言它演示了处理类型系统私有协议钩子时的防御性编码范式对于一般使用者它解释了为什么在动态修改类型参数对象属性后泛型别名会抛出看起来奇怪的异常——这实际上是对潜在内存错误的正确拦截。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考