前言

golang1.18发布了。更新日志(​​https://go.dev/blog/go1.18​​​)中提到,模糊测试工具(fuzzing)被完整集成至工具链。同时,官方文档也增加了模糊测试的使用demo,详见​​https://go.dev/blog/fuzz​​​


关于模糊测试,golang官方的解释是,Fuzzing is a type of automated testing which continuously manipulates inputs to a program to find issues such as panics or bugs. These semi-random data mutations can discover new code coverage that existing unit tests may miss, and uncover edge case bugs which would otherwise go unnoticed. Since fuzzing can reach these edge cases, fuzz testing is particularly valuable for finding security exploits and vulnerabilities.

模糊测试是一种自动化的测试方法、通过连续不断地向程序提供输入,进而发现程序崩溃或是bug。这些半随机(何为半随机?)的数据突变,可以补充单元测试的不足,做到更高的代码覆盖率;并且,也可以去暴露一些易被忽略的边界情形bug。因为模糊测试可以触及这些边界情形,这项技术被常用于发现安全漏洞。

通俗一点, 即单元测试时候,通过fuzz来得到变异的输入,来尝试把函数跑崩,或是发现bug。

此内置工具的使用方法,与go test相似,即在xxx_test.go文件中,编写名为FuzzXXX的函数。将原本的go test命令,改为go test -fuzz=Fuzz

但此工具同样有些限制。例如,一个xxx_test.go文件中,仅能有一个FuzzXXX测试函数,否则会报错 testing: will not fuzz, -fuzz matches more than one fuzz test.

提供给fuzz函数的种子,必须为相同类型。

fuzz可变异的数据类型,仅为

  • ​string​​,​​[]byte​
  • ​int​​,​​int8​​,​​int16​​,​​int32​​/​​rune​​,​​int64​
  • ​uint​​,​​uint8​​/​​byte​​,​​uint16​​,​​uint32​​,​​uint64​
  • ​float32​​,​​float64​
  • ​bool​



功能测试DEMO

参考​​https://go.dev/blog/fuzz​

首先,创建main.go,在其中定义一个待测函数Reverse,如下,其功能为反转字符串

func Reverse(s string) string {
b := []byte(s)
for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
return string(b)
}


之后,创建main_test.go

功能测试函数如下,提供三组输入输出,分别为{"Hello, world", "dlrow ,olleH"},      {" ", " "},      {"!12345", "54321!"}

func TestReverse(t *testing.T) {
testcases := []tst_struct{
{"Hello, world", "dlrow ,olleH"}, {" ", " "}, {"!12345", "54321!"}, }
for _, tc := range testcases {
rev, _ := Reverse(tc.in)
if rev != tc.want {
t.Errorf("Reverse: %q, want %q", rev, tc.want)
}
}
}

go test, 然后如预料一般,pass, ok.

是不是认为这样就没问题了?


Fuzz测试DEMO

原有的反转函数,当入参字符串为ascii类型时,未发现报错。

若改为其他类型(utf8),返回结果是否仍符合期望?

基于此,在其中编写fuzz函数

func FuzzReverse(f *testing.F) {
testcases := []string{"Hello, world", " ", "!12345"}
for _, tc := range testcases {
f.Add(tc) // Use f.Add to provide a seed corpus }
f.Fuzz(func(t *testing.T, orig string) {
rev := Reverse(orig)
doubleRev := Reverse(rev)
if orig != doubleRev {
t.Errorf("Before: %q, after: %q", orig, doubleRev)
}
if utf8.ValidString(orig) && !utf8.ValidString(rev) {
t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
}
})
}

此测试函数的逻辑为,将一个字符串连续反转两次,判断返回反转结果是否与原始字符串相同(与原功能测试逻辑相同)。另外,当原始字符串为有效utf8编码字符串时,判断反转一次的结果是否同样为有效的utf8字符串。

第2行的testcases即为fuzz测试所需要的种子集,据此生成变异入参。(变异逻辑待后续阅读源码后尝试补充。)


完成后,打开命令行,进入项目目录,执行go test -fuzz=Fuzz

go1.18 模糊测试(fuzzing)初探_golang

如图,测试后,得到了一个单次反转结果不满足utf8编码的输入。

触发了这个逻辑的变异输入,详见testdata/fuzz/FuzzReverse目录

go1.18 模糊测试(fuzzing)初探_golang_02


根据这个,需要对原Reverse函数进行修改。

原函数应增加一个error类型的出参。当输入的字符串或是反转后的字符串,为无效utf8时,将错误返回。同时,将原字符串转为[]rune后再进行反转,这样可以覆盖到中文等类型的字符串输入。

参考如下

func Reverse(s string) (string, error) {
if !utf8.ValidString(s) {
return s, errors.New("input is not valid UTF-8")
}
r := []rune(s)
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r), nil}

Fuzz函数修改如下。当返回的错误不为空时,即代表入参或出参为不合法utf8类型。故测试时候将此情形跳过。

func FuzzReverse(f *testing.F) {
testcases := []string{"Hello, world", " ", "!12345"}
for _, tc := range testcases {
f.Add(tc) // Use f.Add to provide a seed corpus }
f.Fuzz(func(t *testing.T, orig string) {
rev, err1 := Reverse(orig)
if err1 != nil {
return }
doubleRev, err2 := Reverse(rev)
if err2 != nil {
return }
if orig != doubleRev {
t.Errorf("orig:%v,rev:%v, doubleRev:%v", []byte(orig), []byte(rev), []byte(doubleRev))
t.Errorf("Before: %q, after: %q", orig, doubleRev)
}
if utf8.ValidString(orig) && !utf8.ValidString(rev) {
//fmt.Println(orig) //fmt.Println(doubleRev) t.Errorf("orig:%v,rev:%v, doubleRev:%v", orig, rev, doubleRev)
t.Errorf("orig:%v,rev:%v, doubleRev:%v", []byte(orig), []byte(rev), []byte(doubleRev))
t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
}
})
}

修改后再次执行测试,通过

go1.18 模糊测试(fuzzing)初探_模糊测试_03

至此,一个完整的fuzz测试和debug流程告一段落。


结尾

原demo中有这样一段话,When fuzzing, you can’t predict the expected output, since you don’t have control over the inputs

即,当模糊测试时候,因为输入不可控,故不能精准预测输出。

这也是模糊测试和常规的功能测试主要区别。

通过大量的变异数据输入,来验证函数功能是否完整,这就是模糊测试。