- 静态分析
- 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.9.0 版本的官方发布说明(cpp/ql/src/change-notes/released/0.9.0.md)为核心,深度剖析该版本的两项关键变更:删除重复查询cpp/tainted-format-string-through-global,以及新增查询cpp/use-of-string-after-lifetime-ends。读者将理解这两项变更背后的设计动机,掌握新查询的检测原理、适用场景与修复方法,并学会通过仓库内的查询实现与测试用例验证结论。
版本概览:一次聚焦的增量更新
0.9.0 是 CodeQL C++ 查询集的增量发布,改动集中在两点:
- 破坏性变更(Breaking Changes):删除查询
cpp/tainted-format-string-through-global; - 新增查询(New Queries):
cpp/use-of-string-after-lifetime-ends,用于检测对即将销毁的字符串调用c_str的悬垂指针问题。
两项变更一"删"一"增",前者是查询集的收敛与去重,后者是安全能力的扩展,共同体现了 CodeQL 查询维护中"减少噪音、提升精度"的一贯取向。
破坏性变更:移除cpp/tainted-format-string-through-global
变更内容
0.9.0 版本删除了一条长期存在的查询:cpp/tainted-format-string-through-global。发布说明明确给出了删除理由:该查询检测到的告警集合是cpp/tainted-format-string查询结果的一个子集,属于完全重复,删除不会造成任何相关告警丢失。
也就是说,两条查询在功能上存在包含关系——凡是通过全局变量传播污点触发的格式化字符串问题,cpp/tainted-format-string本身已经能够发现,专门的"through-global"变体只是增加了维护成本,却没有带来额外的检测价值。
源码佐证:主查询仍然在岗
删除"through-global"变体后,主查询仍保留在仓库中:
- cpp/ql/src/PointsTo/TaintedFormatStrings.ql:基于指针分析(PointsTo)实现污点流分析的格式化字符串查询;
- cpp/ql/src/Security/CWE/CWE-134/UncontrolledFormatString.ql:对应 CWE-134(使用外部控制的格式字符串)的安全查询。
查询 IDcpp/tainted-format-string继续生效,与格式化字符串相关的告警覆盖范围不受影响。在后续版本中,cpp/ql/src/CHANGELOG.md 也持续记录了该系列查询的演化,读者可沿此路径追溯格式化字符串检测的完整历史。
对使用者的影响
- 升级到 0.9.0 后,
cpp/tainted-format-string-through-global这一查询 ID 将不再可用,引用它的自定义查询套件(QLS 文件)需要同步清理; - 无需担心告警回归:被删除查询能发现的全部问题,均已在
cpp/tainted-format-string的结果中体现; - 若你的工作流中曾按查询 ID 精确筛选结果,请改用
cpp/tainted-format-string作为过滤条件。
新增查询:cpp/use-of-string-after-lifetime-ends
问题本质:临时 std::string 与 c_str 的悬垂指针
对std::string对象调用c_str()会返回指向其底层字符数组的指针;当该std::string对象被销毁时,这个指针立即失效。若指针在对象销毁后仍被使用,则属于未定义行为(undefined behavior)。
典型触发场景是:函数调用(或重载运算符)按值返回一个std::string,而返回结果没有被及时保存到一个能够延长临时对象生命周期的变量中。根据 C++ 语言规则,这类临时std::string对象在包含它的完整表达式语句结束时即被销毁,连同c_str()返回的内存一并失效。
最典型的坏味道写法如下(取自查询官方示例 UseOfStringAfterLifetimeEndsBad.cpp):
#include <string> void work(const char*); // BAD: the concatenated string is deallocated when `c_str` returns. So `work` // is given a pointer to invalid memory. void work_with_combined_string_bad(std::string s1, std::string s2) { const char* combined_string = (s1 + s2).c_str(); work(combined_string); }这里(s1 + s2)产生一个临时std::string,对其调用c_str()拿到的指针在表达式结束后即悬空,work(combined_string)实际读取的是已释放内存。
查询实现原理
新查询的完整实现位于 cpp/ql/src/Security/CWE/CWE-416/UseOfStringAfterLifetimeEnds.ql,其元数据声明了关键属性:
| 元数据项 | 值 | 含义 |
|---|---|---|
@name | Use of string after lifetime ends | 查询显示名称 |
@kind | problem | 告警类查询 |
@precision | high | 高精度,低误报 |
@id | cpp/use-of-string-after-lifetime-ends | 稳定查询 ID |
@problem.severity | warning | 告警级别为警告 |
@security-severity | 8.8 | 高安全严重度 |
@tags | reliability / security / cwe-416 / cwe-664 | 关联 CWE-416(释放后使用)与 CWE-664(生命周期管理不当) |
核心检测逻辑只有短短几行,思路非常清晰:
import cpp import semmle.code.cpp.models.implementations.StdString import Temporaries from Call c where outlivesFullExpr(c) and not c.isFromUninstantiatedTemplate(_) and (c.getTarget() instanceof StdStringCStr or c.getTarget() instanceof StdStringData) and isTemporary(c.getQualifier().getFullyConverted()) select c, "The underlying temporary string object is destroyed after the call to '" + c.getTarget() + "' returns."逐条解读其检测条件:
outlivesFullExpr(c):从 Temporaries 模块导入,判定该调用的结果"存活"超过了完整表达式的边界——即指针被保存下来,而非在表达式内立即消费掉。这是区分"危险"与"安全"的关键:若指针只在本表达式内使用(如call(std::string("hello").c_str())),临时对象销毁前指针已被消费,没有问题。not c.isFromUninstantiatedTemplate(_):排除来自未实例化模板的调用,避免在模板定义阶段产生冗余告警,保证查询在真实实例化上下文中的精度。c.getTarget() instanceof StdStringCStr or ... StdStringData:通过 StdString 模型识别目标函数为std::string的c_str()或data()——两个函数都会返回指向底层缓冲的指针,性质完全相同。isTemporary(c.getQualifier().getFullyConverted()):检查调用者(即被调用c_str/data的对象)经过完整类型转换后是一个临时对象。非临时对象(如命名变量、延长了生命周期的引用绑定)不满足此条件,因而不会被误报。
从源码结构看,该查询是"临时对象生命周期"分析(Temporaries模块)与标准库字符串模型(StdString模块)的组合应用:前者回答"指针是否逃逸出完整表达式",后者回答"调用是否作用于临时字符串"。两者缺一不可。
官方推荐修复方式
查询官方文档 UseOfStringAfterLifetimeEnds.qhelp 给出的建议非常直接:确保c_str()返回的指针不会活得比底层std::string对象更久。
修复示例见 UseOfStringAfterLifetimeEndsGood.cpp:先把拼接结果存进一个命名变量以延长生命周期,再在需要时调用c_str():
#include <string> void work(const char*); // GOOD: the concatenated string outlives the call to `work`. So the pointer // obtainted from `c_str` is valid. void work_with_combined_string_good(std::string s1, std::string s2) { auto combined_string = s1 + s2; work(combined_string.c_str()); }由于combined_string是具名局部变量,其生命周期覆盖整个函数体,c_str()返回的指针在work调用期间始终有效。
测试用例:正反例全覆盖
仓库为查询配备了完整的回归测试,位于 cpp/ql/test/query-tests/Security/CWE/CWE-416/semmle/tests/UseOfStringAfterLifetimeEnds/:
- 测试驱动文件:test.cpp,通过内联注释
$ Alert标注每个用例的预期结果; - 期望输出:UseOfStringAfterLifetimeEnds.expected,记录 13 处告警的精确位置与消息文本;
- 测试引用:UseOfStringAfterLifetimeEnds.qlref,将测试目录绑定到目标查询。
测试用例刻意覆盖了c_str()与data()两种 API、直接赋值与条件表达式(?:)两种传播路径、以及多种"逃逸"方式,形成了一张完整的正反例矩阵:
被判为 BAD 的场景(产生告警):
| 场景 | 说明 |
|---|---|
auto s1 = std::string("hello").c_str(); | 指针存入变量,逃出完整表达式 |
b1 ? std::string("hello").c_str() : "" | 条件表达式结果存入变量 |
s4 = std::string("hello").c_str(); | 赋值给既有指针变量 |
v1.push_back(std::string("hello").c_str()); | 指针存入容器 |
v2.push_back({ std::string("hello").c_str() }); | 结构体聚合初始化后存入容器 |
S s5[] = { { std::string("hello").c_str() } }; | 数组元素持有悬垂指针 |
return std::string("hello").c_str(); | 函数返回悬垂指针 |
std::string("hello").data()系列 | data()与c_str()同等对待 |
被判为 GOOD 的场景(不产生告警):
| 场景 | 说明 |
|---|---|
call(std::string("hello").c_str()); | 指针在完整表达式内立即被消费 |
call_by_value({ std::string("hello").c_str() }); | 按值传入,消费即时 |
char c = std::string("hello").c_str()[0]; | 立即解引用读取字符 |
s.c_str()(s为具名变量) | 底层对象生命周期覆盖指针使用 |
sRef.c_str()(引用绑定) | 引用未延长临时生命周期场景之外的安全路径 |
std::string&& sRefRef = std::string("hello");后调用 | 右值引用绑定了临时对象,生命周期延长 |
这个测试矩阵直观地展示了查询的判定边界:问题不在于"对临时字符串调用 c_str",而在于"c_str 的返回值逃逸了临时对象的生命周期"。这也解释了为什么查询被标记为@precision high——通过精确的生命周期建模而非简单的语法模式匹配,在保持高检出率的同时将误报压到最低。
集成状态:进入默认安全查询套件
新查询并非孤立存在,它已被纳入 CodeQL 的默认查询套件。在集成测试的套件期望文件中可以看到其身影,例如:
- cpp/ql/integration-tests/query-suite/cpp-code-scanning.qls.expected(code scanning 默认套件)
- cpp/ql/integration-tests/query-suite/cpp-security-extended.qls.expected(安全扩展套件)
- cpp/ql/integration-tests/query-suite/cpp-security-and-quality.qls.expected(安全与质量套件)
这意味着用户在 GitHub Advanced Security 的 code scanning 或codeql database analyze运行标准 C++ 套件时,无需额外配置即可获得该查询的告警。@security-severity 8.8的高严重度评级,也说明维护者将其视为值得优先修复的可靠性/安全问题。
小结
CodeQL C++ 查询集 0.9.0 通过一删一增完成了能力优化:
- 删除
cpp/tainted-format-string-through-global是查询集的去重瘦身,告警覆盖不变,维护成本降低; - 新增
cpp/use-of-string-after-lifetime-ends将"临时 std::string 生命周期"这一隐蔽的未定义行为纳入默认检测范围,其实现融合了临时对象生命周期分析与标准库字符串模型,并配有覆盖 13 处告警、数十种正反例的回归测试,精度定位为 high。
对于 C++ 开发者而言,理解这条新查询的价值在于建立一条直觉:任何对临时字符串对象调用c_str()/data()并保存指针的写法,都应当改为先具名保存字符串对象再取指针。这正是 CWE-416(释放后使用) 与 CWE-664(生命周期管理不当)在标准库 API 层面的典型体现。后续版本(如 1.6.0、1.4.4)的变更记录中仍可看到相关查询的持续演进,读者可以沿 CHANGELOG 追踪其后续变化。
- 静态分析
- SAST
- 应用安全
- 漏洞扫描
- 代码质量
【免费下载链接】codeql
CodeQL: the libraries and queries that power security researchers around the world, as well as code scanning in GitHub Advanced Security
相关推荐
CodeQL 1.25 C/C++ 分析变更详解:污点追踪库升级、嵌套字段流与默认启用的格式字符串查询
CodeQL 1.25 C/C++ 分析变更详解:污点追踪库升级、嵌套字段流与默认启用的格式字符串查询 本文基于 CodeQL 仓库中 change notes
静态分析SAST应用安全漏洞扫描代码质量Rust FFI 传递字符串(Passing Strings)惯用法:CString 生命周期、unsafe 最小化与悬垂指针陷阱
Rust FFI 传递字符串(Passing Strings)惯用法:CString 生命周期、unsafe 最小化与悬垂指针陷阱 导读 在 Rust 与 C
文档教程gh_mirrors/im/im_service API参考:开发者必备的接口文档大全
gh_mirrors/im/im_service API参考:开发者必备的接口文档大全 gh_mirrors/im/im_service 是一个基于 Golan
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考