ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

CPython 修复 `type()` 选择 `tp_new` 为 NULL 的元类时的崩溃:TypeError 替代空指针解引用

CPython 修复 `type()` 选择 `tp_new` 为 NULL 的元类时的崩溃:TypeError 替代空指针解引用 CPython 修复type()选择tp_new为 NULL 的元类时的崩溃TypeError 替代空指针解引用【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython导读本篇文章围绕 CPython 仓库中一条核心修复记录展开当type()在选择元类metaclass的过程中遇到一个tp_new槽位为NULL的元类时CPython 此前会触发空指针解引用NULL pointer dereference导致解释器崩溃而修复后则统一以TypeError优雅拒绝。文章会结合 CPython 源码Objects/typeobject.c与测试用例Lib/test/test_descr.py、Lib/test/test_capi/test_misc.py讲解崩溃的根因、修复的实现位置与行为变化并给出可复现的触发与验证方法帮助读者理解 CPython 对象模型与元类机制的底层细节。一、修复记录原文本次修复的原始记录位于仓库的新闻条目文件 Misc/NEWS.d/next/Core_and_Builtins/2026-06-22-14-48-22.gh-issue-151912.YcxfnU.rst内容如下Fixed a crash intype()when selecting a metaclass whosetp_newslot isNULL. Such metaclasses are now rejected withTypeErrorinstead of causing a NULL pointer dereference.翻译为中文即修复了type()在选择一个tp_new槽位为NULL的元类时的崩溃问题。这类元类现在会被以TypeError拒绝而不再引发空指针解引用。这条记录属于Core and Builtins类别即 CPython 核心对象系统与内建类型层面的修复说明问题出在解释器最底层的类型创建逻辑中。二、崩溃根因元类选择路径中的空指针解引用2.1 元类选择的整体流程在 CPython 中调用type(name, bases, dict)创建新类时内部核心实现是 Objects/typeobject.c 中的type_new()入口为type_new_get_bases()。其关键步骤如下计算最派生的元类most derived metatype调用_PyType_CalculateMetaclass()见 Objects/typeobject.c#L4126-L4161从显式传入的元类与所有基类的元类中选出一个胜者winner。如果胜者不是当前的元类则说明需要把类创建的契约移交给胜者调用胜者的tp_new来实际创建类型对象。如果胜者的tp_new是NULL代码直接对空指针调用引发崩溃。2.2 崩溃点的精确位置崩溃发生在 Objects/typeobject.c#L5000-L5015 的type_new_get_bases()中if (winner ! ctx-metatype) { if (winner-tp_new ! type_new) { /* Check if tp_new is NULL (cannot instantiate this type) */ if (winner-tp_new NULL) { PyErr_Format(PyExc_TypeError, cannot create %.400s instances, winner-tp_name); return -1; } /* Pass it to the winner */ *type winner-tp_new(winner, ctx-args, ctx-kwds); ...修复后的逻辑在调用winner-tp_new(...)之前先检查winner-tp_new NULL如果为空则调用PyErr_Format(PyExc_TypeError, cannot create %.400s instances, winner-tp_name)设置TypeError异常并返回-1上层type_new()检测到返回值小于 0 后直接返回NULL并传播该异常。修复之前这段代码没有tp_new NULL的检查当胜者元类的tp_new为空时会直接执行winner-tp_new(...)即对一个空函数指针发起调用造成空指针解引用NULL pointer dereference轻则段错误崩溃重则可能被利用造成安全问题。2.3 为什么会出现tp_new为 NULL 的元类tp_new槽位在PyTypeObject中承担创建该类型实例的职责。按照 CPython 的约定一个类型如果不允许被实例化其tp_new可以是NULL——此时任何创建实例的尝试都应被拒绝通常配合Py_TPFLAGS_DISALLOW_INSTANTIATION标志。但在元类选择路径中_PyType_CalculateMetaclass()只关心类型之间的继承关系PyType_IsSubtype并不检查胜者的tp_new是否可用从而让一个不可实例化的元类被推举为类创建的最终执行者最终在调用tp_new时崩溃。从测试辅助代码可见这种元类确实存在于现实中Modules/_testcapi/heaptype.c#L926-L930 定义了一个名为_testcapi.HeapCTypeMetaclassNullNew的测试元类它通过Py_TPFLAGS_DEFAULT | Py_TPFLAGS_DISALLOW_INSTANTIATION标志被声明为禁止实例化且没有提供tp_new槽位static PyType_Spec HeapCTypeMetaclassNullNew_spec { .name _testcapi.HeapCTypeMetaclassNullNew, .flags Py_TPFLAGS_DEFAULT | Py_TPFLAGS_DISALLOW_INSTANTIATION, .slots empty_type_slots };该元类通过PyType_FromMetaclass()创建并注册到_testcapi模块Modules/_testcapi/heaptype.c#L1484-L1486专门用于复现与验证此类场景。三、修复行为与错误信息语义3.1 新增的错误信息修复后触发该场景时会抛出如下形式的TypeErrorTypeError: cannot create MetaName instances其中MetaName为胜者元类的tp_name即元类的限定名。这条消息与 CPython 中其他类型不可实例化场景如 Lib/test/support/init.py#L2548 中辅助函数所使用的cannot create ... instances消息保持了一致的语义明确告知用户当前元类不允许被实例化。3.2 行为对比场景修复前修复后winner-tp_new ! type_new且winner-tp_new ! NULL调用winner-tp_new()完成类创建不变winner-tp_new NULL空指针解引用解释器崩溃抛出TypeError: cannot create ... instances需要说明的是这一检查只发生在元类选择路径中即当type()因为基类元类更派生而需要把创建工作移交给其他元类时。若直接对这类元类调用其自身例如SomeMeta(...)则在常规实例化路径中仍由既有的禁止实例化机制拒绝两者不冲突。四、测试验证仓库中的回归测试修复并非孤立改动仓库配套了两处回归测试从Python 层调用type()与直接调用元类两个角度覆盖该场景。4.1Lib/test/test_descr.py中的回归测试Lib/test/test_descr.py#L818-L825 新增了test_type_with_null_new_metaclass测试刻意走type()的元类选择路径注释明确说明 Exercise type_news metaclass selection path, not a direct callunittest.skipIf(_testcapi is None, need the _testcapi module) def test_type_with_null_new_metaclass(self): metaclass _testcapi.HeapCTypeMetaclassNullNew base _testcapi.pytype_fromspec_meta(metaclass) # Exercise type_news metaclass selection path, not a direct call. with self.assertRaisesRegex(TypeError, rcannot create .* instances): type(Derived, (base,), {})该测试先通过_testcapi.pytype_fromspec_meta(metaclass)以该元类创建一个堆类型base然后调用type(Derived, (base,), {})。此时base的元类就是HeapCTypeMetaclassNullNew在_PyType_CalculateMetaclass()计算出的胜者即为该元类且其tp_new NULL——正是本次修复要保护的崩溃路径。测试断言抛出TypeError且消息匹配cannot create .* instances。4.2Lib/test/test_capi/test_misc.py中的覆盖Lib/test/test_capi/test_misc.py#L707-L720 的test_heaptype_with_custom_metaclass_null_new从两个入口验证def test_heaptype_with_custom_metaclass_null_new(self): metaclass _testcapi.HeapCTypeMetaclassNullNew self.assertIsSubclass(metaclass, type) # Class creation from C t _testcapi.pytype_fromspec_meta(metaclass) self.assertIsInstance(t, type) self.assertEqual(t.__name__, HeapCTypeViaMetaclass) self.assertIs(type(t), metaclass) # Class creation from Python with self.assertRaisesRegex(TypeError, cannot create .* instances): metaclass(PyClassViaMetaclass, (), {})从 C 侧创建_testcapi.pytype_fromspec_meta(metaclass)走的是PyType_FromMetaclass()/PyType_FromSpecWithBases()的 C API 路径不经过type_new的元类选择逻辑因此可以正常创建从 Python 侧创建metaclass(PyClassViaMetaclass, (), {})直接调用该元类来创建类此时元类的tp_new NULL测试断言抛出TypeError: cannot create ... instances。这两组测试共同锁定了修复行为C API 路径不受影响Python 层的type()与元类直接调用则被安全拒绝。五、可复现的触发方式与验证步骤若要亲手验证该修复需使用带_testcapi模块的调试构建_testcapi是 CPython 的 C API 测试扩展普通安装版可能未编译该模块。在 CPython 源码目录下构建解释器后执行cd /data/web/disk1/git_repo/GitHub_Trending/cp/cpython ./configure --with-pydebug make -j$(nproc)然后运行以下 Python 脚本import _testcapi metaclass _testcapi.HeapCTypeMetaclassNullNew base _testcapi.pytype_fromspec_meta(metaclass) # 修复前解释器在此处崩溃空指针解引用 # 修复后抛出 TypeError try: type(Derived, (base,), {}) except TypeError as exc: print(fTypeError: {exc})预期输出修复后TypeError: cannot create _testcapi.HeapCTypeMetaclassNullNew instances同时可以直接运行仓库自带的回归测试确认修复完整./python -m test test_descr -m test_type_with_null_new_metaclass ./python -m test test_capi -m test_heaptype_with_custom_metaclass_null_new两条命令分别执行 Lib/test/test_descr.py 与 Lib/test/test_capi/test_misc.py 中的对应测试均应以成功告终。六、延伸理解_PyType_CalculateMetaclass与元类选择规则为深入理解本次修复有必要回到元类选择的核心函数 Objects/typeobject.c#L4126-L4161 的_PyType_CalculateMetaclass()/* Determine the most derived metatype. */ PyTypeObject * _PyType_CalculateMetaclass(PyTypeObject *metatype, PyObject *bases) { ... nbases PyTuple_GET_SIZE(bases); winner metatype; for (i 0; i nbases; i) { tmp PyTuple_GET_ITEM(bases, i); tmptype Py_TYPE(tmp); if (PyType_IsSubtype(winner, tmptype)) { continue; } if (PyType_IsSubtype(tmptype, winner)) { winner tmptype; continue; } /* else: */ PyErr_SetString(PyExc_TypeError, metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases); return NULL; } return winner; }其算法要点winner初始为调用方传入的元类通常为type本身遍历所有基类取每个基类的元类Py_TYPE(tmp)与当前winner比较继承关系若winner是tmptype的子类型继续若tmptype是winner的子类型则tmptype成为新的winner更派生的元类胜出若两者互不相关抛出metaclass conflict的TypeError最终返回最派生的元类。可以看出该函数只依据类型继承关系做选择不检查胜者的tp_new是否可用。本次修复正是在其下游的调用点type_new_get_bases()中补上了这一缺失的检查属于在正确的位置拒绝非法操作的防御性修复。从源码结构可以推断类似的防御模式也存在于 CPython 其他选择后调用的路径中但本次修复针对的是type()元类选择这一具体入口。七、总结项目内容修复对象type(name, bases, dict)创建类时的元类选择路径根因当选出的元类tp_new NULL时直接调用空函数指针引发 NULL pointer dereference修复方式在 Objects/typeobject.c#L5003-L5008 增加tp_new NULL检查改为抛出TypeError: cannot create ... instances关联测试Lib/test/test_descr.py#L818-L825、Lib/test/test_capi/test_misc.py#L707-L720测试辅助类型Modules/_testcapi/heaptype.c#L926-L930 的HeapCTypeMetaclassNullNew这一修复将解释器底层的崩溃风险转化为明确的异常既提升了稳定性与安全性也让开发者面对元类不可实例化时得到可读、可定位的错误信息。对于编写扩展模块、实现自定义元类的开发者而言理解tp_new槽位在元类选择路径中的角色有助于避免在自己的 C 扩展中复现同类问题。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表