☰
CodeQL C++ 新增 cpp/overrun-write 查询:基于 ProductFlow 的缓冲区越界写入检测
2026/10/6 12:23:49 网站建设 项目流程
  • 静态分析
  • SAST
  • 应用安全
  • 漏洞扫描
  • 代码质量

【免费下载链接】codeql

CodeQL: the libraries and queries that power security researchers around the world, as well as code scanning in GitHub Advanced Security

项目地址:https://gitcode.com/gh_mirrors/co/codeql
点击查看免费下载

导读

本文围绕 CodeQL 仓库中 C++ 查询包 0.6.3 版本发布说明(cpp/ql/src/change-notes/released/0.6.3.md)引入的新查询cpp/overrun-write展开,深入解析它如何检测 C 风格缓冲区操作函数中的缓冲区溢出(buffer overflow)。读完本文,你将掌握该查询的触发原理、与cpp/overrunning-write系列查询的分工差异、底层依赖的 ProductFlow 与范围分析(Range Analysis)实现,以及如何结合仓库内的示例与测试用例验证查询行为。

一、发布说明中的新查询

在 cpp/ql/src/change-notes/released/0.6.3.md 中,0.6.3 版本的发布说明只有一条核心变更:

Added a new query,cpp/overrun-write, to detect buffer overflows in C-style functions that manipulate buffers.

即:新增了cpp/overrun-write查询,用于检测“在操作缓冲区的 C 风格函数”中发生的缓冲区溢出。与之配套的正式 CHANGELOG 记录见 cpp/ql/src/CHANGELOG.md。

这里有两个值得关注的关键词:

  • C 风格函数(C-style functions):指strcpy、strncpy、memset、sprintf、memcpy等以裸指针/字符数组方式操作缓冲区的 C 库函数,而非 C++ 的std::vector、std::string等自带长度管理的容器。
  • 缓冲区溢出(buffer overflow):写入或访问超出分配范围之外的内存,轻则导致段错误(segmentation fault),重则成为可利用的安全漏洞。

二、查询定位:cpp/overrun-write是什么

2.1 查询元数据

该查询的实现文件为 cpp/ql/src/Security/CWE/CWE-119/OverrunWriteProductFlow.ql,其头部元数据完整定义了查询的身份信息:

元数据项值含义
@nameOverrunning write查询名称
@descriptionExceeding the size of a static array during write or access operations may result in a buffer overflow问题描述
@kindpath-problem输出带数据流路径的问题
@problem.severityerror问题严重级别
@security-severity9.3安全严重级别(CVSS 风格评分)
@precisionmedium检出精度(中等,意味着存在一定误报/漏报空间)
@idcpp/overrun-write查询唯一标识
@tagsreliability、security、external/cwe/cwe-119、external/cwe/cwe-131关联 CWE-119(缓冲区操作中的内存破坏)与 CWE-131(缓冲区大小计算错误)

与常见的problem类查询不同,cpp/overrun-write是path-problem查询:它不仅报告“哪里溢出”,还会呈现从分配源头(source)到写入点(sink)的完整数据流路径,便于开发者定位根因。

2.2 查询套件归属

从集成测试的套件清单(cpp/ql/integration-tests/query-suite/cpp-security-and-quality.qls.expected 与 cpp/ql/integration-tests/query-suite/cpp-security-extended.qls.expected)可以看到,OverrunWriteProductFlow.ql同时被cpp-security-and-quality与cpp-security-extended两个套件收录,即它默认参与 C/C++ 代码扫描的安全质量检查。

三、工作原理:从源码看检测机制

3.1 核心依赖

OverrunWriteProductFlow.ql顶部导入了以下关键库:

import cpp import semmle.code.cpp.ir.dataflow.internal.ProductFlow import semmle.code.cpp.ir.IR import semmle.code.cpp.models.interfaces.Allocation import semmle.code.cpp.models.interfaces.ArrayFunction import semmle.code.cpp.rangeanalysis.new.internal.semantic.analysis.RangeAnalysis import semmle.code.cpp.rangeanalysis.new.internal.semantic.SemanticExprSpecific import semmle.code.cpp.security.ProductFlowUtils.ProductFlowUtils import semmle.code.cpp.rangeanalysis.new.RangeAnalysisUtil import StringSizeFlow::PathGraph1 import codeql.util.Unit

这些依赖揭示了查询的三层机理:

  1. IR 层:基于 CodeQL 的中间表示(IR)进行分析,CallInstruction、LoadInstruction等指令级元素是分析的原子单位。
  2. ProductFlow 库:这是一个“双投影”数据流框架,同时追踪缓冲区指针(第一投影)与缓冲区大小(第二投影)两条独立的数据流,并将二者关联成“缓冲区 + 大小”的配对约束。
  3. Range Analysis(范围分析):新式范围分析(semmle.code.cpp.rangeanalysis.new)用于推导分配大小的数值上下界,判断写入长度是否会越界。

3.2 源点:提取分配大小表达式

查询通过hasSize谓词从分配表达式中提取“大小变量”与“偏移量 delta”:

