登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

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 中执行。区别在于,种子是你用来引导初始覆盖范围的输入,失败语料则是已经暴露出问题、应该被长期保留的回归样本。不要把所有随机输入都手工复制进代码,真正有价值的是能覆盖边界或复现缺陷的少量样本。

Go fuzzing 中 F.Add、testdata 语料和失败输入文件的静态关系
图1:Go fuzzing 的两类语料来源与默认测试入口的静态说明图,不是运行截图或执行证据。

用 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;在通常可写的项目中,优先以测试输出给出的项目内路径为准。

Go fuzzing 失败语料文件、Fuzz target 和 go test 复现命令的静态关系
图2:失败语料的编码内容、fuzz target 参数和复现命令之间的静态说明图,不是运行截图或执行证据。

不带 -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 不保留语料吗?

可以临时探索,但不适合作为回归流程。要让问题在后续构建和提交中持续可见,应保留失败语料或等价的稳定种子。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>