Repository navigation
Releases: DannyAn/secguard-clang
Release list
v0.9.9
[0.9.9] - 2026-10-22
第三方库 API 语义模型(libxml2 / cJSON / libc 扩展 API)
docs/secguard-clang支持第三方库函数.md 首批清单落地:sgre/internal/apikb/external_api.go
新增 ExternalAPISpec(API 身份 / 分配 / 释放 / 所有权 / 返回可空性 / 借用,各维独立、
来源可审计),模型通过既有 IsDeclaredAllocator / IsDeclaredDeallocator /
IsKnownNullableReturn / IsEscapeFunction 查询通道被 memory-leak、null-deref、UAF/
double-free、graph ownership 边共同消费——不在各 detector 里散落第二份名单。落地能力:
- 专用配对验证(docs §6):xmlNewDoc→xmlFreeDoc、xmlXPathNewContext→xmlXPathFreeContext、
xmlXPathEvalExpression→xmlXPathFreeObject、cJSON_Create*/Parse→cJSON_Delete、
cJSON_Print→cJSON_free(free 等价);错配释放(XPath 对象交给 context 释放器、
free() 释放 XPath context、cJSON_Delete 释放 strdup 块)不再算有效释放,泄漏候选带
free_mismatch证据与free-mismatch@verdict fact 交 AI 研判。malloc/calloc/realloc
为 unrestricted(保持 tc116 等既有行为);未建模分配 API 行为不变。 - 所有权三态(docs §5.2C):
cJSON_AddItemReferenceToObject/Array建模为 Borrow,
覆盖_add启发式的误转移(引用型添加后不删 item 的泄漏从漏报翻为 definite);
cJSON_AddItemTo*/xmlDocSetRootElement/xmlAddChild/xmlAddSibling建模为
TransferOnSuccess——未检查返回值时豁免,守卫内(if (!add(...)))且无失败分支释放时
不再无条件豁免(失败分支泄漏以 suspected 候选报出,transfer-cond@提示 AI);
xmlSetNs因版本差异保守取 TransferAlways。转移参数按模型 argIdx 精确匹配。 - 出参分配成功分支绑定(docs §5.3):
asprintf/vasprintf/getline/getdelim的
char**出参分配从 detector 硬编码迁入模型;if (asprintf(&p,...) < 0) return;
(含ret = asprintf(...); if (ret < 0)赋值形态)的失败早退不再当泄漏证据
(该分支分配未发生)。失败方向只匹配< 0/== -1(空输出 0 是成功,防漏报)。 - 可空借用(docs §5.1D/§5.2A):
xmlDocGetRootElement/xmlFirst/NextElementChild/
cJSON_GetObjectItem*/cJSON_GetArrayItem挂 nullable 精确表(与 strchr 同档
auto-confirm),同时 RetBorrow 保证永不当作新分配——null-deref 对这批 API 的
unguarded 解引用从 external-unknown suspected 升为 confirmed,守卫/仅比较形态不报。 - cJSON 生命周期标准用法(用户权威形态):
cJSON_DetachItemFromObject/CaseSensitive/ FromArray建模为 AllocRet(脱离后归调用方,root 的 Delete 不再覆盖 → 脱离后不删是
definite 泄漏);新增 free-on-borrow 通道——释放借用指针(cJSON_GetObjectItem 的
item 被 cJSON_Delete、strstr/getenv/setlocale 结果被 free)报 invalid-free/UAF 风险
suspected 候选(hintfree-on-borrow@<api>+ verdict fact,永不 auto-confirm,AI 判
CWE-415/416 后果);借出后重新赋值则不报(不能证明释放的还是同一对象)。 - 测试:apikb 模型表驱动 11 组;evidence 泄漏矩阵 24 个 tc140-163 fixture(含用户
main 标准用法/Detach/借用删除形态);planner null-deref 6 组 + free-on-borrow 管道
1 组;benchmark Phase 26+26b(P26-01..12,5 finding + 6 no_finding + 1 suspected 侧)。
干净 A/B(独立 --db):第一轮候选 78→76、auto-confirm 139→143、post-only 空;第二轮
(26b)候选 76→77(post-only 恰为 P26-10 借用删除预期新增)、auto-confirm 143→144
(+1 = P26-12 Detach 泄漏)、pre-only 空。
use-after-free may-free 通道(用户 cbwl_tbl.c 样例)
用户五步审计法样例(PATTERN-02 释放后间接访问 + PATTERN-07 宏展开隐式 UAF)暴露
的漏报:防御式条件释放 wrapper(cbwl_tbl_destory 体内 if (node != NULL) free(node))的 FuncSummary 是 may-free(ParamMayDirectFrees),而
findAllFreeSites 只消费 must 的 ParamDirectFrees——跨函数条件释放后的使用
(含经宏 cbwl_nodeinfo 包装、展开后解引用 node->key 的间接使用)全部漏报。
修复:may-free 也生成 free site(事件带 may_free + callee);findUseSites 的
wrapper 参数 skip 同步扩 may(防释放行自报);LifetimeFilter 的
mustReaching→confirmed 加 !c.MayFree gate(对齐 double-free 的 MayFree
先例——conditional release 永不自动确认,suspected 交 AI,hint 带
may-free/cond@<helper>);use-after-free 的 SKILL.md Evidence Pattern 与
守卫测试同步(may_free fragment)。测试:
tc_uaf_conditional_release_use.c(用户精确形态三变体:may 漏报红→绿、
must 确认、修复对照零事件)+ planner 分级测试(may=suspected、must=confirmed)
- benchmark Phase 27(P27-01 must+宏 finding 走 macro-context AI 复核、
P27-02 修复对照 no_finding);A/B 全量 78→78 / 145→145 双向差集空(零回归)。
用户 HRP 空指针审计样例回归(能力锁定,零检测器改动)
用户两文件五步审计样例(CWD-1031/1034 四 PATTERN)端到端判定全部已检出,无需修复:
P-02(guard 分支内 ctx = Hrp_AllocSession(peerId) 重赋值为树内 proven-nullable
wrapper(malloc+字面 return NULL)后未判空)→ retNullable proven 通道
auto-confirmed(入口 ctx==NULL 守卫不保护重赋值后的解引用,ND-16 同档);
P-03(ctx->negotiator->state 成员链,extern assumed 源)→ null@opaque-origin
suspected + implicit-contract ptr_guard 双通道;P-09(IPC msg->ctxPtr 强转
解引用)→ null@opaque-origin src@ 强转行 suspected + implicit-contract;
P-05(条件与指针无关的分支内外双重解引用)→ null@caller-arg suspected
(boundary 场景实证)。用户三个修复方案全通道零候选(FP=0)。落地:
tc164/tc165/tc166 fixture + planner 五组单测(四 PATTERN tier 断言 + 修复
对照零候选)+ benchmark Phase 28(P28-01 finding + P28-02/03/04 修复
no_finding;P-03/P-09/P-05 suspected 侧由单测锁定——P-05 依赖 boundary 语义,
全量目录含 main 时 hrp 函数不可达被 CallReachFilter 正确丢弃)。A/B 80→80 /
146→146 双向差集空(零回归)。判定记录:现有 fixture(tc106 等)仅覆盖
cast-reassign 防误报方向,用户四 PATTERN 正向形态此前无同类用例,本轮补齐。
goto cleanup 形态 CFG 缺口修复(用户 hrp_buf.c double-free 样例)
用户样例 PATTERN-01(错误路径 free 后 goto cleanup 跳到 return 0; 之后的
label 块再次 free)暴露 StmtCFG 架构缺口:buildBlock 在 terminator 后 break,
label 块整体不建模(无节点/无事实/无可达性);goto 的 dead-end 处理注释
"exact for forward jumps" 前提不成立——cleanup 形态的 label 恰位于 return 之后,
流 filter 将候选误 dismiss 为 unreachable(端到端实证漏报,detector 事件已生成、
filter input=2 output=1)。修复(graph/control_flow.go):
(1) buildBlock 在 terminated 后为 labeled_statement 建 detached 孤立链
(from=-1 无直连入边,label 后续语句链式挂接,return 出口正常连 exit);
(2) 新增 linkGotoLabels 后处理——解析 goto_statement 的 statement_identifier
目标与 labeled_statement 名匹配,补 goto 节点→label 首语句节点边(去重、
self-edge 防护);(3) fall-through NULL 事实语义保持(goto 边只从 goto 节点
出发,if (p==NULL) goto out; 反证单测锁定)。P-06(跨函数条件释放)走
既有 may-free 通道零改动即检出(suspected + may-free/cond@ hint)。测试:
tc167/tc168 fixture + planner 四组单测(P-01 confirmed 含 free@30,38 hint、
P-06 suspected+mayFree+may-free/cond@queue_push、修复对照零候选、goto
null-guard fall-through 反证)+ benchmark Phase 29(P29-01 finding /
P29-02/03 no_finding);A/B 候选 83→83 双向差集空、auto 148→149(+1 =
P29-01),go test ./... + nosqlite 全绿。
0.9.9 发布前自检修复(GLM5.3 改动复核)
发布前对 0.9.8 之后 20 个提交(GLM5.3 为主)逐项复核,go build ./... /
go test ./... / nosqlite 子集全绿;发现并修正两处:
- use-after-free
cond@hint 越界:buildHintParts对任何带 callee 的 UAF 候选
都发cond@<helper>,但callee在 must 间接释放(callee 无条件 free)与字段释放时
也会置位——cond@("条件释放 helper")语义被错误安到已证实释放的候选上。改为仅在
may_free时发cond@,并把 hint token 描述从 double-free 专属措辞泛化。 - 出参分配失败守卫
<方向过宽:isFailureLiteral对op == "<"对任意数字字面量
都返回真,if (ret < 5) return;这类正值比较会被误判为"分配失败"而吞掉成功短输出
分支上的真实泄漏。收紧为字面量<= 0才算失败守卫(< 0与负边界),正值比较不再豁免。
null-deref 守卫形态误判修复(生产 MAF 扫描报告,候选重发)
生产环境 MAF 全量扫描(122 文件 / 977 函数)反馈报告经逐条实证后落地两项修复
(报告的 boundary tier 一刀切降级建议经三重语义冲突验证后否决,未采纳):
- ternary 内联守卫识别(auto-confirmed 误报修复):
return (profile != NULL ? profile->field : default)形态被机器判定为null@malloc-fail + confirmed并
auto-confirmed——三元表达式位于单条直线 CFG 节点内,边 kill 守卫模型不可见。
新增buildTernaryGuardMap:从 AST 精确记录守卫分支(consequence /
alternative)内 base 与行号均精确的解引用点,nullable_source收敛阶段消费。
AST 级分支判定保证同行p == NULL ? p->a : 0(NULL 路径解引用在 consequence)
仍可报出——行区间近似会误杀该真缺陷形态。 - branch-hint 宏包装条件解包(auto-confirmed 误报修复):
if (unlikely(slot == NULL)) return;形态同样产生 confirmed 误报——
call_expression 条件对守卫解析不透明。parser层GuardedNonnullVars/
NullCheckedVars入口统一解包likely/unlikely/__builtin_expect
(含*_likely/*_unlikely厂商变体与成对!!否定),全部 20+ 消费点
(guard_model 边 kill、短路弱守卫、caller_null、null_guard、unchecked_return、
contract_gap、memory_leak)一次受益。 - 验证:A/B 基线对照——修复前 2 个 CWE-476 auto-confirmed 误报(ternary +
unlikely 各一),修复后 0;benchmark 83/83 候选与 16/16 null-deref
auto-confirmed 完全一致(零回归);全量测试与 nosqlite 子集绿;7 个红绿对照
用例转正回归测试(zz_null_deref_guard_forms_test.go),boundary fail-open
契约另固化 characterization 测试(zz_null_deref_boundary_char_test.go)。
AI 研判编排优化(并发复核去重 + 跨函数模式复用)
同报告落地的指令层两项(extension/shared,一致性守卫通过):
- RANGE-split 类型 auto-confirmed 复核 owner 去重:单类型按候选区间拆分为多个
并发子代理时,--list-auto-confirmed对每个 sibling 返回同一批未复核条目(幂等
过滤只剔除已有 verdict 的条目,并发竞态下 sibling 尚未写入),201 候选 4 分片
场景 7 条 auto-confirmed 被复核 28 次。编排指令改为指定首个分片为该类型复核
owner、其余 sibling 跳过该节;audit 前count: 0全局校验保持兜底(owner 分片
失败时补派 re-review-only 子代理)。 - 跨函数同模式研判复用(Group verdict 扩展):同文件同 tier 同形态的候选组
(回调四件套注册进同一 ops 结构、迭代器族、同框架参数的 sibling handler)从
一个代表函数的 Code Context 一次研判,跨 sibling 以pattern:<file>:<tier>
引用;形态分叉的 sibling 必须单独研判,模式复用不得越过异质项。 - 预期效果:MAF 场景 null-deref 2 个 auto-confirmed 误报归零(对应复核负担
消除)+ 同质合约研判成本经模式复用显著下降;201 候选量级压缩需按"根因审查
单元"方向另行立项,本版本不改变 boundary fail-open 候选协议。
null-deref 端到端复检与系统性修复(20 commits,全程 TDD)
对 null-deref 子链路(detector → planner → auto-confirm → candidates → AI 研判)
做端到端复检并逐项修复,覆盖 2 CRITICAL / 12 MAJOR / 5 MINOR + 1 项编排文档:
- 跨函数空值传播断链(CRITICAL):
functionParamNames只认直接
function_declarator,所有返回指针的函数(T *f(T *p)的 declarator 被
pointer_declarator 包裹)被 caller_null / opaque_null / buffer_overflow /
buffer_unchecked_index / contract_gap 五个 detector 同时跳过,参数永不 seed;
叠加computeRetNullablefixpoint 未按 buildFlowResults 方式拆分
entrySeeds/definiteEntrySeeds——caller_null 源按行号分桶后不落任何 CFG 节点,
T *q = p; return q;形态的 callee 被误判 notNull,所有调用点派生解引用静默
漏报。两处协同修复(evidence 剥壳 + planner 拆分),端到端实证callee(NULL)
调用点候选恢复。 - AI 举证断链(CRITICAL):assumed-return 变量路径候选(
p = f(); p->field,
f 为 fail-open in-scan callee)的 Hint/VerdictFact/SKILL 三处都承诺"callee body
已嵌入## Callee Definition",但 markdown 嵌入条件只覆盖 IsCallResultDeref 与
malloc-fail——AI 翻遍候选文件找不到承诺的段落。嵌入条件对齐 buildHintParts。 - detector / evidence 形态覆盖(9 项):指针算术解引用(
*(p+1))基指针
提取;guard 判定删除 evidence 本地漂移副本统一走parser.GuardedNonnullVars
(p != q误判 +unlikely()漏识双向漂移);caller 短路||守卫;cast
guard 结构提取变量名("(struct T *)p"垃圾键);p = p->next循环携带发
field_read 源(D8 测试断言曾把漏报固化为正确,已修正);out-of-scan alloc
wrapper(xmalloc仅声明)双边真空按"谁实际处理"拆分 skip;(*p).x基变量
键卫生;assert 必须 dominating call(嵌套 assert 不再误 guard);interprocedural
arg 对齐 classifyArg(cast/field 形态不再跳过)。 - planner 守卫与分析(6 项):switch null-case-return 守卫(buildSwitch 新建
透明 header 节点供 guard 挂载);branch-hint + helper 复合守卫
(unlikely(is_empty(p)));deref-read 赋值(r = *(T**)msg)发 deref_read
源且不 contract-derived;cast 跳 type_descriptor(g_arr + i * 8静态数组
算术不再残留 suspected);isGrowCall 词边界(growth_rate不再误命中);
typedef 拆分形态 opaque 判定(typedef struct Foo Foo_t;+ 独立定义)。 - AI 协议一致性(5 项):
null@opaque-origin/null@assumed-return两个
tier token 补入 agent-body / command-instructions 主清单,新增 ...
v0.9.8
[0.9.8] - 2026-10-21
两项"待拍板"的结论(已按方案落地,不再等决策)
1) auto-confirmed 复核:不做抽样,做"策略类批量确认"。 unreviewed=0 是
"AI 阶段完成"的硬门禁,抽样会让它变成可跳过项——用 18 条一次性 --review-json
的写入成本,换一个会被悄悄放宽的诚实性契约,不值得。真正该省的是"对纯名单类
发现逐条读上下文":dangerous-function(禁用 API)/signal-handler(异步不安全
API)/weak_algorithm/weak_random 的证明就是"名字在名单里",复核写成一次批量
confirmed + reason: policy:<list>,零读源、零 Evidence 打开;语义类
(null@*/leak/uninit/oob)保持逐条复核。
2) null-deref 系统性误报:不做"非空约定推断",做"扫描范围 + 组裁决"。 跨函数
字段写入推断对外部构造的对象不成立(边界扫描里调用方在树外,字段值恰恰未知),
用它压候选等于用误报换漏报。真正可闭环的是两条:
- 扫描范围:新增
scan_scope诊断,且 boundary 模式写进扫描摘要——"目标是无
main的子树,边界指针参数一律 fail-open,请扫 worktree 根"。maf 的 201 条
null-deref 最可能就是这一类(扫maf/src而main在别处)。 - 组裁决:同函数行在
_index.md里连续(按 file:line 排序),一次判一个契约,
其余行复用group:<fn>:<tier>;boundary 扫描额外给"树外调用方"快捷判据。
候选条数不变(每个 still 一条审计记录),但研判从"逐行重推契约"变成"每函数一次"。
扫描流程闭环:读源/证据预算、子代理红线、组裁决、扫描边界诊断
按 maf 生产扫描复盘(122 文件 / 977 函数,种子 5250 → 候选 314,AI 研判 520s)逐条核对:
--complete-type协议矛盾(真 bug):step 4 的 Claude Code 分支原文让子代理写完
findings 后调--complete-type,而 OpenCode 分支与"子代理禁止"规则说不要——子代理
照做了。已删除该指令,并在子代理提示词顶部加红线(禁止--complete-type/--audit、
禁止重跑 scan/plan/index)。MarkAIStageDone是幂等 UPDATE(同值并发不会写坏行),
真正的风险是"提前置 done"让 orchestrator 跳过未写完的类型。- Evidence 打开预算:此前只有 ≤5 源读取预算,Evidence 打开无上限(memory-leak 25/25
全开)。新增MAX_EVIDENCE_OPENS_PER_TYPE=12:决定性 Hint 必须零打开结案
(null@cfg-proof/null@malloc-fail/leak@definite+escape@none/常量idx@越
cap@/contract@known/async-unsafe@/cast@),只有启发式 token 才开;超预算时
未决候选进结构化报告needs_context,orchestrator 按候选范围二次下发一次。F6 审计
增加evidence_opens > 12观察项。 - leak 类型 Hint 可裁决:memory-leak 技能补明确表(
leak@definite+escape@none
直接 confirm;leak@conditional对比free@N,M;只有escape@return|store才开
Code Context)。 - 组裁决:null-deref / uninit / leak 中同
Function+ 同来源 token +guard@none
的行按一个契约问题判一次,其余复用group:<fn>:<tier>,不再逐行重推(对应"201 候选
0 确认"的研判成本)。 - confirmed=0 跳过写入:全 dismissed 的分片不写 findings 文件、不调
--write-json。 - 分片均衡:
n = ceil(count/100)段、每段ceil(count/n)(201 → 3×67),不再
100+100+1 或 55/55/55/36。 - 扫描边界诊断(新增
scan_scope):scan 输出新增mode(complete-program/
boundary)+enclosing_worktree+ 提示。扫描repo/src而main在repo/app
时,整个模块会被当作外部边界 fail-open——这是 maf 201 条 null-deref 候选最可能的
量级来源;诊断让它第一次就可见,而不是靠 AI 判 201 次"外部契约"。 - PATH 写法:orchestrator 脚本禁止
VAR=... for ...,已写进提示词。 - 未改(需产品决策):auto-confirmed 抽样复核(会破坏
unreviewed=0终态门禁);
null-deref 的项目级非空约定建模(需跨函数字段写入分析)。
测试夹具的 fd 0 关闭竞态(随机 FAIL 的真因)
log.New(os.NewFile(0, os.DevNull), ...) 把 fd 0 包成了 *os.File:GC 执行该
File 的 finalizer 时会 close(0);此后任何 open() 拿到 fd 0,再被下一次 finalize
关闭,就出现随机的 TempDir RemoveAll cleanup: unlinkat ...: bad file descriptor。
这就是十轮里"不同用例随机挂、单独跑又过"的根因(与检测逻辑无关),在 TMPDIR 放进
仓库或 /tmp 时都会出现。四处测试助手改为 log.New(io.Discard, ...);planner +
evidence 连续 8 轮全量复跑 0 失败(此前约 2/6 轮随机失败)。
检测器输入统一为全树事实层(声明在头文件不再丢失)
审计发现的系统性缺陷:多处以 root.FindAll(...) 只采当前文件的声明事实,声明放在
头文件(axxx.h -> axxx.c、axxx.h + bxxx.h -> cxxx.c 这种 C 最常规布局)时容量/
类型/宏静默取不到,检测直接消失——out-of-bounds 的 svr[actived_index] 只是被撞见的
一个实例。
- 新增共享事实层
internal/repofacts:一次遍历全部索引文件,聚合 typedef 别名、对象
宏、struct/union 布局(成员类型/指针/数组容量+容量符号)、文件作用域数组;按 struct
tag / typedef 别名精确解析(同名字段互不遮蔽),冲突即判未知(不猜)。 - 接入的消费者:out-of-bounds / buffer-overflow 的成员数组与全局数组容量、容量宏;
null-deref planner 的数组基址判定(头文件里的数组不再被当成可空指针)。per-file
扫描仍是快路径,事实层只做兜底。 - 类级回归:
TestDeclarationFactInvariance—— 同一组 fixture 的内联版与头文件拆分版
必须产出完全相同的候选(out-of-bounds、buffer-overflow),以后任何"只看当前
文件"的退化都会被它挡住;另加TestUncheckedIndexHeaderDeclaredStruct(原帖形态)与
TestArrayCapacityDeclaredInHeader(头文件全局数组)。 - 体量:benchmark out-of-bounds 2 → 3(新增即 P25)、null-deref 22 不变;redis
out-of-bounds 1 → 36、buffer-overflow 219 → 230、null-deref 1320 不变。 - 已知未覆盖:
crypto_misuse的 undersized-key 仍只扫declaration,结构体成员 key
在头文件时不报——它需要"非函数实体归属"的事件模型,本轮不改(明确记录)。
本版本经多轮重发收敛为最终内容:正文只列最终能力、验证结果与升级方式(逐轮迭代
明细见 git log 中各提交说明)。
发布概要
- null-deref:第三方 typedef / 前向声明 / 头文件定义的类型不再因为"类型信息不完整"
而漏报;外部入口按扫描边界判定是否 fail-open;派生指针的来源归因正确;同一指针的
多处解引用在一份候选证据里列全。 - out-of-bounds:固定数组成员以未校验配置字段为下标的越界读,连同下游空指针以
"一条 finding + CWE-125→CWE-476 链条"上报。 - unchecked-return:按消费点普查收敛"调用处未检查返回值"的系统性误报。
- CLI:DB 向上查找在项目根停止,避免把外层 checkout 的旧库当成当前项目。
null-deref:不完整类型信息
- 断点在播种,不在解析:tree-sitter 对未知 typedef 完全容错,语法与解引用节点都在。
- opaque 字段值通道:base 参数类型在扫描树内不可见时,
p = (T *)base->field与
链式中间值base->f1->f2播种 fail-open 来源(suspected,永不 auto-confirm)。 - 类型普查:
collectDefinedTypes覆盖无函数的头文件(此前被跳过,导致树内 header
类型被误判 opaque);typedef struct X X_t;前向声明仍判不可见。 - 入口边界策略(取代旧"布局可见 = 非空"判据):扫描树内有
main时按项目自身非空
不变式收敛;无main(库 / 部分子树)时边界即外部 ABI,一律 fail-open。两个基准语料
均为完整程序,候选量零变化。 - 派生指针来源:
q = (T *)param、q = t(opaque 字段读)、q = malloc 别名的
tier 归到真正持有来源的变量(caller-arg / opaque-origin / malloc-fail),不再一律
external-unknown。 - 多解引用点:按变量收敛仍是一条候选,但
deref_lines与证据 sink 段列出全部解引用
行(分支内与分支后的tree[0].group_name都在)。 - 地址取用普查:注册表裸函数名(
{"get", getCommand})与被#include的生成注册
片段(commands.def)都计入;ADDR_TAKEN因此补齐,注册回调不再被当作死代码或
不可达而丢弃。
null-deref:误报与候选量级
- 调用链开放性 fixpoint:只在"可能为空的值真能到达参数"时播种;纯转发不播种、任一
环节的守卫闭合整条链。修复生产环境候选从历史 ≤160 爆炸到 1264 的问题。 - 守卫契约:短路求值、条件非空 getter、guard-mutex 调用三类误报收敛(6 项 FP)。
- retNullable 三态:proven(体内有 NULL 路径)/ assumed(fail-open)/ 不可为空,
分别对应 confirmed / suspected / dismiss。
out-of-bounds:未校验外部索引
- 新增
array_oob_read_unchecked/array_oob_write_unchecked(suspected):结构体
固定数组 + 字段来源下标 + 函数内无与容量相等的边界检查。 - 头文件拆分形态:成员数组容量改为全树普查(宏跨文件合并、按 struct tag / typedef
别名精确解析),结构体定义在头文件时不再漏报——生产回归即svr[actived_index]
全仓库 0 候选;&arr[idx]传址逃逸优先作为oob-escape@。 - 链条以
cwe-chain@CWE-125→CWE-476token、候选## Vulnerability Chain段、SARIF
properties.cwe_chain导出;根因与后果只出一条 finding。 - 回归:benchmark Phase 24/25(含头文件拆分正例与修复对照)+ evidence 同形态用例;
体量 redis out-of-bounds 1 → 30、benchmark 2 → 3,其它类型不变。
unchecked-return
- 消费点普查:调用结果被使用却未检查、且没有其他消费点做过检查时才上报,修复 IDM 报告
25/25 全误报的根因。
CLI / 扩展
resolveExistingDBPath在第一个含.git的祖先停止(项目根自己的 DB 仍然命中)。claude-cac(CodeAgentCLI)宿主下子代理按命名空间名分发。
验证
go test ./...:16 个包全绿;-tags nosqlite子集通过。- benchmark
--selftest:206 用例、25 个 CWE 标签全映射。 - 本版新增 evidence / planner 回归覆盖不完整类型、入口边界、派生来源、多解引用点、
注册表与 include 片段;均验证"去掉修复即失败"。 - 体量:redis 7.2.4(136k LOC)null-deref 候选 1283 → 1320(+2.9%,新增为注册回调的
框架出参,suspected 交 AI);benchmark 22 条不变。 - 性能:95k LOC / 100 文件合成语料 detectors 85.0s → 87.4s(+2.8%)。
升级
- 重装二进制与扩展(
./deploy.sh或对应平台安装包),确认secguard --version为
0.9.8。
v0.9.7
[0.9.7] - 2026-10-16
发布后修复(审计 P3 收尾)
- 候选阶段
report.md的 "Candidates by Skill" 表新增Auto-confirmed列,逐类型拆分机器确认数——此前纯 auto-confirmed 类型(如 out-of-bounds)在表中显示 0 候选,掩盖了实际已确认的发现。 - 移除 injection 检测器命令注入事件中无人读取的死字段
source。
v0.9.6
[0.9.6] - 2026-10-16
证据契约断链修复(round2,R2-01~17 闭环)
本轮修复检测器已算出的语义事实在「候选装载 → Evidence 渲染 → Hint → 候选文件/索引」链路上被丢弃的问题,使 AI Agent 能从 _index.md 的 Source+Hint 列直接研判,而不是打开源码或凭空推断。
- 补齐字段流:TOCTOU 的
check_function/use_function/path_arg、toctou_shared_state的mutex/锁行、cryptoreason/key_size、format_arg、path-traversalpath、memory/resource-leak 的 acquireapi、integer-overflowlhs_influenced等此前静默归零的字段,现经eventProps统一解码(数字字符串兼容)后真正进入Candidate。 - 补齐空 Hint 列:unchecked-return 的
contract@known/unknown+callee@<file>:<line>定义定位、divide-by-zero 的divisor@<shape>、deadlock 的locks@A|B+reverse@<fn>、use-after-free/double-free 的free@N/free@N,M、uninit 的certain/maybe-uninit等,任何类型的 Hint 不再出现裸 "—"。 - Code Context 策略:integer-overflow 改按整函数 span 渲染(上限 200 行,签名→候选);源→汇锚行落在 ±8 窗口外时按
[min-3, max+3]拓宽;taint 类候选填充SourceLine并新增## Taint Source段。 - unchecked-return:候选文件新增
## Failure Contract段——contract@unknown时给出 callee 定义定位(跨文件工厂常见),无定义时显式标注"external/library",AI 不再烧 read 预算盲找。 - hardcoded-secret:结构化
secret_value片段脱敏(前缀 4 字符 + 长度),避免报告文件泄露完整字面量。 - deadlock:反向持锁函数回填,
reverse_order片段与reverse@Hint 在生产链路首次可用。
契约守卫测试
TestSkillFragmentsMatchBuildEvidence:SKILL.md 的 Evidence Pattern 与BuildEvidence片段词汇双向一致性机器守卫。TestSeedPropsFlowToCandidate:props→Candidate 字段流守卫(覆盖 R2-01~07 的断链)。buildhint_test/evidence_package_test/zz_range_filter_test/zz_lock_order_test/zz_unchecked_return_callee_test:Hint 非空、DivisorInterval 传递、reverse 回填、callee 定位等定向回归。
v0.9.5
[0.9.5] - 2026-10-14(2026-10-15 二次重发)
unchecked-return 全局变量存工厂函数误报修复(二次重发)
修复 0.9.5 生产冒烟发现的 unchecked-return 误报:g_user_grp_id_mgr = idm_new_idmgr(&cfg)
这类「_new 工厂函数返回值存入全局变量」被误判为 CWE-252。根因有二:
apikb.IsAllocator的_new子串启发式把idm_new_idmgr这类对象工厂当成了分配器,
契约仅按名字猜测(contract@unknown);- 检测器的「返回值是否被检查」分析(
checkedVars)是函数作用域的,而全局变量的
判空发生在别的函数的 use 处(或懒初始化守卫),检测器看不到,于是把「存入全局」误判为
「未检查」。
修复:unchecked_return.go 对 contract@unknown(名字启发式)的分配器调用,当返回值存入
非局部存储(全局/static/extern,判定为「既非本函数局部声明、也非参数」)时不再产事件。
真正缺陷(解引用 NULL 全局)由 null-deref 在 use 处兜底;声明式分配器(malloc/strdup/VOS_*)
存全局、启发式工厂存局部,均仍照旧报告。
回归用例:tc_unchecked_return_global_assign.c(单文件)+ unchecked_return_crossfile/
(跨文件复现 idm_db_id.c 引用 idm_idmgr.h 声明、idm_idmgr.c 实现的真实场景)+
unchecked_return_global_assign_test.go(全局存工厂 0 事件 / 局部存工厂仍 lead / malloc 存全局仍报)。
null-deref 字段强转派生指针漏检修复(重发)
修复 0.9.5 生产验证发现的系统性漏检(false negative):tree = (T*)(resp->group_data); tree[0] 这类「字段读取 + 强转 → 局部指针」的空指针解引用未被 null-deref 检出。根因是
字段/下标读取赋值 q = p->f 只传播字段位置 p->f 自身的空值源、不传播基指针 p 的
空值源,导致派生指针 tree 无法继承参数 resp 的 caller-null 源,在 nullable_source
过滤环节被淘汰。
null_flow.go:字段/下标读取复制新增copyFieldBase效应——q = p->f/q = (T*)p->f
同时把基指针的空值源传导给q(字段位置自身空值之外),派生指针得以继承 caller-null 源;- 仅作用于 null-deref(
nonNullKills门控),taint / use-after-free / uninit 流不变; - 派生指针仍为
suspected、不 machine-confirm,判空守卫(if (tree == NULL) return;)正确抑制; - 回归用例
zz_null_deref_field_cast_test.go:未判空调用方 / 字面 NULL 调用方 / 不透明结构体 / 判空抑制。
implicit-contract 隐式契约缺失检测(CWE-20,新增 vuln type)
新增 implicit-contract 能力:检测「函数入口缺少对输入对象关联状态校验」的隐式契约
缺失——结构体指针参数的可空指针字段(如 tree = (T*)resp->group_data)在无支配性判空的
情况下被解引用。区别于 null-deref 的「可证明空源」,本类型覆盖开放世界场景(字段由
调用方/框架供给、函数自身不写该字段),全部 suspected 交 AI 研判,绝不 machine-confirm。
- 证据门(三选一,无证据不产候选):
- E-b 自证:函数自身在另一条路径判过该字段空;
- E-a 同类契约:同形参类型的兄弟函数判过该字段空(按声明类型锚定,不跨 struct 裸名匹配);
- E-c 调用方:调用方判空作证据;所有调用方都判空则抑制(调用方已完成校验,§7.1)。
- 确定性砍刀:call-reach 不可达丢弃;CFG
ReachesAvoiding守卫支配丢弃(逐 use site);
字段被函数自身写则归 null-deref(避免双告警)。 - count/length 只作可选增强证据,不 gate 检测——
tree == NULL(指针判空)独立于任何
计数关系;count==0是业务语义,不做独立count_guard。
配套:新增 CONTRACT_GAP 事件 + evidence/contract_gap.go detector +
planner/filter_contract_gap.go(守卫支配 + 调用方校验抑制)+ implicit-contract skill;
schema 枚举/vuln_type_skill.go 同步。回归用例:tc_contract_gap.c(E-b 部分守卫/纯指针
无 count/无人判空反例/调用方校验两侧)、tc_contract_gap_prod.c(生产用例
import_dn_group_tree_refresh 镜像 + 变体:兄弟契约、单分支判空、count==0 处理但指针未判、
字段读未解引用、调用方已判空)。
dangerous-function 分层策略扩展(CWE-676)
将 dangerous-function 从「10 个无争议函数的极简名单」扩展为分层的禁用函数策略
检查,覆盖企业编码规范中「调用即违规」的函数族。检测器仍在
evidence/dangerous_function.go,内置规则表按 category 分层:
process_exit:exit/ExitProcess/pthread_exit/ExitThread(仅非入口函数)、
atexit、abortstack_allocation:allocanonlocal_jump:setjmp/longjmpunsafe_memory/unsafe_string/unsafe_format/unsafe_scan:非_s的
mem*/str*/wcs*/格式化输出/格式化输入/gets族unchecked_conversion:atoi/atol/atoll/atofunsafe_alloc:reallocassert_usage:assertinsecure_temp/obsolete_api:原有清单(mktemp/gethostbyname/inet_addr…)
判定:除 assert_usage 外全部 confirmed(名字匹配即缺陷);assert 是否用于
校验入参需意图判断,交 AI 研判(suspected)。exit 族仅在
main/wmain/WinMain/wWinMain 之外报;inet_addr(常量) 仍豁免。
unsafe_memory/unsafe_string/unsafe_format/unsafe_scan 与
buffer-overflow/injection/format-string 刻意重叠:后者回答「这一次调用会不会
溢出/注入」,前者回答策略维度「是否使用了非 _s API」。
配套:registry.go 增加 CategoryConfidence{assert_usage: suspected};
SKILL.md/vuln_type_skill.go/docs/config.md/usage.go 同步;tc109 与 benchmark
p11_dangerous_function.c(DGF-01..15)扩展;AI_JUDGMENT_BOUNDARY.md 登记
assert_usage 例外。
Annex K _s 合约表修正:snprintf_s/vsnprintf_s CountArgIdx
SecureFunctions 中 snprintf_s 和 vsnprintf_s 的 CountArgIdx 误设为 2,
指向 format 字符串参数而非 count 参数。C11 Annex K 的签名为
snprintf_s(s, n, format, ...) / vsnprintf_s(s, n, format, ap)——n 既是容量
也是最大写入数,无独立 count 参数。修正为 CountArgIdx: -1。原值因
evaluateSizeArg 对字符串字面量返回 0 而未造成误报,但合约定义错误已修正。
tc68 增加 secure_lying_snprintf / secure_correct_snprintf 回归用例。
IDM 仓库误报治理:分级修正(第二轮)
对 docs/实战IDM仓库误报问题改进建议.md 第一轮未闭环的项逐条复现后判定:「机器自动
确认」的误报必须修(AI 没有机会过滤),启发式命中不得进确定性层,漏报优先于
误报。本轮落地 4 项:
- dangerous-function /
inet_addr(CWE-676):规则表自己写着它的风险是「运行期可控
字符串 + 未检查INADDR_NONE」的条件风险,却按「调用即缺陷」自动确认,与规则自身
的理由矛盾。inet_addr 改用类别obsolete_api_conditional(→suspected),交 AI 按
实参来源与返回值检查研判;gethostbyname/gethostbyaddr/mktemp等无条件风险项
仍是confirmed,inet_addr(<常量>)仍豁免。SKILL.md补了对应研判规则(内部配置
字段/已解析协议值/已查INADDR_NONE→ dismiss);AI_JUDGMENT_BOUNDARY.md同步登记。 - injection:纯透传执行器不再自动确认。
idm_exec_sql(sql) { return sqlite3_exec(g_db, sql, ...); }体内没有任何 SQL 构造,字符串完全来自调用方,「调用方拼的 SQL 是否可
注入」在本地无法判定;这类候选改为suspected交 AI(原先的 drop 分支只在「看不到调用
方污染」时生效,一旦 PARAM_BINDING 证明有污染就升级 confirmed,正是误报来源)。函数内
sprintf构造后 exec 的真实注入仍为 confirmed(回归用例覆盖两侧)。 - unchecked-return:名字启发式命中不再自动确认。新增事件属性
contract_known:被调方是内置 API 表 / 已声明 allocator / 函数体已检视的透传
wrapper 时为 true;仅凭名字像分配器(alloc子串 /_dup/_new)+ 返回指针的匹配
为 false,ReturnCheckFilter只提升 true 的候选。这与 hardcoded-secret 的
value_proven(值本身确定 vs 仅名字命中)同构。修掉文档 mode A 的 3 个误报
(idm_asset_record_dup往调用方缓冲区拷贝、不会失败),而malloc(n)未判空这类
契约已知的漏判仍为confirmed。 - use-after-free 漏报修复(非文档项,本轮复现发现):
free(p); p->n = 1;这类
对被释放对象整体做字段写此前被漏报——isFieldWrite把所有字段写都排除在「使用」
之外,但该排除只对「被释放的是那个字段」成立。现在字段写会记一条基对象使用,
匹配阶段再按fs.field丢弃:free(s->msg); s->msg = NULL;(字段释放后清指针)与
free(s->msg); return s->mode;仍不报,free(p); p->n = 1;现在报(写已释放内存 =
堆破坏)。实测p->n=1/p->next=0/*p=/p[0]=/读/传参 7 种形态全部命中。
另外两项判定为无需修改(复现为「不产生候选」或已修):
- double-free / use-after-free 的文档形态不成立:
http_destroy_active_conn_list(pool)
/http_destroy_idle_conn_list(pool)/http_free(pool)与free(p); free(p);的复现
用例中,前者seed_count=0(_destroy名字启发式已不再等同「释放该对象」),后者只对
真free命中,与文档描述的误报不符——已在更早版本修掉。 - null-deref 的「被调函数入口守卫」已被识别:复现中
callee_guarded(p){ if(p==NULL) return -1; return p->v; }+ caller 传未证非空变量 → 0 候选,而未加守卫的对照函数
→ 1 个suspected。文档 risk #2 的 resize 语义在 v0.9.4 已修。
同时把字符串常量表收敛为一份:buildGlobalStringMacros 扩展为支持宏拼接
(#define LOG DIR "/log")并被 path-traversal 复用(原先 path-traversal 自带一份
per-file 宏表,属重复),path-traversal 只额外收集本文件的 static const char 对象。
回归用例:zz_quality_round_test.go(UAF 字段写/字段释放对照、contract_known 两侧、
inet_addr 三态:常量豁免 / 参数与字段 suspected / 其他 obsolete_api 仍 confirmed)、
TestReturnCheckFilter_HeuristicAllocatorStaysSuspected、
TestTaintSourceFilter_PassthroughSQLNotAutoConfirmed。未发布版本,VERSION 不变。
IDM 仓库误报治理:常量路径与安全函数族(第一轮)
针对 docs/实战IDM仓库误报问题改进建议.md 中 30 个误报的根因,检测器/收敛层收敛:
- path-traversal(CWE-22):
- 编译期常量路径识别(零配置):
buildStringConstants收集文件内的字符串常量
——展开后只由字符串字面量/已知常量构成的宏(#define DIR "/var"、
#define DB DIR "/db")、以及static const char且未被重新赋值的对象
(数组形式 / 指针形式;指针形式要求static且在文件内无赋值)。sink 的路径实参
直接是这些名字时抑制候选;实参是仅由常量拼装的本地缓冲区时同样抑制
(snprintf(path, sizeof path, "%s", DB_PATH)、strcpy(path, DB_PATH),按
dest-arg + 全常量实参列表判定)。反例全部保持上报:#define P DIR user_name
这类混入未知标识符的宏、非 const 的static char[]、被重新赋值的
static const char *、非 static 的const char *,以及
snprintf(path, sizeof path, "%s/%s", BASE, name)这类含运行期分量的构造
(常量 BASE 本身不构成安全证明:name里的../仍能逃出 BASE,故交 AI 研判)。 - AI 研判边界收紧:
TaintSourceFilter不再因「外部可达的参数流到 sink」把
path-traversal 升级confirmed,改为保持suspected交 AI。「外部数据到达路径
参数」只证明调用方能影响字符串,不证明能出现目录逃逸分量——按
AI_JUDGMENT_BOUNDARY.md的定义,confirmed 要求缺陷在每条路径上成立。
injection/format-string 的升级保持不变(未参数化的 sink 被外部数据触达即为缺陷)。
AI_JUDGMENT_BOUNDARY.md已把 path-traversal 从 confirmed 表移入「故意留在
suspected」表。这正是文档 P0 建议「auto-confirm 分级」的落地,一次性消除该类
9/9 的自动确认误报。 - 未采用
[guards.path_traversal]配置:名称白名单只能表达「函数 F 校验值 V」,
覆盖不到「路径由谁拼出来」这一数据流事实(IDM 9 个误报里仅 2~3 个属于该形态),
却引入一个需要用户维护、配错就静默漏报的配置面,因此改为零配置的常量识别 +
交 AI 研判。
- 编译期常量路径识别(零配置):
- integer-overflow(CWE-190):无符号减法流入 Annex K / Windows
_s安全函数族
(sprintf_s/memcpy_s/strcpy_s等,apikb.SecureFunctions)的参数时不再报
unsigned_sub_underflow——这些函数在运行期校验容量/长度并以错误码返回,不会越界。
抑制范围仅限_s族:apikb.SafeFunctions还包含strlcpy/strlcat、
sqlite3_bind_blob、openat、exec*等「长度参数被无校验消费」的 API,其下溢仍是
真实缺陷,必须继续上报。 - unchecked-return(CWE-252):
- 函数指针遮蔽系统调用名:函数内以
read/write等名字声明了先于调用点的局部
对象(如int (*read)(void *, size_t, void *);)时,该调用不是 POSIX 系统调用,
不再上报。 - 间接防护改为基于被调函数入口守卫:
v作为实参传给一个在入口处拒绝该参数并
返回失败码的被调函数(if (p == NULL) return -1;),且调用结果被检查(内联判断,
或存入随后被检查的变量),则v的 unchecked-return 事件是误报。判定要求:
守卫分支必须返回失败哨兵(return 0;/ 裸return;不算)、极性正确
(if (p != NULL) ... else return -1;算,if (p != NULL) return 0;不算)。
仅凭「变量出现在条件里的某个调用实参中」不再抑制——if (strlen(p) > 0)是对
可能为 NULL 的指针解引用,仍然上报。
- 函数指针遮蔽系统调用名:函数内以
- 配置文档同步:
secguard config --help/--example与docs/config.md补齐 5 个
此前只存在于代码里的配置段(allocators/deallocators/ownership_transfer_calls/
nullability/integer_overflow)。docs/config.md是本仓库约定与
SKILL.md/vuln_type_skill.go/usage.go一起同步的配置面文档,此前一直缺失。
回归用例:path_traversal_constant_test.go(7 个常量形态抑制 + 6 个可影响形态上报)、
tc_unchecked_return_indirect_guard.c(含极性/成功返回/解引用三类反例)、
integer_overflow_safe_sink_test.go(sprintf_s 抑制 + malloc/strlcpy/
sqlite3_bind_blob 仍报)、filter_taint_source_test.go
TestTaintSourceFilter_PathTraversalNotAutoConfirmed(path-traversal 保持 suspected、
injection 仍为 confirmed)。未发布版本,VERSION 不变。
v0.9.4
[0.9.4] - 2026-10-12
null-deref void* 契约收敛漏报修正(ND-3 / RISK-2,CWE-476)
v0.9.4 首次发布的 ND-3 修复(commit d8410b4)引入 contractDerived dismiss 规则收敛 void* 契约解引用误报,但端到端验证发现一条漏报路径:当 accessor 调用(t = (T*)get_raw(p); t->f)的 get_raw 是可能返回 NULL 的普通函数时,contractDerived 仍追溯赋值 RHS 到 void* 契约参数并 dismiss,导致真缺陷被抑制。
根因:contractDerived 对 call_expression RHS 统一假设「函数名不在 allocator / known-nullable 列表 → 返回值非空 → 追溯参数到契约」,无法区分「框架保证非空的 accessor」与「可能返回 NULL 的普通函数」。
修正:contractDerived / derivedName / derivedExpr 新增 models *nullModel 参数;遇到 accessor call 时调用新增的 nullModel.hasExternalCallSource 检查赋值目标变量是否存在 external_call null source。若存在(分析器认为 accessor 可能返回 NULL)则不追溯、不 dismiss,surface as suspected 交 AI 研判;直接 cast 无 external_call source 则仍追溯,正确收敛。
端到端验证(本地 CLI + C 夹具):FP-1 direct cast / FP-3 type-agnostic alias 仍正确抑制;FP-2 accessor cast 改为 surface as suspected(交 AI);TP-1/2/3 仍正确检出;RISK-2(accessor 返回 NULL)从漏报修正为 candidate (suspected)。benchmark ND-10 从 no_finding 改为 finding,expected_final_certified 77 / expected_no_finding 65。
integer-overflow 子代理超时根治(CWE-190,紧急修复)
v0.9.3 生产冒烟(nlog src/ 全量扫描,1012 文件 / 7092 函数)中 integer-overflow 两个子代理均因 LLM 单次推理超时失败(total_tokens: 0 + Request timed out),117 条候选全部未落库判定。根因是 unsigned_sub_underflow 候选(98/117)只有 cat@ 类别标签、无任何数据流事实,子代理被迫逐候选打开 ±8 行源码找 guard 与操作数来源,单 turn 推理量过大导致 API 超时。本次从「收敛阶段用语义图算清事实」彻底闭环:
- interprocedural bounded-return summary(
evidence/bounded_return.go):给每个被索引函数计算「返回值 ≤ 某 capacity 参数」的截断包装器契约(snprintf_truncated_s/sprintf_s/vsprintf_s及转发到它们的一跳/多跳自定义包装器),带保守不动点 + 环保护。累加器模式length += add_*(buf, size - length)因此在检测阶段被证明有界并自动 dismissed,不再进入 AI 研判——这是 98 条buf_len - length类候选超时的直接原因。 - 配置驱动截断包装器白名单(
apikb.TruncatingWrapperCapArg+secguard.toml [integer_overflow.truncating_wrappers]):对定义在扫描树外的第三方截断包装器(如nlog_syslog_add_*),用户可声明name = <capacity 实参下标>一键闭环,与 memory-leak 的[ownership_transfer_calls]同款机制。 - 累加器容量实参校验(
boundedAccumulators):只认「total - length剩余空间 / 首次total全量」的严格形状,两个全量写(length += f(buf, 100)×2)不会被误判有界,杜绝把真溢出当误报漏掉。 - operand provenance Hint:
unsigned_sub_underflow的 Hint 从裸cat@升级为lhs@<kind> rhs@<kind> rhs-influenced sink@<kind> guard@none,AI 从_index.md一行即可判断被减数来源(rhs@param/rhs-influenced= 外部可控,真实风险;rhs@local无rhs-influenced= 内部有界累加器)。 - 入口 guard 自动消除(
planner/filter_unsigned_sub.go):if (consumed > total) return;早退 guard 证明减法安全,收敛阶段直接 dismissed。 - Fix Suggestion 按 category 细分:
unsigned_sub_underflow/size_calc_overflow/integer_overflow各自给出减法 guard、乘法检查、加法检查模板,不再统一塞 malloc 场景。 - 回退上一版「函数级 200 行 Code Context」为 ±8 窗口(保留
## Location一行函数签名):117 行_index.md≈ 8K token 本身不构成压力,真正会爆的是 106 个证据文件 × 200 行 ≈ 200K+ token,改为密集 Hint 承担判定,避免源码上下文炸弹。
测试:TestIntegerOverflow_MixedAccumulatorBoundedReturn、TestTruncatingWrapperCapArg_*、TestLoad_IntegerOverflowTruncatingWrappers、TestUnsignedSubGuard_*、TestBuildHint 等全绿。
重新发布(2026-10-12,厂商复测反馈):累加器真实代码形状补全
厂商用 v0.9.4 复测后反馈:unsigned_sub_underflow 的累加器误报(buf_size - len)仍有 ~80 条未消减。回源定位到 boundedAccumulators 只认「uint32_t len = 0;(声明初始化)+ len += f(...)」这一种形状,漏掉了真实代码里两种同样常见、同样有界的形状:
len = 0;赋值语句初始化:此前被当成「非累加写」而将len整体排除(disqualify),实际是安全复位,仍满足len <= total不变式。len = f(...)纯赋值首写(len = snprintf_truncated_s(buf, size, ...)后接len += f(...)):此前同样被排除,实际是「覆写为 callee 的有界返回值」,恒len <= total。
现 boundedAccumulators 区分「覆写写(= f(...),恒有界)」与「累加写(+= f(...) / = len + f(...),两个全量累加才可能超 total)」,并把 len = 0 识别为安全复位(重置增长、保持 total)。回归夹具 tc129 增加 fmt_buf2(赋值初始化)/fmt_buf3(纯赋值首写)两个形状,TestIntegerOverflow_MixedAccumulatorBoundedReturn 断言两者均不再发射候选。
重新发布(2026-10-13,第二轮生产审视闭环):跨函数误报消除 + 编排稳定性
对「0.9.4 版本扫描审视改进建议第二轮」逐条回源核实后的闭环修复:三处跨函数误报(其中一处为 high 级 memory-leak 误报)与两处编排稳定性问题,其余条目核实为已修复或误诊、未改代码。
- injection per-type 统计漏计(真缺陷):
status --per-type的written_count只按主 CWE(CWE-78)计数,规则 id 为CWE-117的日志注入 finding 落库后仍显示 0,导致编排器 F5 误判子代理失败并触发不必要重试。现ListPerTypeStatus按类型全量 CWE(canonical + legacy + category,新增planner.CWEsForType)求和并统一大小写归一化。测试TestListPerTypeStatus_MultiCWE。 - divide-by-zero 跨函数三元返回值漏分析(真缺陷,medium 误报):
returnSummaryResolver.exprInterval缺conditional_expression分支,nlog_get_clear_cycle_times()返回cond ? (2*SECONDS_FOR_8H) : (2*SECONDS_FOR_1MIN)被判「未知」而不过滤。现两臂皆非零才判非零(任一臂可能为零则保持保守)。测试TestDivideByZeroReview_TernaryReturnCall。 - dangerous-function 字符串宏参数未按常量过滤(真缺陷):
inet_addr(NLOG_HOST_IP)中NLOG_HOST_IP是字符串型宏,hasConstantArg只认直接字符串字面量与整数宏,导致本应安全(INADDR_NONE 混淆不存在)的调用被标 high。现新增buildGlobalStringMacros收集字符串型宏。测试TestDangerousFunction_StringMacroConstant。 - memory-leak 经 helper 的所有权转移漏识别(真缺陷,high 误报):
nlog_db_create_new_ctrl_item分配后经nlog_set_mon_disk_head(p)(内部g_head = p)注册到全局链表并返回缓存指针,调用方不持有所有权;但借出检测只认直接写全局,且函数名_new子串被零配置分配器启发式独立命中,被误报为 high 泄漏。现新增精确的跨函数「参数存入全局」摘要(globalStoreFuncs/storesParamToGlobal)+ borrow 集合(borrowFuncs)抑制命名启发式(isBorrowCall)。夹具tc134+ 测试TestMemoryLeak_BorrowViaHelperCall。 - 编排:批量容量估算改为二维(真缺陷):
batch_type_count × 12 < 54只按类型数估算,4 类型 × 12 = 48 通过校验但 86 候选 + 4 次 skill 加载实际溢出子代理,遗留 race-condition 未处理。现改为type_count × PER_TYPE_FIXED_TURNS(6) + candidate_count × PER_CANDIDATE_TURNS(0.8) ≥ 54即拆分(extension/shared/command-instructions.md)。 - 编排:遗留类型重派单而非放弃/顺序自分类(真缺陷):
written_count == 0(从未启动)的类型此前被 F1 标记 missing-type 或顺序自分类(墙钟 +3~5 分钟)。现 F1 优先派全新单类型子代理重试一次,禁止顺序自分类,仅二次仍空或子代理不可用才标 missing-type;并加terminal_state == "done"守卫避免把「全 dismissed」误当「未启动」。
v0.9.3
[0.9.3] - 2026-10-11
重新发布(2026-10-12):生产冒烟复核修复(12 项回源)
对 v0.9.3 生产冒烟报告(xlog 全量扫描,约 1012 文件 / 7092 函数 / 178358 行)的 12 项发现逐项回源复核:8 项含真实缺陷(其中"out-of-bounds 漏报"的漏洞判断为误诊、日志误报为真缺陷)已修复,4 项判定为误诊或能力空缺。
- uninit_variable 检测器 panic(P0,真缺陷):
outputParamInitializedVars(evidence/uninit_variable.go)在调用点无函数摘要(外部库函数)时仍读取summary.ParamWrites。命中"已知 out-param 函数 + 非输出参数取地址"形状(localtime_r(&t, &tm):&t不在outputParamInitializerArgs的输出下标集合内)即空指针崩溃,整个 uninit 检测器中断,候选被静默截断。根因定位方式:v0.9.3 日志只留一句nil pointer dereference、无行号,本地用一个 C 代码库当复现靶子跑出同样 panic,补打堆栈后定位。现摘要为零值时以零值FuncSummary兜底;确定性回归夹具tc131_uninit_addr_outparam.c+TestUninit_AddrOutParamNilSummaryPanic(修复前该测试必 panic,已验证)。detector panicked日志同时补打完整堆栈(debug.Stack()),避免同类崩溃再次不可定位。 - category filter 告警误报(真缺陷,日志层):
category filter dropped all seed events打印的是spec.Categories(本类型允许清单)而非真实被丢弃的 category,且对"种子事件被兄弟类型认领"这一正常路由也告警。真实会话中 out-of-bounds 丢弃写向 BUFFER_ACCESS 事件(归 buffer-overflow)被误读为"6 个 OOB 读取被类别过滤器一刀切"的漏报。现仅当被丢弃 category 无任何注册类型认领(真实漂移)才 WARN,并输出真实orphan_categories计数;正常跨类型路由降为 Debug,明细写入PipelineSummary.SeedRoutedByCategory供审计。测试TestUnroutedCategories。 - format-string(真缺陷 ×2):①
fprintf_s/vfprintf_s的格式串实参下标错为 0((FILE*, fmt, ...)应为 1),导致字面量格式串被当成非常量 sink 误报(实测fprintf_s(fp, "%d\n", 1)入选候选)。② Annex K_sprintf 变体位于SafeFunctions,被safe_function_exclude无条件丢弃——与 format-string 检测器刻意发射_s非常量格式串(CWE-134)自相矛盾,生产会话 36 个种子 100% 过滤为零。现格式串类别仅允许 curated 项目安全包装函数豁免,边界安全不再豁免格式串。回归tc132_format_string_annexk.c、TestFormatString_AnnexKFormatIndex、TestSafeFunctionFilter_FormatStringUnderscoreSNotDropped。 --write-json定位字段静默丢失(真缺陷):缺省或拼错(如filepath/line_number)的定位键解码为零值,静默落库为file_path=''/line=0且 exit 0。现拒绝空定位并指明期望键名,同时接受 DB 列名别名file_path/line_number/function_name(findingInput.normalizeAliases)。--review-json契约(真缺陷,轻):缺失/拼错必填字段时报entry N (finding M): missing required field "review_status" ...(含期望值与"拼错即为空"的提示),不再只报invalid review_status "";report --help补齐--review-json/--list-auto-confirmed/--type/--limit说明与必填字段清单。report --audit复核兜底(真缺陷):audit 前不校验status=auto-confirmed 且 review_status 为空的 finding;0 候选类型不派子代理时,该机器确认项会被静默带过。现审计信封新增unreviewed_auto_confirmed与warning(auditNoticeFields,不改变退出码契约)。- scan.log 缺终态(真缺陷,轻):日志止于 plan 阶段且随即关闭,扫描结束边界与退出状态均不可审计。现以命名返回值在退出时补记
scan finished+exit_code(含--fail-on门禁的 2)。 - report.md 计数口径(真缺陷,轻):per-type 表新增
Auto-confirmed列区分机器确认与 AI 确认;findings/文案更正为 "confirmed(pipeline auto-confirmed + AI)"。
复核后判定为误诊 / 已实现(未改动代码)
- memory-leak 收敛链漏掉真实泄漏(
xlog_db_delete_all_table:234):误诊。candidates/只承载首轮待 AI 研判的 suspected 线索;status=auto-confirmed的确定性发现按设计直接落 findings 表并进入二次复核。已按报告给出的形状构造最小复现:泄漏被检测器正确发射并 auto-confirm(xlog_db_create_new_ctrl_item分配 + NULL 检查 + 后续早退),无需修改leak_proof/ownership_transfer。 - out-of-bounds 6 个种子被类别过滤器丢弃 = 确定性漏报:误诊,见上。读写向 category 已分别路由到 out-of-bounds / buffer-overflow,实测两类均正确进入候选/auto-confirm。
.sgre/.tmp/残留未清理:误诊。secguard scan启动即RemoveAll+MkdirAll;残留的<type>-partN.json是 scan 结束后 AI 阶段写入的中间产物,由下一次 scan 清理,与文档一致。TaskCreate工具参数契约:误诊。项目源码与extension/shared/指令均无TaskCreate,文档要求用宿主 todo 列表跟踪类型进度。- 缺官方编排命令(
secguard orchestrate):确认是能力空缺而非缺陷;批次数/范围切分仍由编排器计算,本次未新增命令。
ds4.1 检视修复(40 项回源复核)
对 v0.9.2→HEAD 的 12 个提交做全量回源复核(docs/ds4.1检视sgre-20261019.md),逐项验证并修复真实缺陷。关键项:
- uninit 自动确认精确化(修订本节的 macro-context 方案):上一版以"候选前后 ±8 行出现大写宏"降级 confirmed→suspected,既误伤无关宏(
a + 1 + MYMACRO(0)的a被拖下水)又漏掉取字段地址(HTONBUF(&ipv6.hpfS6Addr32[0]))。根因是hasOutputParamWrite只认&裸标识符。现删除 ±8 窗口,改为精确识别&x/&x.f/&x[i]与x.f/x[i]退化传参(PP-06)。 - global_invariants:
nonZeroVars不再跨函数按裸名共享(PP-01 真除零漏报);静态零初值/BSS 计入"可能为零"(PP-05);复合赋值置allNonZero=false(PP-04)。 - divide-by-zero 常量折叠:
*/<<折叠带 32 位宽度溢出/移位量判断,(1u<<16)*(1u<<16)、1u<<32不再误判非零(EL-05/PP-03);sizeof_x前缀误判修复(PP-11)。 - memory-leak:容器逃逸仅当容器自身逃逸才成立(EL-01)、字段逃逸补
base==varName(EL-02)、getter-borrow 禁用命名启发式(EL-03)、typedef 标量识别(EL-06)、同名遮蔽 fail-open(EL-07)。 - agent-native re-review 链路:audit 计数以
FinalStatus为准(RV-01)、三条 re-review 指令对齐(RV-02)、--limit分批(RV-03)、--review-json校验 auto-confirmed(RV-04)。 - benchmark:Phase 15/16 行号修正、汇总计数同步、补 CLI 端到端 re-review 契约测试。
uninit 自动确认与 out-param 误报(CWE-457)
基于 nlog 生产扫描 candidates/uninit/_index.md(78 候选,5 个 certain-uninit macro-context 被 auto-confirm)诊断:
- macro-context 不再 auto-confirm:
HTONBUF/NTOHBUF/memcpy_s等 ALL_CAPS 宏展开的写入对流模型不可见,此前"每路径未初始化"被机器确认为 confirmed。现把macroContextDetector接入DefiniteInitFilter,macro-context 候选一律降级为 suspected 交 AI 研判(对齐 null-deref 的macro_uncertain)。消除 5 个确定性 auto-confirm 误报。 - POSIX 时间转换 out-param:
localtime_r/gmtime_r/localtime_s/gmtime_s/strptime加入outputParamInitializers,localtime_r(&t, &tm)后读tm.tm_year等字段不再被判为未初始化(此前与stat/gettimeofday同形却漏配)。消除 ~20 个local_tm/timeinfo/systime类误报。 - 回归夹具:
tc130_uninit_localtime_outparam.c;测试TestUninit_LocaltimeOutParam、TestUninit_MacroContextNotConfirmed。
memory-leak 标量状态码与 getter 借用误报(CWE-401)
生产 result.sarif 的 memory-leak 子集 34 条 auto-confirmed 中 21 条是确定性误报,两类根因:
- 标量状态码当指针:
bool ret = nlog_is_time_to_create_new(...)、uint32_t ret = nlog_disk_monitor_remalloc()、uint32_t resev2 = HpeDynmemAlloc(id, &pNewNode)(真分配是&pNewNodeout-param)——_new/remalloc/*_alloc名字启发式命中了返回状态码的函数,检测器未做类型校验。修复:findScalarVarsFrom仅当基础类型是内建标量(primitive_type/sized_type_specifier)才跳过整变量目标;typedef 基础类型(MsgPdu p=char **p)fail-open。 - getter 借用当分配:
nlog_db_get_ctrl_item/nlog_get_ai_detect_stat_list等 get-or-create-and-cache 访问器返回注册表/缓存的引用。修复:allocatorReturnFuncs不再把「返回前已逃逸(存入全局/静态/字段或交给容器)的指针」标为分配器包装器。 - 回归夹具:
tc131_memory_leak_scalar_status.c、tc132_memory_leak_get_or_create_borrow.c。
AI 复核 auto-confirmed 结果改为 agent 原生(移除外部 AI provider)
0.9.2 的 secguard review-autoconfirmed 走外部 HTTP AI([ai_review] 配置 + aireview),与「AI 就是 agent」的架构冲突且未接入编排,已删除(命令 + review/aireview 包 + [ai_review] 配置)。二次复核现折回 security-auditor subagent 自身的 per-type 分类循环,复用已加载的 skill:
secguard report --list-auto-confirmed [--type]列出机器 auto-confirmed finding(含id/rule_id/snippet);secguard report --review-json回写dismissed覆盖(dismissed经EffectiveStatus从result.sarif抑制)。- Bash 主机用两条命令;OpenCode-NGA(无 Bash)用新增的
secguard_reviewMCP 工具(list_auto_confirmed/reviews),已在 opencode + opencode-nga 双端注册并通过一致性检查。 VulnTypesToRuleIDs迁入planner。回归测试TestAutoConfirmedReReviewLoop。
v0.9.2
[0.9.2] - 2026-10-10
空运行检测器跳过(性能 P0-5)
sizeof_misuse/unchecked_return空运行跳过:启动前轻量预扫是否有触发形态(sizeof(标识符)处于 size 上下文 / 存在 unchecked-return API 或分配器调用),无则跳过昂贵的跨文件 typedef 表与 passthrough 修复点。预扫出错时 fail-open 照常运行完整检测,不引入漏报。
integer-overflow 截断累加器误报(CWE-190)
unsigned_sub_underflow截断累加器抑制(P1-2):size - count中count若只由截断式格式化包装器累加(count += snprintf_truncated_s(...)/count = count + sprintf_s(...)),则按契约count不超过size,该减法不会下溢,不再发unsigned_sub_underflow。仅匹配*truncated*(含 printf/sprintf)与精确sprintf_s/vsprintf_s,snprintf/snprintf_s(返回值可超界)不匹配,保持保守。回归夹具:tc128_int_overflow_snprintf_accumulator.c。
race-condition 锁封装函数识别(CWE-362)
- 纯 lock/unlock 封装函数对(P1-4):
xxx_lock()(函数体仅pthread_mutex_lock(&g_mutex))与xxx_unlock()视为与直接原语等价,位于两者之间的共享状态写不再被误报为 shared_data_race。仅当函数体只有 lock(或只有 unlock)调用、无其它调用时才识别为纯封装,关键段 helper 不算。回归夹具:tc129_race_lock_wrapper.c。
resource-leak 自动确认误报修复(CWE-404)
生产环境反馈 0.9.2 的 resource-leak 自动确认(auto-confirmed)包含大量确定性误报,是"几乎无法使用"的主要原因。本轮针对两大非泄漏模式去掉自动确认(仍保留为 suspected 交由 AI 研判,不丢召回):
- 锁配对(P0-1):
xxx_lock()内pthread_mutex_lock(&g_mutex)、同文件xxx_unlock()内pthread_mutex_unlock(&g_mutex)的标准封装函数对,以及pthread_rwlock_rdlock/wrlock配对,不再标记definite(跨函数解锁在本函数内不可证,不能 auto-confirm)。 - 全局/静态变量持有(P0-2):
g_handler = iconv_open(...)、g_handle = dlopen(...)、g_fd = epoll_create(...)及函数内static局部缓存等进程生命周期资源持有,不再标记definite。findLocalVarsFrom同步将static局部视为非局部(逃逸),消除 memory-leak / resource-leak 两侧的静态缓存误报。
divide-by-zero 跨文件常量除数(CWE-369)
- 跨文件
#define常量解析:x / NLOG_SECOND_PER_MINUTE这类除数宏定义在.h头文件时,此前每文件常量表看不到、被误报为"可能为零"。现改用buildGlobalConstants合并所有已索引文件(含头文件)的编译期常量(冲突值保守地降为"不确定"),头文件里的非零常量除数像字面量一样被丢弃。回归夹具:tc127_divide_by_zero_crossconst/。
生产内存分配模式识别(对齐 docs/req_内存分配释放典型性优化.md)
- 第三方 SDK 分配/释放名进入精确集合:
VOS_Malloc_F/VOS_Free_F、HpeMemAlloc/HpeMemFree、VOS_Mem_Allock_F/VOS_Mem_ReAllock_F/VOS_MemFree_F加入BuiltinAllocators/BuiltinDeallocators,使 null-deref 确认层与 double-free / UAF / unchecked-return 检测器像对待malloc/free一样精确对待这些跨仓库 SDK 宏,而非依赖 fail-open 命名启发式。 - "封装成函数"包装器识别(新增能力):识别"函数体返回分配器结果"的包装器(
p = malloc(n); if(!p) return NULL; memset_s(...); return p;),即使包装器名不含alloc/malloc子串,调用方的未释放结果也会被检出为泄漏(memory-leak)。这是 40+ 个仓库各自命名、但都"判空 + memset_s 初始化后返回指针"的共性形态。 - 回归夹具:
tc123_resleak_lock_pair.c、tc124_resleak_global_hold.c、tc125_prod_alloc_macros.c、tc126_passthrough_alloc_wrapper.c。
AI 复核 auto-confirmed 结果(新能力)
- 新增
secguard review-autoconfirmed命令:对 sgre 判定为auto-confirmed的 finding 做一次独立 AI 复核,降低 pipeline 自动确认的误报、恢复 AI 研判价值。支持--scan-id/--type/--limit/--dry-run/--status/--output-json。 - 复核结论复用现有
findings.review_status第二遍字段:false_positive映射为 dismissed(真正过滤)、ai_confirmed映射 confirmed、needs_more_evidence保守保留原 confirmed,并在result.sarif以review_status属性暴露供观测。 review_error(复核调用失败)只写日志、不落库、不进result.sarif:finding 保持未复核并在下次运行时自然重试,避免"复核失败却仍标 confirmed"误导用户。- 完整复核审计(supporting/contradicting/missing 证据数组、模型/provider、prompt/schema 版本、源码 revision、confidence、耗时)合并进
properties.review,与auto_confirm_evidence共存,不污染原始机器结论。 - 扫描时记录源码 revision(
scan_runs.source_revision),复核时校验 revision 一致性;payload 修复绝对路径解析与路径穿越防护。 - 新增
[ai_review]配置:provider/model/endpoint/api_key_env/timeout_ms/max_retries/budget_usd/cost_per_call_usd/prompt_version/schema_version。
v0.9.1
[0.9.1] - 2026-10-09
memory-leak 准确率收敛(CWE-401)
- definite 通道扩展到不可达释放:
hasLostResource证明泄漏路径后,即使存在free/escape/transfer行,只要这些行在 CFG 上从分配点不可达(无条件 return / goto 之后的死代码),仍判定为 definite leak 并 auto-confirm;条件分支内的释放保持 suspected(ML-01/18 扩展)。 - compound assignment 不再当作分配:
ret += CLI_NewDefineCmdElement(...)是算术运算而非存储分配,此前_new触发零配置分配器启发式导致 confirmed 误报。 - output-param escape/free 识别:
*param输出参数形态的分配逃逸(*root = malloc(...))与释放(*p的 free)被正确解包,消除跨函数所有权的漏报/误报。 - config 驱动
ownership_transfer_calls:新增[ownership_transfer_calls] names = [...]配置,项目自定义容器插入类函数(list_push_back/dict_set/buffer_append等)在 CLI 启动时注册进 apikb,指针传入即视为所有权逃逸而非泄漏,替代原有硬编码。
SKILL 对齐
extension/shared/skills/memory-leak/SKILL.md从通用方法论重写为对齐 detector 实际行为:真实事件(MEMORY_ALLOC / MEMORY_RELEASE)、definite 通道、confirmed/suspected/false-positive/dismissed 判定表,并补充别名释放、cast 释放、escape 变体、隐式分配器、self-realloc、goto cleanup、RAII 路径级等 false-positive 场景。
v0.9.0
[0.9.0] - 2026-10-08
出口前内存与空指针收敛
- uninit:字段写目标和取地址表达式不再被当成未初始化读取,修复
que_list->field = ...与&struct.field两类 pipeline-proved 误报。 - null-deref:三态 return-nullability 模型补齐直接调用结果路径(
f()->x/*f()/f()[i]),never-null 直接丢弃、known-nullable 自动确认;修复function_summary空文本列导致 planner 退回 fail-open 的精度降级。 - null-deref triage:
caller_null中仅“调用方实参未证明非空”的候选在 AI 前直接收敛;字面量NULL调用和带具体来源行的候选继续保留。 - null-deref guard/macro triage:短路判空条件内的解引用(
p == NULL || p->field)前置收敛;无具体 NULL 来源行的 macro-context 候选不再进入 AI。 - allocator/deallocator 语义:扩展零配置识别到
_new/_create/_dup/strdup与_destroy/_delete/_cleanup/_recycle;统一calloc/zmalloc/zalloczero-init 判定。 - memory-leak:deallocator 多参数只消费释放实参;
put/push/enqueue/register/insert/append/store/send/add等调用型所有权逃逸不再误报为泄漏。
切换到自维护 tree-sitter-c fork + 原生解析 GNU C 扩展
go.mod将github.com/tree-sitter/tree-sitter-c替换为github.com/DannyAn/tree-sitter-c v0.24.4(fork)。- 原生解析 GCC
typeof(typeof_specifier四种拼写),移除先前的 GCC typeof 预处理重写链。 - 补齐 6 类此前
HasError()=true的 GNU C 语法(B1-B6):- B1 变量声明后置
__attribute__(int g __attribute__((aligned(16))) = 0;) - B2
typedef级__attribute__(typedef int __attribute__((vector_size(16))) v4si;) - B3
enum级__attribute__(enum E __attribute__((packed)) { ... };) - B4 标签级
__attribute__(L: __attribute__((cold));) - B5 GNU case range(
case 1 ... 5:) - B6 computed goto(
void *p = &&L; goto *p;)
- B1 变量声明后置
sgre 检测层适配(22 处)
- v0.24.4 把
attribute_specifier插进init_declarator的 declarator 与 value 之间,原先用位置索引NamedChildren()[1]取 initializer 的 22 处检测/图/规划点会误取到 attribute,导致带__attribute__的初始化(malloc / 显式 NULL / 别名 / 常量源)被漏检。 - 新增
parser.Node.AssignParts():对init_declarator按字段名(declarator/value)取,对assignment_expression保持位置索引。 - 统一替换 22 处:null-source / memory-leak / resource-leak / double-free / uninit / unchecked-return / buffer-overflow 检测器,graph alias/interproc,planner uninit-flow/null-flow。
- 新增回归测试:
tc121_attribute_init.c+AssignParts单测,锁定char *lp __attribute__((aligned(16))) = malloc(10)等场景不再漏报。