predicate hasSize(HeuristicAllocationExpr alloc, DataFlow::Node n, int state) { exists(VariableAccess va, Expr size, int delta | size = alloc.getSizeExpr() and // Get the unique variable in a size expression like `x` in `malloc(x + 1)`. va = unique( | | getAVariableAccess(size)) and // Compute `delta` as the constant difference between `x` and `x + 1`. bounded(any(Instruction instr | instr.getUnconvertedResultExpression() = size), any(LoadInstruction load | load.getUnconvertedResultExpression() = va), delta) and n.asExpr() = va and state = delta ) }

例如对malloc(x + 1),查询提取出变量x,并计算出常量偏移delta = 1,作为数据流的状态值(state)在后续传播中携带。这保证了malloc(size + 1)这类“多分配一个元素”的常见防御性写法不会被误判为越界。

3.3 汇点:识别带大小参数的缓冲区函数

isSinkPairImpl0谓词通过ArrayFunction模型识别“缓冲区 + 大小”成对出现的调用:

predicate isSinkPairImpl0( CallInstruction c, DataFlow::Node bufSink, DataFlow::Node sizeSink, int delta, Expr eBuf, Instruction sizeBound, Instruction sizeInstr ) { exists(int bufIndex, int sizeIndex, Instruction bufInstr, ArrayFunction func | bufInstr = bufSink.asInstruction() and c.getArgument(bufIndex) = bufInstr and sizeBound = sizeSink.asInstruction() and c.getArgument(sizeIndex) = sizeInstr and c.getStaticCallTarget() = func and pragmaonly_bind_into .hasArrayWithVariableSize(pragmaonly_bind_into, pragmaonly_bind_into) and bounded(sizeInstr, sizeBound, delta) and eBuf = bufInstr.getUnconvertedResultExpression() ) }

ArrayFunction是 CodeQL 中为 C 库缓冲区操作函数建立的模型接口(定义于 cpp/ql/lib 下的models/interfaces/ArrayFunction.qll),它声明了哪些参数是缓冲区、哪些参数是其大小。strncpy(dst, src, n)中dst与n、memset(p, c, n)中p与n均属于这类配对。

3.4 状态传播与越界判定

查询的核心在于StringSizeConfig这一ProductFlow::StateConfigSig实现。它把“大小”维度设计为一个整数状态FlowState2:

  • 源端:hasSize(bufSource.asExpr(), sizeSource, state2)记录分配时的偏移量。
  • 汇点:isSinkPair中比较delta > state2——即调用处实际传入的大小参数超出分配时携带的大小状态偏移时,判定越界。
  • 屏障:SizeBarrier与isBarrierOut2用于阻断回边(back edge),避免循环导致的无限数据流与误报。

最终的getOverflow谓词计算“越过缓冲区边界访问的元素个数”,并输出形如以下的消息:

This write may overflow$@by N elements.

从select子句看,查询以path-problem形式报告CallInstruction对应的表达式、双源点、双汇点及越界元素数,完整呈现从malloc到strncpy/memset的整条路径。

四、典型漏洞示例

仓库为查询配套了示例文件 cpp/ql/src/Security/CWE/CWE-119/OverrunWriteProductFlow.cpp:

int f(char * s, unsigned size) { char* buf = (char*)malloc(size); strncpy(buf, s, size + 1); // wrong: copy may exceed size of buf for (int i = 0; i <= size; i++) { // wrong: upper limit that is higher than size of buf cout << buf[i]; } }

两处问题一目了然:

  1. strncpy(buf, s, size + 1):buf只分配了size字节,却可能写入size + 1字节,越界 1 字节。
  2. for (int i = 0; i <= size; i++)循环:访问buf[0..size]共size + 1个元素,下标i == size时越界。

官方 qhelp 文档(OverrunWriteProductFlow.qhelp)给出的修复建议是:检查高亮操作中使用的偏移量与大小,确保不会发生缓冲区溢出。安全做法是改为strncpy(buf, s, size)并将循环上界改为i < size。

五、测试用例验证:查询行为边界

查询的自动化测试位于 cpp/ql/test/query-tests/Security/CWE/CWE-119/SAMATE/(对应.qlref为 OverrunWriteProductFlow.qlref),测试源码 test.cpp 用注释标注了预期结果($ Alert[cpp/overrun-write]/$ Source[cpp/overrun-write]/GOOD/MISSING),是理解查询能力边界的绝佳素材:

  • 跨函数传递:mk_string_t分配缓冲区并保存size到结构体,strncpy(str->string, buf, str->size)被判定 GOOD,而str->size + 1触发 Alert,证明查询能跟踪“分配→结构体保存→读取使用”的跨函数流。
  • 包装函数:strncpy_wrapper场景(test2)验证流经中间调用层后大小关系仍被保留。
  • 条件约束:if(anotherSize < str->size)保护下的调用是 GOOD,if(anotherSize <= str->size + 1)则是 BAD——查询对<=/<与+1偏移的组合语义判断细致。
  • size与str->size的差异:当结构体保存的大小与局部变量size不同源时(test3/test4),查询对“直接使用局部变量大小”的某些越界(如size + 1)标注MISSING: Alert [NOT DETECTED],如实反映medium精度的漏报边界。
  • 分配大小减一:test5 中malloc(size - 1)配合str->size = size - 1,随后strncpy(..., str->size + 1)被检出,而strncpy(..., str->size)安全。
  • memset与分配语义:memset(p, 0, size + 1)对malloc(size)的缓冲越界被检出(test_flow_through_setter、foo);repeated_alerts中循环内++size导致的大小变化场景标注NOT DETECTED,体现对“大小在循环中可变”这一复杂情况的处理限度。
  • 误报抑制:test7 中malloc(++n)后的memset(p, 0, n)被标注为SPURIOUS: Alert ... GOOD [FALSE POSITIVE],说明当前实现对该场景仍存在已知误报。

这些标注让查询的“已知能力”与“已知局限”都以可验证的方式固化在仓库中,是评估该查询精度时最重要的第一手资料。

六、与同族查询的分工:overrun-write 在 CWE-119/CWE-120 家族中的位置

cpp/overrun-write并非孤立存在,它与 CWE-120(缓冲区复制不当)下的多个查询构成互补的检测家族:

查询 ID实现文件特点
cpp/overrun-writeSecurity/CWE/CWE-119/OverrunWriteProductFlow.qlpath-problem、ProductFlow 双投影、追踪“分配大小↔写入大小”跨函数流
cpp/overrunning-writeSecurity/CWE/CWE-120/OverrunWrite.qlproblem 类,基于BufferWrite模型,要求getMaxDataLimited估计值超过目标大小
cpp/very-likely-overrunning-writeSecurity/CWE/CWE-120/VeryLikelyOverrunWrite.qlproblem 类、precision high,仅采用ValueFlowAnalysis得出的高置信估计
cpp/overrunning-write-with-floatSecurity/CWE/CWE-120/OverrunWriteFloat.ql专门覆盖%f等浮点格式化导致的极端长度写入

从 OverrunWrite.ql 的源码可见,cpp/overrunning-write明确排除ValueFlowAnalysis原因的估计(“we exclude ValueFlowAnalysis as it is reported in cpp/very-likely-overrunning-write”),而 VeryLikelyOverrunWrite.ql 恰恰只接收ValueFlowAnalysis原因——两者互补去重。历史发布说明 cpp/ql/src/change-notes/released/0.0.8.md 也记载了cpp/very-likely-overrunning-write加入默认套件后接管了cpp/overrunning-write的部分结果。

相比之下,cpp/overrun-write走的是另一条技术路线:不依赖BufferWrite模型的“估计字节数”,而是通过 ProductFlow 将分配点的大小表达式与调用点的大小参数做符号级关联,因此在“结构体保存分配大小、经包装函数间接使用”这类场景下具备更强的跨函数追踪能力。

七、如何运行与验证该查询

在 CodeQL CLI 环境中,可使用查询套件或直接指定查询文件运行:

# 方式一:通过安全套件运行(该查询已收录) codeql database analyze <database> codeql/cpp-queries --format=sarif-latest --output=result.sarif # 方式二:直接指定查询文件 codeql database analyze <database> \ cpp/ql/src/Security/CWE/CWE-119/OverrunWriteProductFlow.ql \ --format=sarif-latest --output=result.sarif

仓库内验证该查询的推荐方式是 CodeQL 查询测试框架:以 OverrunWriteProductFlow.qlref 指向查询文件,配合 test.cpp 中的GOOD/BAD标注,运行codeql test run即可复现全部预期结果。

八、小结

cpp/overrun-write是 C++ 查询包 0.6.3 引入的缓冲区溢出检测利器,其独特价值在于:

  1. path-problem 输出:给出从分配到写入的完整路径,便于定位根因;
  2. ProductFlow 双投影:同时追踪缓冲区指针与大小值,处理malloc(x + 1)等带偏移分配、跨结构体/包装函数的大小传播;
  3. 与既有查询家族互补:与cpp/overrunning-write、cpp/very-likely-overrunning-write、cpp/overrunning-write-with-float按估计来源与置信度分工,降低重复告警。

值得注意的边界:该查询精度为medium,仓库测试明确标注了对“局部变量大小直接 +1”与“循环内大小递增”等场景的漏报,以及对malloc(++n)场景的已知误报。实际部署时,建议将其与同族查询(尤其是 precision 更高的cpp/very-likely-overrunning-write)组合使用,并依据测试用例理解告警含义,再结合代码评审确认修复方案。

  • 静态分析
  • SAST
  • 应用安全
  • 漏洞扫描
  • 代码质量

【免费下载链接】codeql

CodeQL: the libraries and queries that power security researchers around the world, as well as code scanning in GitHub Advanced Security

项目地址:https://gitcode.com/gh_mirrors/co/codeql
点击查看免费下载

相关推荐

上一篇:helm/charts仓库结构全解:stable与incubator的核心区别及Chart发布流程
下一篇:CopyQ 脚本命令(Script Commands)完全指南:扩展命令行接口与覆盖剪贴板处理函数

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询