- 静态分析
- SAST
- 应用安全
- 漏洞扫描
- 代码质量
【免费下载链接】codeql
CodeQL: the libraries and queries that power security researchers around the world, as well as code scanning in GitHub Advanced Security
导读
本文以 CodeQL 仓库中 C++ 查询包发布的 0.8.3 版本变更记录(cpp/ql/src/change-notes/released/0.8.3.md)为核心,深入讲解cpp/uninitialized-local(“Potentially uninitialized local variable”/CWE-457、CWE-665)这一查询在持续降低误报(false positive)方面的演进历程。读完本文,你将理解该查询基于 IR 数据流(MustFlow)的检测原理、源码中列出的全部误报豁免规则、测试用例如何验证行为,以及如何在本地运行和验证这条查询。
一、0.8.3 变更记录:一句话背后的技术含义
0.8.3 版本的变更记录非常简洁,属于Minor Analysis Improvements(次要分析改进)类别:
The
cpp/uninitialized-localquery has been improved to produce fewer false positives.
这句话翻译过来即:cpp/uninitialized-local查询经改进后产生的误报更少。在同仓库的 cpp/ql/src/CHANGELOG.md 中,0.8.3 段落(第 468–472 行)记录了完全一致的说明。
虽然 0.8.3 的变更记录本身没有逐条列出“具体修掉了哪几类误报”,但结合仓库中的查询源码、配套文档、测试用例以及前后多个版本的变更记录,可以完整还原这次“减少误报”改进所处的技术脉络——它是该查询在 0.7.1、0.8.3、0.9.4、0.9.9、1.2.5 等多个版本中持续“去伪存真”的一环。
二、认识 cpp/uninitialized-local 查询
2.1 查询元数据
查询主体位于 cpp/ql/src/Likely Bugs/Memory Management/UninitializedLocal.ql,其头部元数据定义了这条查询的公开身份:
| 元数据项 | 值 |
|---|---|
@name | Potentially uninitialized local variable |
@description | Reading from a local variable that has not been assigned to will typically yield garbage. |
@kind | path-problem(路径问题,可展示 source→sink 数据流路径) |
@id | cpp/uninitialized-local |
@problem.severity | warning |
@security-severity | 7.8 |
@precision | medium |
@tags | security、external/cwe/cwe-665(不正确的资源释放/初始化)、external/cwe/cwe-457(未初始化变量的使用) |
配套的查询帮助文档 UninitializedLocal.qhelp 给出了检测原理的定性说明:非 class 类型的局部非静态变量在初始化之前取值是未定义的——例如,依赖一个未初始化的整数等于 0 是错误的。
2.2 为什么未初始化变量值得关注
在 C/C++ 中,未初始化局部变量读取到的是栈上的残留垃圾值,其行为由 ISO/IEC 9899:2011 第 6.3.2.1 节规定为未定义。这类缺陷可能造成信息泄露、逻辑错误乃至可利用的内存安全问题,这也是查询被标记为 CWE-457(Use of Uninitialized Variable)与 CWE-665(Improper Initialization)且安全严重度达 7.8 的原因。
三、查询实现原理:基于 IR 的 MustFlow 数据流分析
从源码结构看,cpp/uninitialized-local是一张**路径问题(path-problem)**查询,其检测机制建立在 CodeQL 的 IR(Intermediate Representation,中间表示)与MustFlow数据流库之上。核心导入语句为:
import cpp import semmle.code.cpp.ir.IR import semmle.code.cpp.ir.dataflow.MustFlow import UninitializedLocal::PathGraph3.1 数据流配置:source、sink 与 barrier
查询通过实现MustFlow::ConfigSig来定义整个分析图(见 UninitializedLocal.ql):
module UninitializedLocalConfig implements MustFlow::ConfigSig { predicate isSource(Instruction source) { source instanceof UninitializedInstruction and exists(Type t | t = source.getResultType() | not allocatedType(t)) } predicate isSink(Operand sink) { isSinkImpl(sink.getDef(), _) } predicate allowInterproceduralFlow() { none() } predicate isBarrier(Instruction instr) { instr instanceof ChiInstruction } }关键设计点:
- Source(数据流起点):任何
UninitializedInstruction(未初始化指令),且其结果类型不是“无需初始化”的分配类型(见 3.2)。 - Sink(数据流终点):对变量的读取操作(
LoadInstruction),且满足isSinkImpl中的一系列豁免条件(见 3.3)。 - allowInterproceduralFlow 返回 none():查询刻意不启用跨函数数据流,仅做函数内部分析,这一约束直接限制了分析规模,也避免了跨函数路径带来的误报。
- Barrier(阻断点):
ChiInstruction(χ 指令,IR 中表示条件性合并/可能写入的节点)被作为数据流屏障。换言之,当存在可能对变量写入的 χ 节点时,未初始化状态不再向后传播,从而避免“部分路径已初始化”的场景被误报。
3.2 allocatedType:哪些类型天生“无需初始化”
辅助谓词allocatedType用于判定:栈上分配的数组、结构体(class)等类型,成员/元素各自赋值即视为完成初始化,因此整个变量不构成未初始化 source(UninitializedLocal.ql):
predicate allocatedType(Type t) { t instanceof ArrayType // "int foo[1]; foo[0] = 42;" 是合法的 or t instanceof Class // "struct foo bar; bar.baz = 42" 是合法的 or allocatedType(t.(TypedefType).getUnderlyingType()) // typedef 递归展开 or allocatedType(t.getUnspecifiedType()) // 类型说明符不影响分配属性 }这条规则对误报控制至关重要:例如int buf[4]; buf[0] = 1;中buf本身不需要“整体初始化”,逐个元素赋值即可,查询不会对这类数组/结构体变量报未初始化。
3.3 commonException:查询内置的误报豁免清单
真正体现“减少误报”设计的是commonException谓词(UninitializedLocal.ql)。它列出了四类被显式豁免的场景,源码注释直接说明了每一条的动机:
VariableAccess commonException() { // 1. 宏展开中的未初始化使用(如 va_start),通常不应告警 result.getParent().isInMacroExpansion() or // 2. 内建操作(BuiltInOperation)内的使用 result.getParent() instanceof BuiltInOperation or // 3. 显式转型为 void 且作为表达式语句的使用(丢弃值) result.getActualType() instanceof VoidType and result.getParent() instanceof ExprStmt or // 4. 含内联汇编的函数,无法可靠推断其行为 containsInlineAssembly(result.getEnclosingFunction()) or // 5. 作为静态成员函数调用限定符(qualified)的使用 exists(Call c | c.getQualifier() = result | c.getTarget().isStatic()) }对应的isSinkImpl会要求not va = commonException()且not va.getTarget().(LocalVariable).getFunction().hasErrors()(即目标函数无提取错误),只有满足这些条件的使用点才被当作 sink(UninitializedLocal.ql)。查询最终输出消息为:
The variable $@ may not be initialized at this access.
四、0.8.3 在误报治理时间线中的位置
将 0.8.3 与相邻版本的变更记录并置,可以看到“减少误报”是一个持续迭代的过程。以下内容全部来自仓库中各版本变更记录原文:
| 版本 | 变更记录文件 | 针对 cpp/uninitialized-local 的误报治理内容 |
|---|---|---|
| 0.7.1 | 0.7.1.md | 排除“显式转型为 void 且作为表达式语句”的未初始化使用,减少误报 |
| 0.8.3 | 0.8.3.md | 查询经改进,产生更少的误报 |
| 0.9.4 | 0.9.4.md | 变量作为静态成员函数调用的限定符时不再告警 |
| 0.9.9 | 0.9.9.md | 查询被转换为path-problem查询(可展示完整未初始化路径) |
| 1.2.5 | 1.2.5.md | 当函数存在提取错误(extraction errors)时修复误报 |
可以推断,0.8.3 的改进落在“排除误报类使用场景”这条主线上,与 0.7.1、0.9.4 的改动同属一类;同时 0.9.9 将查询升级为 path-problem 后,0.8.3 之后的分析结果具备了“从定义到读取”的可视化数据流路径,配合 1.2.5 对提取错误场景的兜底,构成了该查询“低误报 + 可解释路径”的完整能力。
五、测试用例如何验证“误报更少”
5.1 测试布局
该查询的测试位于 CWE-457 目录下:
- 查询引用:UninitializedLocal.qlref(指向
Likely Bugs/Memory Management/UninitializedLocal.ql,并使用utils/test/InlineExpectationsTestQuery.ql做行内期望校验) - 测试用例:test.cpp(覆盖十余个正反场景)
- 边界用例:errors.cpp
- 期望输出:UninitializedLocal.expected
5.2 关键正反用例(摘自 test.cpp)
| 场景 | 代码形态 | 期望 |
|---|---|---|
| 已初始化后使用 | int foo = 1; use(foo);(test1) | GOOD,不告警 |
| 直接未初始化使用 | int foo; use(foo);(test2) | Alert(BAD) |
| 全分支均赋值 | if/else 两分支都赋值(test3) | GOOD |
| 存在未赋值路径 | 仅 if 分支赋值,else 缺失(test4) | 代码中有// $ MISSING: Alert注释,标记为未检出 |
| 循环内赋值(常量次数) | for (int i = 0; i < 10; i++) foo = i;(test5) | GOOD |
| 循环内赋值(运行期次数) | for (int i = 0; i < count; i++) foo = i;(test5 重载) | 标注// $ MISSING: Alert,未检出 |
| 守卫使用 | 先if (b) foo = 42;再if (b) use(foo);(test6) | GOOD |
| flag 守卫使用 | 用bool set标记是否已赋值(test7) | GOOD |
| extern / static 变量 | extern int i; use(i);(test10);static int i; use(i);(test11) | GOOD(非自动存储期,不适用未初始化规则) |
| 取地址不算初始化 | int foo; &foo; use(foo);(test13) | Alert(BAD)——对变量取地址并不会初始化它 |
5.3 期望输出印证 path-problem 形态
UninitializedLocal.expected 采用 path-problem 查询的标准输出结构(edges、nodes、#select),例如对test.cpp第 11–12 行:
| test.cpp:12:6:12:8 | foo | test.cpp:11:6:11:8 | definition of foo | ... | The variable $@ may not be initialized at this access. | foo | foo |即:sink 为第 12 行的读取foo,source 为第 11 行的definition of foo,两者之间构成一条未初始化路径。errors.cpp:13-14的用例同样被报告。这一输出格式正是在 0.9.9 版本将查询转为 path-problem 后形成的,它让开发者在收到告警时能直接看到“变量在哪定义、在哪读取”的完整证据链。
六、实战示例:absWrong / absCorrect
查询配套示例 UninitializedLocal.cpp 直观展示了“何时该告警、如何修复”:
int absWrong(int i) { int j; if (i > 0) { j = i; } else if (i < 0) { j = -i; } return j; // wrong: j may not be initialized before use } int absCorrect1(int i) { int j = 0; // 修复方式一:声明时赋初值 if (i > 0) { j = i; } else if (i < 0) { j = -i; } return j; } int absCorrect2(int i) { int j; if (i > 0) { j = i; } else if (i < 0) { j = -i; } else { j = 0; } // 修复方式二:补全所有分支的赋值 return j; }absWrong中当i == 0时j从未被赋值却被return,正是查询要捕获的未初始化读取;absCorrect1通过初始化器、absCorrect2通过补全 else 分支消除问题,这也与 UninitializedLocal.qhelp 的建议(“为变量补充初始化器,或检查是否某条路径缺少赋值”)一一对应。
七、如何在本地运行与验证这条查询
仓库是只读的,以下操作仅涉及查看与运行,不修改仓库内容:
- 阅读源码:查询主体 UninitializedLocal.ql、帮助文档 UninitializedLocal.qhelp、示例 UninitializedLocal.cpp。
- 理解版本脉络:对照 CHANGELOG.md 与 change-notes/released 目录下各版本记录,观察误报治理的持续演进。
- 运行查询(需本地安装 CodeQL CLI):对目标 C/C++ 项目先创建数据库,再执行查询并输出路径信息:
codeql database create <db> --language=cpp --source-root=<src> codeql query run cpp/ql/src/Likely Bugs/Memory Management/UninitializedLocal.ql \ --database=<db> --output=uninit.bqrs codeql bqrs decode --format=csv uninit.bqrs - 运行测试套件:在
cpp/ql目录下通过 CodeQL 测试框架执行qltest,即可复现 UninitializedLocal.expected 中的期望结果,验证不同版本行为差异。
结语
0.8.3 版本记录中“更少的误报”这一句话,背后是cpp/uninitialized-local查询一整套精密的豁免机制:allocatedType让数组/结构体免于误报、commonException排除宏展开/内建操作/void 转型/内联汇编/静态成员限定符五类场景、ChiInstruction屏障避免部分初始化路径被误判,再加上不启用跨函数流、跳过含提取错误的函数等保守策略。结合 UninitializedLocal.ql 源码与 CWE-457 测试目录 的用例,读者可以完整复现并验证这条安全查询的每一次误报治理改进。
- 静态分析
- SAST
- 应用安全
- 漏洞扫描
- 代码质量
【免费下载链接】codeql
CodeQL: the libraries and queries that power security researchers around the world, as well as code scanning in GitHub Advanced Security
相关推荐
5 步部署 AzerothCore:魔兽世界私服从零跑起来
5 步部署 AzerothCore:魔兽世界私服从零跑起来 一个周末,把一台 3.3.5 的魔兽世界私服拉起来:不用买服务器、不用手搓环境,AzerothCor
静态分析SAST应用安全漏洞扫描代码质量Foundry 静态分析实战:理解并修复 uninitialized-local(未初始化局部变量)Lint 规则
Foundry 静态分析实战:理解并修复 uninitialized local(未初始化局部变量)Lint 规则 导读 uninitialized local
区块链开发工具CodeQL C/C++ 查询包 0.0.8 发布解析:新查询、高精度化与误报治理
CodeQL C/C++ 查询包 0.0.8 发布解析:新查询、高精度化与误报治理 导读 本文基于 CodeQL 仓库中 C/C++ 查询包 0.0.8 版本的
静态分析SAST应用安全漏洞扫描代码质量
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考