Agentic Fuzzing 真正困难的不是生成输入,而是定义什么算 Bug
当大模型开始参与测试生成、代码分析、调试和漏洞验证后,自动化系统能够生成的测试数量正在迅速增加。
传统 Fuzzer 可能每秒执行数千甚至数百万个输入;LLM 驱动的测试系统虽然吞吐量更低,却能阅读源码、理解 API、组合调用序列,并主动构造具有语义的边界条件。进一步的 Agentic Fuzzing 不再只是让模型生成几个输入,而是让 Agent 自主完成假设提出、测试编写、程序执行、结果分析和漏洞报告。
表面上看,生成能力越强,发现漏洞就应该越容易。
实际情况恰好相反:当测试对象从解析器、媒体库等传统 native 程序,转向 Agent Framework、工作流编排框架和复杂 Python 软件时,真正限制自动漏洞发现能力的往往不再是“能否生成输入”,而是一个更基础的问题:
系统观察到的异常,究竟是不是实现 Bug?
Crash、异常、测试失败、行为差异,甚至另一个大模型给出的“这是 Bug”判断,都不能单独回答这个问题。
可靠的 Agentic Fuzzing 必须同时约束输入是否合法、调用方式是否真实、目标代码是否可达、行为是否违反规范、问题是否稳定复现,以及维护者是否认可缺陷成立。否则,Agent 只会以更高速度生成更多看似合理、实际无法信任的安全结论。
一、为什么传统 Fuzzer 偏爱 Crash Oracle
Fuzzing 要解决两个相互关联但并不相同的问题:
如何产生能够探索新状态的输入;
如何判断一次执行是否暴露了缺陷。
前一个问题属于输入生成和搜索策略,后一个问题属于 Test Oracle。
在传统 native fuzzing 中,Crash 是一种非常有吸引力的自动化信号。段错误、非法内存访问、Sanitizer 报告、断言失败或进程异常退出,通常具有三个优点。
第一,结果相对确定。同一个输入在相同环境中重复执行,往往能够产生相同的崩溃。
第二,检测成本低。Fuzzer 不需要理解程序的业务语义,只需要监控退出码、信号或 Sanitizer 输出。
第三,容易大规模自动化。测试、检测、去重和最小化都可以进入无人值守流水线。
这也是为什么 AFL++、libFuzzer 和 OSS-Fuzz 等系统可以持续运行:它们不需要在每次执行后理解“程序本来应该做什么”。
但即使在传统 fuzzing 中,Crash 也只是强候选信号,而不是完整漏洞结论。崩溃可能来自错误的 harness、违反前置条件的调用、资源耗尽、测试环境问题或不可利用的防御性断言。Crash Oracle 能说明“程序在当前输入下出现了异常执行”,却不能自动证明“这里存在具有安全影响的产品缺陷”。
进入 Agent Framework 后,这种差距进一步扩大。
二、在 Agent Framework 中,异常本身不再可靠
LangChain、LlamaIndex、CrewAI 等 Agent Framework 大量使用 Python、Pydantic、动态配置、回调、异步接口和协议式抽象。测试一个 API,不仅需要参数类型正确,还需要对象初始化顺序、运行状态、依赖注入和调用协议全部成立。
在这种环境中,同一个 ValueError、TypeError 或 KeyError,可能来自完全不同的原因:
Fuzzer 构造了一个不符合 Pydantic Schema 的对象;
测试代码遗漏了必需字段;
调用者违反了文档规定的生命周期;
Mock 对象没有实现框架所要求的协议;
框架没有正确处理一个本应合法的边界状态;
内部状态被错误更新,随后在另一个位置触发异常。
从外部观察,这些情况都可能表现为普通 Python Exception。
这意味着“出现异常”并不能直接区分 API misuse 和 implementation bug。相反,如果系统把所有异常都报告为漏洞,误报会迅速淹没真正的问题;如果系统把常见 Python 异常全部过滤掉,又可能同时过滤真实缺陷。
LogicHunter 的实验对这一问题给出了直接证据。该论文将所有失败直接视为 Bug 时,虽然召回率达到 100%,但精确率只有 2.80%;简单启发式过滤后的精确率也只有 5.82%,平均需要人工检查 17.24 个失败用例才能确认一个真实 Bug。
因此,在复杂 Python 框架中,“异常发生了”只是一个观察结果,而不是 Oracle 的最终答案。
三、比异常更困难的是静默语义错误
异常至少会留下可监控的信号。更棘手的是另一类问题:程序正常返回,类型也正确,但语义已经发生错误。
例如,一个工具注册函数可能返回成功,却没有真正更新注册表;一个 Memory 组件可能正常保存消息,却破坏原有的时间顺序;一个 Graph Store 可能接受重复写入,却没有执行文档承诺的去重;一个异步接口可能返回正确的数据结构,但回调被调用了两次;一个权限过滤器可能返回合法对象,却混入了调用者无权访问的数据。
这些问题通常具有以下共同特征:
没有 Crash;
没有未捕获异常;
返回类型符合声明;
基础单元测试可能通过;
代码覆盖率可能继续增长;
错误只体现在状态变化、调用顺序、数据流或跨步骤关系中。
LogicHunter 报告的一个案例正是这种情况:LlamaIndex 的 SimpleGraphStore.upsert_triplet 在存储时使用列表,在判断重复项时却使用元组进行比较,导致相同关系可以被重复插入。整个过程不抛出异常,只有检查重复调用后图中元素数量是否保持不变,才能暴露问题。
这类 Bug 说明,面对 Agent Framework,仅检查“程序有没有崩溃”或“函数有没有返回”远远不够。Oracle 必须观察行为关系,例如:
幂等性是否保持;
输入字段是否被无意修改;
状态转换是否符合预期;
同步和异步接口是否一致;
回调次数和顺序是否正确;
工具调用是否满足权限边界;
数据是否沿预期路径传播;
多次执行是否产生稳定结果。
这已经不是传统意义上的 Crash Detection,而是在运行时检验一组局部语义不变量。
四、LogicHunter:先生成合法输入,再主动调查异常
LogicHunter 的核心价值不只是“使用 Agent 做 Fuzzing”,而是把输入生成和 Oracle 问题放在同一个闭环中处理。
截至 2026 年 7 月,其公开版本仍是 arXiv v1 预印本,因此论文中的结果应视为作者报告、尚未经过正式同行评审的实验结论。论文针对 LangChain、LlamaIndex 和 CrewAI 三个框架进行了评估。
1. Valid-by-construction inputs
传统 mutation-based fuzzing 会产生大量类型错误、缺失字段或违反协议的调用。这些输入通常在进入核心逻辑前就被 Pydantic 或框架校验层拒绝。
但只满足类型约束同样不够。始终使用默认值、空字符串和最简单对象,虽然“合法”,却很难触及有价值的语义边界。
LogicHunter 因此组合了两类信息:
类型提示、Pydantic Schema 等形式约束;
文档和真实开源项目中的 API 使用方式。
系统首先构造能够正常初始化和执行的 seed,再在 API 使用层进行变异,例如改变边界参数、重排控制流、组合状态依赖或构造语义极端但仍然合法的输入。
目标不是生成任意输入,而是生成:
在类型和协议层面合法,但在语义层面具有挑战性的输入。
这种 valid-by-construction 思路能够减少大量无效失败,但它仍然只解决了“测试能否进入有效状态空间”的问题,并没有自动解决“失败是不是 Bug”。
2. Behavioral probes
LogicHunter 为测试生成局部行为断言,即 behavioral probe。
Probe 可以检查返回类型一致性、字段保持、幂等性或边界行为。例如:
store.upsert_triplet(subject, relation, obj)
store.upsert_triplet(subject, relation, obj)
assert count_triplets(store) == 1这个断言并不天然是真理。它是从文档、接口画像或代码语义中推导出来的待验证假设。
LogicHunter 明确区分了 probe 和最终 Oracle:probe 失败只代表发现了异常信号,不能直接作为 Bug 报告,之后仍需 Agentic Oracle 进行验证。
这种区分非常关键。很多 LLM 测试系统会把自己生成的断言失败直接计入“发现漏洞”,但如果断言本身误解了 API 语义,那么所谓漏洞只是模型对规范的错误猜测。
3. Agentic Oracle
Agentic Oracle 不只接收测试代码和错误日志,然后输出一个标签。它会主动执行调查:
查询当前版本文档;
搜索和阅读相关源码;
检查内部运行状态;
修改测试以区分不同假设;
进行额外的定向执行;
收集支持或否定 Bug 的证据;
最终给出判断和理由。
这与普通的 LLM Judge 有本质差别。
普通 LLM Judge 进行的是静态分类:
test + output + context→bug / non-bug\text{test + output + context} \rightarrow \text{bug / non-bug}test + output + context→bug / non-bug
Agentic Oracle 执行的是主动诊断:
提出假设→检索证据→执行实验→排除替代解释→形成结论\text{提出假设} \rightarrow \text{检索证据} \rightarrow \text{执行实验} \rightarrow \text{排除替代解释} \rightarrow \text{形成结论}提出假设→检索证据→执行实验→排除替代解释→形成结论
论文报告称,LogicHunter 共发现 40 个此前未知的问题,其中包括 8 个意外异常类问题和 32 个静默失败问题;30 个获得开发者确认,26 个已经修复。
这一结果同时说明两件事。
第一,主动 Oracle 确实比单纯依赖 Crash 或静态 LLM 分类更适合 Agent Framework。
第二,即使经过 Agentic Oracle,40 个候选中仍只有 30 个获得开发者确认。剩余候选可能是真实但尚未处理的问题,也可能存在规范歧义、维护者判断差异或证据不足。Agentic Oracle 显著提高了可信度,但它仍然不是形式化证明,也不能替代最终确认。
五、为什么让另一个 LLM 判断“是不是 Bug”仍然不够
一个直观方案是:由第一个 LLM 生成测试,再让第二个 LLM 判断失败是否代表 Bug。
这个方案能够过滤一些低级错误,但仍存在五类根本限制。
1. 模型可能使用过期知识
Agent Framework 的 API 变化很快。模型记忆中的参数、默认行为或生命周期约束,可能已经不适用于当前版本。
即使向模型提供文档,也不能保证文档与具体 commit 的实现完全一致。
2. 模型可能接受测试中的错误前提
测试代码可能假设一个函数具有幂等性,但文档并未作出这一保证;也可能假设调用顺序可以任意调整,而框架实际要求先初始化特定组件。
LLM Judge 若没有独立验证,很容易在测试自身错误的前提下继续推理。
3. 日志不足以恢复内部状态
很多语义错误只能通过检查对象字段、缓存内容、事件序列、调用参数或源码分支才能确认。仅凭最终异常栈或输出值无法完成归因。
4. 生成模型和判断模型可能共享相同偏差
即使使用两个模型,它们也可能对 API 产生相同误解。模型数量增加并不等于证据独立性增加。
5. 语言判断不能替代可执行证据
“这段代码看起来违反预期”只能形成假设。可靠报告还需要证明:
输入满足公开或隐含前置条件;
异常来自目标实现而非测试 harness;
问题在固定环境中可重复;
对照输入不会产生同样结果;
源码路径能够解释观察到的行为。
LogicHunter 的实验也显示,被动 LLM Judge 的精确率仍然有限。论文报告的 GPT-5.2 被动 Judge 精确率约为 29%,而 Agentic Oracle 报告的精确率为 91.17%、召回率为 72.14%,平均人工检查 1.10 个失败用例可确认一个真实 Bug。
需要注意,这一结果来自论文作者构建的评估集和实验设置,不能直接外推到所有软件项目。但它至少表明:模型能力更强,并不能消除主动取证和运行时验证的必要性。
六、一个更可靠的 Oracle 分层
Agentic Fuzzing 不应该把 Oracle 设计成一个简单的布尔函数:
is_bug(test_result) -> true / false更合理的设计是一条分层证据链。只有候选逐层通过,结论强度才逐渐提高。
第一层:语法与类型合法
检查生成代码能否解析、导入和执行,参数是否符合类型提示、Schema 和显式 Validator。
这一层排除明显的生成错误,例如:
不存在的参数;
错误字段类型;
缺少必填属性;
无法实例化的抽象类;
LLM 幻觉产生的 API。
通过这一层只说明测试“形式上可运行”,不说明调用方式合理。
第二层:API 协议合法
检查测试是否遵循框架要求的对象生命周期、初始化顺序、异步协议、上下文管理和依赖约束。
例如:
是否应先调用
initialize();是否必须在 event loop 中使用;
Mock 是否完整实现目标协议;
当前方法是否允许用户直接调用;
测试是否绕过了正常入口。
这一层决定候选是“有效 API 使用”,还是高级形式的 misuse。
第三层:真实入口可达
即使某个内部方法可以被人工调用,也不代表真实应用能够到达该状态。
需要证明:
状态可通过公开 API 构造;
参数组合出现在合理使用场景中;
不依赖任意篡改私有字段;
不需要违反产品部署假设;
上游组件确实能够产生相应输入。
真实入口可达性是区分测试技巧和实际缺陷的重要边界。对于安全漏洞,还应进一步说明攻击者是否能够控制相关输入,以及需要哪些权限和前置条件。
第四层:行为与规范冲突
这一层要求提供行为异常所违反的依据。
依据可以来自:
明确的 API 文档;
类型或 Schema 中表达的不变量;
官方示例;
测试套件中的预期行为;
同步与异步实现之间的差分关系;
幂等性、单调性或状态保持等可解释属性;
源码中的注释、检查和控制流意图。
规范来源可能相互冲突。例如文档与实现不一致时,不能自动断言实现一定错误,也可能是文档 Bug。报告必须保留这种不确定性,并说明当前证据支持哪一种解释。
第五层:最小复现稳定
候选应当被压缩为尽可能小的 PoC,并在固定版本和环境中重复执行。
至少需要记录:
目标 commit 或 release;
Python 和依赖版本;
环境配置;
完整调用序列;
预期行为;
实际行为;
重复次数和成功复现次数;
对照实验;
是否存在并发或外部服务依赖。
一次偶然失败不能支撑高置信度结论。对于非确定性 Agent 工作流,还应报告复现概率,而不是只展示一次成功日志。
第六层:人工或上游确认
人工审核需要检查前面各层证据是否完整。上游维护者确认、Issue 接受、补丁合并或安全公告可以进一步提高结论可信度。
但这些信号也不能简单等同:
提交 Issue 不等于确认;
标记为 Bug 不等于安全漏洞;
合并修复不等于认可全部攻击叙事;
获得 CVE 不等于证明可利用性;
未获得回复也不等于候选为误报。
因此,最终结果应按证据等级报告,而不是把所有候选都称为“发现漏洞”。
一种可执行的分类方式是:
Generated Case
↓
Valid Test
↓
Reachable Anomaly
↓
Specification Violation
↓
Stable Reproducer
↓
Human-Validated Bug
↓
Developer-Confirmed Bug
↓
Security-Relevant Vulnerability每向下一层移动,都需要新的证据,不能依赖模型置信度自动升级。
七、Agentic Fuzzing 论文应该报告什么
生成 10 万个测试、触发 5000 个异常,或者由 LLM 标记出 300 个“疑似漏洞”,都不能单独证明系统有效。
一篇可信的 Agentic Fuzzing 论文至少应报告以下指标。
1. 生成规模
包括生成测试数、实际执行数、执行成功数和有效输入比例。
这反映生成器是否真正理解目标接口,而不是仅仅制造大量不可运行代码。
2. 候选异常数量
应区分:
Crash;
未捕获异常;
Probe 失败;
差分不一致;
状态不变量违反;
模型判断异常。
不同信号的证据强度不同,不能合并为一个笼统的“Bug candidates”。
3. 去重后的独立候选
需要说明去重依据是异常栈、根因、受影响 API、补丁位置还是开发者可见行为。
多个输入触发同一个根因,不能被重复计数为多个漏洞。
4. 验证后真实 Bug
至少分别报告:
人工确认;
上游确认;
已修复;
重复问题;
预期行为;
无法复现;
尚未判定。
“提交了多少 Issue”不能代替“确认了多少 Bug”。
5. Precision、False Positive Rate 和 Recall
Precision 决定人工审核负担。对于需要安全研究人员逐个检查的系统,即使召回率很高,低精确率也可能使系统无法使用。
Recall 更难评估,因为真实未知 Bug 集合通常不存在。论文可以使用历史已修复 Bug、mutation benchmark、人工构建缺陷或后验开发者确认,但必须明确每种基准的局限。
6. 无法判定比例
复杂 Agent Framework 中会有大量规范不清、依赖外部服务或需要完整应用环境的候选。
将这些样本强制标记为 Bug 或 non-Bug 会产生虚假确定性。更合理的分类应包含 unknown 或 insufficient evidence。
7. 单位 Bug 验证成本
至少应统计:
LLM Token 或 API 成本;
Agent 执行时间;
机器运行时间;
人工审核时间;
每个确认 Bug 所需检查的候选数;
最小化和复现成本。
PBFuzz 等 Agentic Directed Fuzzing 工作已经开始报告单位漏洞 API 成本,这比只报告测试数量更接近系统的实际部署价值。
8. 从候选到最终结论的完整漏斗
论文应给出类似下面的结果链:
100,000 generated tests
18,000 executable tests
7,500 protocol-valid tests
1,200 unique anomalies
160 oracle-positive candidates
48 stable reproducers
31 human-confirmed bugs
24 developer-confirmed bugs
8 security-relevant vulnerabilities这个漏斗比“发现 160 个潜在漏洞”更真实,也能暴露系统的主要损耗究竟发生在输入生成、运行验证还是人工确认阶段。
八、Oracle 本身也需要被评估
Agentic Oracle 不是评估体系之外的裁判,而是系统中另一个可能出错的组件。
因此,需要单独评估:
Oracle Precision;
Oracle Recall;
不同运行之间的一致性;
对文档版本变化的敏感性;
去掉源码访问后的性能;
去掉运行时检查后的性能;
不同基础模型下的稳定性;
对 hard negative cases 的误报率;
是否会被测试代码或目标仓库中的文本诱导。
特别是在 Agent 环境中,Oracle 读取的文档、源码注释、Issue 和示例本身可能错误、过期,甚至具有对抗性。一个依赖仓库上下文进行判断的 Agentic Oracle,也可能遭遇 prompt injection 或 context poisoning。
因此,更成熟的设计应区分:
非可信输入:测试内容、异常信息、仓库文档和注释;
半可信依据:当前版本官方文档和已有测试;
强证据:可重复运行结果、源码控制流和最小对照实验;
外部确认:维护者回复、补丁和安全公告。
Oracle 越依赖自然语言推理,就越需要独立的可执行证据进行约束。
九、自动化生成越强,验证层越不能被省略
Agentic Fuzzing 的进步,使测试系统从随机扰动器逐渐变成了自主研究者。
Agent 可以阅读历史漏洞,理解根因,在大型代码库中寻找变体,编写 PoC,并通过调试反馈持续修正假设。AFuzz 已经展示了这种方法在复杂 JavaScript 引擎逻辑 Bug 上的潜力:它不再局限于围绕原始 PoC 进行局部变异,而是尝试理解历史 Bug 的高层根因,再寻找具有不同触发路径的同类问题。
但推理能力越强,系统就越容易生成听起来完整的漏洞故事:
它可以解释为什么代码看起来危险;
可以构造一个能够失败的测试;
可以生成攻击场景;
可以撰写具有说服力的漏洞报告。
这些能力都不能自动保证结论正确。
测试生成解决的是探索问题,Oracle 解决的是认识论问题:我们凭什么相信这个现象构成 Bug?
在 Agent Framework 和复杂软件中,可靠答案通常来自多种证据的组合:
类型和 Schema 证明输入在形式上合法;
真实调用方式证明场景不是人为误用;
运行状态证明异常确实发生;
源码语义解释异常产生的根因;
Behavioral Probe 暴露静默错误;
最小复现和对照实验排除偶然因素;
人工或上游确认完成最终归因。
其中任何单一信号都可能出错。真正可靠的 Oracle 不是某个更强的分类模型,而是一条能够被检查、重复和反驳的证据链。
结语
Agentic Fuzzing 真正困难的不是让模型生成更多输入,而是建立一个足够可靠的判断体系,区分:
无效测试与合法边界输入;
API misuse 与实现缺陷;
可疑行为与规范冲突;
一次性失败与稳定 Bug;
普通软件缺陷与安全漏洞;
模型判断与可验证事实。
Crash 很便宜,但覆盖不了静默语义错误;异常很明显,但无法区分误用和内部缺陷;LLM 能够解释,但解释不等于证据;Agent 可以主动调查,但仍需要确定性检查和人工确认。
自动化生成能力越强,Oracle 质量越重要。
没有可靠的验证层,Agentic Fuzzing 不会自动带来更多可信漏洞,只会更快地产生更多需要人工重新调查的结论。真正有价值的系统,不是声称发现最多 Bug 的系统,而是能够清楚说明每个结论经过了哪些验证、还存在哪些不确定性,以及证据足以支持到什么程度的系统。