CPython 编译期防御:AST 标识符字段禁用 `None`/`True`/`False` 常量名
CPython 编译期防御AST 标识符字段禁用None/True/False常量名【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython导读本文围绕 CPython 仓库中一条核心缺陷修复记录Misc/NEWS.d/next/Core_and_Builtins/2020-06-23-13-59-37.gh-issue-85260.o_LJ76.rst对应 gh-85260展开深入剖析compile()在编译期对 AST 节点标识符字段的合法性校验机制。你将理解当手工构造的 AST 把函数名、类名、导入模块名或异常捕获变量等标识符字段篡改为None、True、False时CPython 为何从“调试构建下崩溃”改为“稳定抛出ValueError”并掌握ast模块、compile()与验证器 Python/ast.c 之间的完整调用链与实战规避方法。修复内容从崩溃到ValueError原始缺陷gh-85260 描述的缺陷如下在调试构建debug build下若compile()接收的 AST 中某个标识符identifier字段——例如函数名、类名、导入的模块名或捕获异常的变量名——被设为None、True或False之一CPython 会直接**崩溃crash**而非给出可捕获的 Python 异常。例如通过ast.parse()解析正常源码再用ast.walk()与ast.iter_fields()把树中所有名为x的标识符替换为True随后调用compile(tree, test, exec)修复前的调试构建会触发解释器崩溃修复后则抛出ValueError: identifier field cant represent True constant修复后的行为该修复让compile()对上述场景稳定抛出ValueError并附带明确的错误信息指明是哪个标识符字段试图表示哪个常量。这一行为对齐了compile()的既有约定对非法输入拒绝编译并报告异常而非让解释器进程崩溃。底层机制AST 验证器validate_name验证器实现修复落地点在 Python/ast.c 的validate_name()函数Python/ast.c。它维护了一张硬编码的禁用名单static const char * const forbidden[] { None, True, False, NULL };只要传入的标识符PyObject *name必须为PyUnicode与名单中任意一项以 ASCII 精确匹配就通过PyErr_Format(PyExc_ValueError, identifier field cant represent %s constant, ...)设置ValueError并返回失败0。从源码结构看这张名单直接对应 Python 语言中三个字面量常量关键字None、True、False——它们不能被用作标识符因而也不能出现在 AST 的任何 identifier 字段中。校验覆盖的 AST 节点validate_name()通过_PyAST_Validate()在整个语法树验证流程中被广泛调用覆盖了文档所述的全部场景标识符字段调用位置Python/ast.c对应语法FunctionDef.namecase FunctionDef_kindPython/ast.cdef x(): ...AsyncFunctionDef.namecase AsyncFunctionDef_kindasync def x(): ...ClassDef.namecase ClassDef_kindPython/ast.cclass x: ...Import.names[i].name/.asnamevalidate_import_names()Python/ast.cimport x、import a as xImportFrom.names[i].name/.asnamevalidate_import_names()from a import x as yExceptHandler.namecase Try_kindPython/ast.cexcept A as x: ...函数/方法参数名validate_args()→validate_name()Python/ast.cdef f(x): ...Name.id表达式case Name_kindPython/ast.c任何命名引用其中validate_import_names()会同时校验alias.name与可选的alias.asname两个字段Try/TryStar分支则校验ExceptHandler.namePython/ast.c。函数参数含位置参数、仅限关键字参数、*args、**kwargs经由validate_args()/validate_arguments()汇总进入同一检查链。此外match语句的模式捕获名如case x:也复用了validate_name()Python/ast.c。调用链compile()与ast.parse()的必经之路无论是用户调用compile()传入 AST 对象还是ast.parse()内部走“解析 → 转 AST → 验证”路径最终都会收敛到同一个验证入口_PyAST_Validate()compile()内置实现位于 Python/bltinmodule.cbuiltin_compile检测到source是 ASTPyAST_Check后先由PyAST_obj2mod()把 Python 层的 AST 对象转换为内部mod_ty紧接着调用_PyAST_Validate(mod)进行完整性校验Python/bltinmodule.c校验失败即释放 arena 并转入错误分支字节码编译路径_PyAST_Compile()的入口同样在转换后立即执行同一校验Python/compile.cast.parse()返回前也会经过_PyAST_ValidatePython/bltinmodule.c。_PyAST_Validate的声明位于 Include/internal/pycore_ast.h是整个 AST 完整性检查的对外接口。这意味着任何经compile()或ast.parse()进入编译管线的 AST都无法绕过这层 identifier 字段校验。回归测试覆盖场景与边界该修复配有完整的回归测试位于 Lib/test/test_ast/test_ast.py单点场景Name节点test_ast.pydef test_constant_as_name(self): for constant in True, False, None: expr ast.Expression(ast.Name(constant, ast.Load())) ast.fix_missing_locations(expr) with self.assertRaisesRegex(ValueError, fidentifier field cant represent {constant} constant): compile(expr, test, eval)注意这里ast.Name(constant, ast.Load())被放进ast.Expression后手工构造的节点缺少行号/列号必须先调用ast.fix_missing_locations()补齐位置信息compile()才会走到验证阶段。全字段覆盖替换语句中的每个标识符test_ast.py测试枚举了文档提到的全部语句形态并对其余每个 identifier 字段做替换for statement in [ def x(): pass, async def x(): pass, class x: pass, from a import x, from a import b as x, from a import b, c, d as x, import x, import a, b, x, try: pass\nexcept A as x: pass, try: pass\nexcept A as b: pass\nexcept B as x: pass\n, ]: for constant in True, False, None: with self.subTest(statementstatement, constantconstant): tree ast.parse(statement) for node in ast.walk(tree): for field, value in ast.iter_fields(node): if value x: setattr(node, field, constant) with self.assertRaisesRegex( ValueError, fidentifier field cant represent f{constant} constant): compile(tree, test, exec)这份测试印证了文档中“函数名、类名、导入模块名、捕获异常”四类场景且将None、True、False三个常量与 10 种语句形态做笛卡尔积覆盖并通过subTest精确定位失败用例。词法伪装Unicode 同形攻击test_ast.py测试还验证了通过字节流构造的“视觉同形”标识符也会被拦截constants [ (True, bTru\xe1\xb5\x89), (False, bFal\xc5\xbfe), (None, bN\xc2\xbane), ] for constant in constants: with self.assertRaisesRegex(ValueError, fidentifier field cant represent {constant[0]} constant): ast.parse(constant[1], modeeval)这些字节序列使用上标/修饰 Unicode 字符伪装成True、False、None会被标识符解析器还原并拒绝。需要注意的是validate_name()使用_PyUnicode_EqualToASCIIString()做精确的 ASCII 比较因此只有语义上完全等价的常量名才会被拒绝普通合法的标识符不受影响。实战影响与规避建议对ast使用者的影响该修复改变了手工构造 AST 并交给compile()的边界行为。任何以编程方式生成/改写 AST 的工具代码生成器、重构工具、静态分析插件都应把ValueError纳入正常错误处理而不是依赖“编译必然成功”。触发条件速查以下操作组合会触发ValueError: identifier field cant represent constant constant手工构造 AST且任一 identifier 字段Name.id、FunctionDef.name、ClassDef.name、alias.name/asname、ExceptHandler.name、函数参数名、match 模式捕获名等于None/True/False对ast.parse()得到的树做字段替换把合法名字换成上述常量名将可解码为上述常量名的 Unicode 伪同形字节串作为标识符源码解析。规避方案保留字检查前置生成 AST 前先对候选标识符与keyword.kwlist及{None, True, False}做比对从源头避免非法标识符错误捕获调用compile()时用try/except ValueError包裹将校验失败转化为可控的错误信息使用ast.fix_missing_locations()手工构造的节点必须补齐行号/列号否则可能在验证前就因为其他错误如 Python/ast.c 的位置范围校验VALIDATE_POSITIONS失败模式匹配场景case _:这类通配捕获不受影响但命名捕获不能使用常量名相关校验在 Python/ast.c。小结gh-85260 是一个典型的“崩溃 → 异常”健壮性修复通过 Python/ast.c 中validate_name()对 identifier 字段的硬编码名单校验配合_PyAST_Validate()在 Python/bltinmodule.c 与 Python/compile.c 两处编译入口的强制调用CPython 保证任何试图以None/True/False作为标识符的 AST 都会得到明确的ValueError而非让解释器在调试构建下崩溃。对 AST 工具链开发者而言理解这条校验链不仅能规避崩溃风险也是构建健壮代码生成器的基础。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考