Go fuzzing 怎样用语料库复现输入
来源:17golang原创
时间:2026-10-20 08:05:40 194浏览 收藏
Go 原生 fuzzing 的关键不是把每次随机输入都保存下来,而是把有价值的输入变成可重复运行的语料。固定样本可以通过 f.Add 注册,也可以放进 testdata/fuzz/;当 fuzzing 找到失败输入时,Go 会把它写回同一个语料目录。这样,之后直接运行 go test 就能重新执行这条回归样本,调试时还可以用 -run 精确定位它。
官方文档:https://go.dev/doc/security/fuzz/
testing 包文档:https://pkg.go.dev/testing#F.Add
想复现一次 fuzzing 失败,先看输出中的 FuzzTest 名称和哈希后缀;想让问题长期留在回归测试里,就保留生成的语料文件,并用不带-fuzz的go test重跑。
先把种子语料和失败语料分开看
Go 的 fuzz test 至少有两类输入来源。第一类是开发者主动提供的种子:可以在调用 f.Fuzz 之前用 f.Add 注册,也可以把文件放入 testdata/fuzz/FuzzXxx。第二类是 fuzzing 过程中发现的失败输入,测试引擎会把它保存为新的语料文件。
两者都会在默认的 go test 中执行。区别在于,种子是你用来引导初始覆盖范围的输入,失败语料则是已经暴露出问题、应该被长期保留的回归样本。不要把所有随机输入都手工复制进代码,真正有价值的是能覆盖边界或复现缺陷的少量样本。

用 F.Add 注册一组稳定的初始输入
F.Add 必须写在 F.Fuzz 之前,传入的参数类型要和 fuzz target 的参数一一对应。下面的例子用字符串覆盖空值、普通文本、包含分隔符的文本和多字节字符;这些输入不是为了模拟所有随机情况,而是为了让每次默认测试都有稳定的起点。
func FuzzNormalize(f *testing.F) {
// 种子覆盖空值、分隔符和多字节文本,帮助快速触达边界。
f.Add("")
f.Add(" go-fuzz ")
f.Add("a,b,c")
f.Add("中文输入")
f.Fuzz(func(t *testing.T, input string) {
// fuzz target 要保持快速、确定,并只使用当前输入。
got := normalize(input)
if strings.Contains(got, "\\n") {
// 失败会让当前输入进入可复现的失败语料路径。
t.Fatalf("结果仍含换行符: %q", got)
}
})
}
如果 target 接收的是 []byte、int 或多个参数,f.Add 也必须用完全相同的参数类型和顺序。类型不匹配时,问题应在编写测试时直接修正,不要试图用字符串化方式绕过 fuzz target 的签名。
找到失败输入后看哪个目录
启用 fuzzing 时,如果 target 因某组输入失败,测试输出会给出类似 testdata/fuzz/FuzzNormalize/ 的路径。文件第一行是 go test fuzz v1,后续每一行对应 fuzz target 的一个参数。它是 Go 的语料编码,不是普通日志,也不应该把注释写进文件。
例如一个只接收字符串的语料文件可能是:
go test fuzz v1
string("异常输入")
如果 target 接收 []byte 和 int64,文件中的两条值就必须按同样顺序出现。官方文档还说明,若项目目录不可写,失败输入可能暂时写入构建缓存中的 fuzz cache;在通常可写的项目中,优先以测试输出给出的项目内路径为准。

不带 -fuzz 也能把失败输入跑起来
开发者第一次看到失败输出时,容易以为必须重新运行一轮随机 fuzzing。其实,失败语料保存后,默认的 go test 会把它当作种子执行。这个特性让发现问题和回归验证分开:第一次用 -fuzz 找到输入,后续普通测试负责稳定地确认修复没有回退。
# 先只执行已登记的单元测试、种子语料和失败语料。 go test ./... # 需要继续探索时,才指定一个 fuzz test,并限制本次探索时间。 go test -fuzz=FuzzNormalize -fuzztime=30s
如果项目里有多个 fuzz test,可以先用普通的 go test 确认全部语料,再用 -fuzz=FuzzNormalize 选择目标。-fuzztime 只限制 fuzzing 探索阶段的时间,不改变已有语料的含义。
用 -run 精确复现一条语料
当一个 fuzz test 的语料很多,或只有某一条输入触发问题时,可以从失败输出复制完整的子测试标识。Go 官方示例使用 go test -run=FuzzFoo/9ddb952d9814 运行单条 seed corpus 输入。实际使用时,把 FuzzFoo 换成你的 fuzz test 名称,把后面的哈希换成输出中的实际标识。
# 只复现 FuzzNormalize 下的一条失败语料。 go test -run='^FuzzNormalize/9ddb952d9814$' # 只运行该 fuzz test 的全部种子,不开启随机探索。 go test -run='^FuzzNormalize$'
这里的 -run 是筛选已经存在的测试和子测试;它不会生成新的随机输入。若哈希或测试名称复制错误,命令可能匹配不到目标,先回到原始失败日志重新复制完整标识。
语料库维护时记住三个边界
| 问题 | 应该怎么做 | 原因 |
|---|---|---|
| 固定样本放哪里 | 少量可读样本用 f.Add,较大的或长期维护的样本放 testdata/fuzz/FuzzXxx | 两者都会进入默认种子集合,但文件更适合管理较大输入 |
| 失败输入要不要删除 | 修复问题前保留;修复后把它作为回归样本继续运行 | 它记录了曾经触发缺陷的真实边界 |
| 如何缩小调试范围 | 从失败输出复制 FuzzXxx/哈希,传给 go test -run | 只执行目标语料,不重新进行随机探索 |
最后再强调一次:种子语料应该小而有代表性,fuzz target 应该快速、确定且不依赖共享可变状态。语料库的价值是让问题可以重复出现,而不是用文件数量制造覆盖率幻觉。
常见问题
失败语料必须手动转换成 f.Add 吗?
不需要。Go 会按语料文件格式读取它;只有当你确实想把小样本直接写在测试代码里时,才把它改写成 f.Add。
为什么普通 go test 也会失败?
因为失败语料已经成为该 fuzz test 的默认种子。修复代码后,普通测试应当通过;如果仍失败,说明这个输入对应的缺陷还没有处理。
可以只用 -fuzz 不保留语料吗?
可以临时探索,但不适合作为回归流程。要让问题在后续构建和提交中持续可见,应保留失败语料或等价的稳定种子。
-
860 收藏
-
843 收藏
-
826 收藏
-
809 收藏
-
792 收藏
-
269 收藏
-
222 收藏
-
341 收藏
-
373 收藏
-
334 收藏
-
242 收藏
-
148 收藏
-
367 收藏
-
387 收藏
-
258 收藏
-
302 收藏
-
160 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习