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

Go fuzzing 怎样让输入先通过格式预检查

来源:17golang原创

时间:2026-10-18 23:03:13 381浏览 收藏

在 Go fuzz target 的回调入口先检查长度、固定头部或字符范围,不满足本目标前提时用 t.Skip 或直接返回,再对通过的输入执行解码和较昂贵的性质断言。预检查不会让 fuzz 引擎只生成合法输入,而是让不适用的输入尽早结束;它也不应该把本来要测试的解析错误挡在被测函数之外。

官方说明:https://go.dev/doc/security/fuzz/。testing 的跳过规则见 https://go.dev/src/testing/testing.go。下面比较三种策略,示例只限定输入筛选与单次测试成本,不把执行量当成覆盖率或性能结论。

先限定昂贵断言的输入域

例如,目标性质是“合法十六进制文本解码、规范化之后,内容摘要保持一致”。奇数长度或包含非十六进制字符的字符串,并不满足该性质的前提。让它们继续做摘要或业务对象构造没有意义,宜先筛掉。

但如果目标是“解码器面对任意文本应正确拒绝非法格式且不崩溃”,非法输入就是测试主体,不能过滤。先把这两个目标分开命名,比在同一个回调中到处遇错 return 更清楚。

比较直接返回、Skip与构造输入

return 适合结束当前回调,不需要额外说明;t.Skip 明确表达该输入不属于当前目标的有效域,适合排查种子为何未进入断言。二者都不是修复数据,也不能代替发现错误时的 t.Fatal。不要先标记失败,再期待 Skip 将失败抹掉。

第三种策略是 fuzz 原始字节,再通过 hex.EncodeToString 构造合法文本。它适合专测合法数据后的业务性质,能避免反复生成坏格式;代价是只探索构造器可表达的文本形式,例如统一小写,不能覆盖各种大小写、空白或畸形文本。本文选择字符级筛选,保留输入文本本身的变化。

用轻量格式检查约束目标

在现有 Go 模块中新建 hexcheck_test.go,写入下面的完整示例。长度上限 4096 是本例成本预算,不代表标准库只允许这么长。空串符合偶数长度和字符条件,必须保留,不能顺手把空输入当作非法输入跳过。

package hexcheck

import (
    "crypto/sha256"
    "encoding/hex"
    "strings"
    "testing"
)

// 小样本上限属于本测试的成本预算,不是十六进制格式的通用限制。
const maxHexLen = 4096

func isHexByte(c byte) bool {
    return c >= '0' && c = 'a' && c = 'A' && c  maxHexLen || len(s)%2 != 0 { return false }
    for i := 0; i 

检查器先判断字节长度,再检查 ASCII 字符,避免为了筛选而提前完整解码、分配大型对象。进入有效域后,如果 DecodeString 返回错误,就应失败,而不是再次 Skip;否则预检查与解码器之间的分歧会被藏起来。摘要只是一个可复制的性质示例,真实业务可替换为确定性的业务断言。

合法输入目标中的格式约束与属性检查静态依赖
图1:静态结构说明图。输入约束包含长度和字符规则,合法域目标依赖这些条件;连线表示依赖而非执行步骤,不是运行截图。

把预检查成本和有效样本分开看

该筛选最多扫描预算内的字符串;只有格式通过后才分配解码结果。它仍有扫描开销,如果后续计算本来就很便宜,重复实现格式规则未必划算。对于 JSON、复杂协议或压缩格式,另写一个接近完整解析器的预检查器尤其容易成本倒挂,可优先用已有解析结果决定后续性质是否适用。

大量合法种子不代表变异输入都会合法,f.Add 只是起点。判断筛选是否值得时,应分别观察单次筛选成本、后续断言成本,以及进入断言的样本是否足够多。fuzz 的 execs 不是“执行了昂贵断言的次数”。本文没有执行压测,不提供虚构的加速倍数或跳过比例。

保留不应被筛掉的坏输入

下面增加独立的拒绝路径目标。它只设置长度预算,不在调用解码器之前排除坏格式。十六进制语法简单,因此用字符规则建立接受性期望;复杂格式不宜复制整套生产解析逻辑作为所谓独立判断。

// 与上面的代码放在同一个 _test.go 文件中,复用导入和常量。
func FuzzHexReject(f *testing.F) {
    for _, s := range []string{"0", "gg", "0x12", "a b"} { f.Add(s) }
    f.Fuzz(func(t *testing.T, s string) {
        // 此目标只限制测试预算,不按格式预先跳过。
        if len(s) > maxHexLen { t.Skip("超过本目标预算") }
        wantValid := len(s)%2 == 0
        for i := 0; i 

FuzzHexDigest 负责合法输入性质;FuzzHexReject 负责预算内原始输入的接受性。两者共享 maxHexLen 和字符定义,但不共享“坏格式立即跳过”的策略。这个组合依然不测试超过 4096 字节的数据,若大输入属于产品风险,应另设有明确资源预算的测试,而不是声称已经全覆盖。

合法性质目标与原始输入接受性目标的覆盖边界
图2:静态边界说明图。两个目标分别依赖性质断言和接受性判断,共享长度预算;分组不表示先后,也不代表实测覆盖结果。

按目标选择筛选方式

目标选择主要代价
合法格式后的昂贵性质轻量检查加 Skip无效变异仍会消耗调用成本
只关心合法载荷业务计算原始字节加编码构造文本形式受到构造器限制
解析器拒绝行为不按格式筛选需要单独控制长度和计算预算
预检查几乎等于完整解析复用解析结果需明确解析错误是否属于预期域外

以下命令供读者在自己的模块包目录运行。普通 go test 可运行种子,持续 fuzz 使用精确匹配选择一个目标;分别运行两个目标,不把同一次执行当作同时变异两个目标。本文未执行命令。

# 先运行这两个目标已有的种子,不启动持续变异。
go test -run '^FuzzHex(Digest|Reject)$' -count=1 .
# 只对合法性质目标做有限时长变异。
go test -run '^$' -fuzz '^FuzzHexDigest$' -fuzztime=20s .
# 单独探索原始文本的接受性。
go test -run '^$' -fuzz '^FuzzHexReject$' -fuzztime=20s .

预检查失败应该用 Fatal 吗?

如果它只是说明输入不满足该性质前提,应 Skip 或结束回调;若检查的是被测实现必须履行的承诺,则应报错,不能跳过。

能用 recover 忽略所有 panic 吗?

不应这样做。把真实崩溃转成跳过会掩盖缺陷。筛选条件应来自目标的输入契约,而不是失败现象。

只加合法种子就能取消预检查吗?

不能据此假设后续输入都合法。要么保留适当筛选,要么用构造器明确控制格式域,并另测原始坏输入。

选择规则很简单:先问正在测试“合法输入后的性质”还是“格式接受与拒绝”,再决定是否筛选。省掉无意义的昂贵断言是合理目标,省掉需要发现的失败不是。

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