把模糊测试发现的输入固化为长期回归用例
来源:17golang原创
时间:2026-10-07 11:33:05 174浏览 收藏
我在 Go fuzz 测试里遇到过一种很容易被忽略的“修好”:模糊测试找到了一个输入,代码修复后本地通过了,但下一次重构时这个输入又把同一个边界打出来。真正可靠的做法,是把失败输入保存在仓库的 testdata/fuzz/FuzzXxx 目录中,让它成为 seed corpus。这样,普通的 go test 也会再次运行它,而不是只依赖一次 fuzz 进程里的缓存。
- 失败输入被最小化后,优先保存到对应 fuzz 测试的
testdata/fuzz/FuzzXxx。 f.Add适合少量稳定种子;文件形式适合提交较长或二进制的 corpus。$GOCACHE/fuzz是运行期间的 generated corpus,不等同于应该提交到 Git 的回归用例。
从失败输入到可提交的 seed corpus
Go 原生 fuzzing 会尝试把失败输入缩小到仍能复现问题的形式,并在输出中给出保存位置和定向重放命令。看到 Failing input written to testdata/fuzz/FuzzParseQuery/... 后,不要只复制最后一行字符串到工单里:这个文件本身就是 fuzz 测试的 corpus 条目,应该和修复一起提交。
目录名必须对应 fuzz 函数名。下面的示例假设测试函数叫 FuzzParseQuery,包目录下的结构应保持稳定:
package parser_test
// 该命令只定向重放已经保存的失败样本,便于确认修复是否生效。
// 参数末尾的哈希目录名来自 Go fuzz 输出,不能随意改写。
go test -run=FuzzParseQuery/a878c3134fe0404d44eb1e662e5d8d4a24beb05c3d68354903670ff65513ff49
// 修复后把 corpus 文件与代码一起提交;普通 go test 会默认运行它。
// 这里的路径是相对当前 Go 包目录,而不是 $GOCACHE/fuzz。
testdata/fuzz/FuzzParseQuery/
└── a878c3134fe0404d44eb1e662e5d8d4a24beb05c3d68354903670ff65513ff49

文件内容采用 Go fuzz corpus 格式,首行声明格式版本,后面按 fuzz 参数顺序保存值。不要手工把它改成普通 JSON;如果参数是 []byte、整数或字符串,文件里的类型和值必须与 fuzz target 的参数完全对应。
让 fuzz 目标适合长期回归
持久化样本只是第一步,fuzz target 也要足够稳定。官方规则要求函数形如 FuzzXxx(*testing.F),并且每个 fuzz 测试只有一个 f.Fuzz 目标。参数类型要使用 Go 支持的基本类型,seed corpus 的类型和顺序必须与它们一致。
例如,下面的目标用 URL 查询字符串展示“解析后再编码仍保持语义”的不变量。不能解析的随机字符串被跳过,真正能进入业务语义的输入才参与比较:
func FuzzParseQuery(f *testing.F) {
// 先放一个稳定种子,帮助普通测试和 fuzz 从有效输入开始。
f.Add("user=go&lang=zh")
// fuzz target 要快速、可重复,不把结果写入全局状态。
f.Fuzz(func(t *testing.T, raw string) {
values, err := url.ParseQuery(raw)
if err != nil {
// 无法进入本例业务语义的输入不作为失败样本。
t.Skip()
}
encoded := values.Encode()
roundTrip, err := url.ParseQuery(encoded)
if err != nil {
// 编码后的结果本应可再次解析,错误说明不变量被破坏。
t.Fatalf("重新解析编码结果失败: %v", err)
}
if !reflect.DeepEqual(values, roundTrip) {
// 失败信息要能帮助后续定位保存下来的最小输入。
t.Fatalf("往返解析结果不同: before=%v after=%v", values, roundTrip)
}
})
}
如果失败样本是短字符串,可以用 f.Add 表达;如果样本较长、含二进制字节,或需要保留 fuzz 引擎生成的精确编码,文件形式更容易审查和回放。两种形式都会被当作 seed corpus,区别只是一个写在测试代码里,一个写在 testdata/fuzz/FuzzXxx 里。
用普通测试和定向重放验证持久化结果
修复代码后,先使用输出里的定向命令重放原始失败样本。定向命令成功,只能说明这个输入当前不再触发失败;接着还要运行普通 go test,确认仓库中的 seed corpus 能在日常测试入口被执行。
# 先重放单个失败样本,快速确认修复覆盖了原始边界。
# 中文注释说明该命令只筛选一个 FuzzXxx/哈希测试用例。
go test -run='FuzzParseQuery/a878c3134fe0404d44eb1e662e5d8d4a24beb05c3d68354903670ff65513ff49'
# 再运行包内普通测试;f.Add 和 testdata/fuzz 下的 seed corpus 都会参与。
# 不加 -fuzz 时不会持续生成随机输入,但会检查已有的回归样本。
go test
# 需要继续探索新路径时再开启 fuzz,并限制时长避免占用机器不退出。
# -fuzztime 只控制本次探索,不决定样本是否进入仓库。
go test -fuzz=FuzzParseQuery -fuzztime=30s

这里最容易混淆的是 $GOCACHE/fuzz。fuzz 运行时会把能扩展覆盖率的输入放进 generated corpus,以便当前探索继续推进;它是缓存,不是团队审查过的回归契约。真正需要长期保留的,是能说明 bug 边界、已经最小化并能被普通测试重放的 seed corpus。
常见边界与排查方式
- 定向重放找不到样本:先确认命令使用的是同一个包目录、同一个
FuzzXxx名称,以及输出中的完整哈希。 - 普通
go test仍失败:不要删除 corpus 来“恢复绿色”,先判断失败是修复遗漏、测试不变量不成立,还是样本类型与参数不一致。 - 只在本机能复现:检查样本是否误留在
$GOCACHE/fuzz;未提交的生成缓存不能替代仓库中的testdata/fuzz。 - 测试偶尔抖动:移除对时间、随机数、全局可变状态和外部服务的依赖,让同一个输入每次得到相同的判定。
相关问题
f.Add 和 testdata/fuzz/FuzzXxx 可以同时使用吗? 可以。两者都是 seed corpus,前者适合少量可读初始样本,后者适合需要独立保存的失败输入或二进制样本。
生成 corpus 是否应该提交到 Git? 通常不应该。生成 corpus 位于 $GOCACHE/fuzz,服务于 fuzz 运行期的覆盖率探索;只有经过确认的失败输入或业务边界,才应整理为仓库中的 seed corpus。
-
860 收藏
-
843 收藏
-
826 收藏
-
809 收藏
-
792 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习