【问题标题】:Apply fuzzing to a function that parses some string将模糊测试应用于解析某些字符串的函数
【发布时间】:2021-10-11 20:57:15
【问题描述】:

最近 Go 团队发布了一个 fuzzer https://blog.golang.org/fuzz-beta

您能否帮助描述一下我对 fuzzer 的测试目标的期望?

如何应用模糊器?

在认为它足够好之前,请提供一些关于我们将运行它多长时间的见解

如何将执行失败与代码关联起来(我希望有 GB 的结果,我想知道这可能会造成多大的压力以及如何处理)

请看这段非常棒的代码,肯定需要进行模糊测试

package main

import (
    "fmt"
    "log"
)

func main() {
    type expectation struct {
        input  string
        output []string
    }

    expectations := []expectation{
        expectation{
            input: "foo=bar baz baz foo:1 baz ",
            output: []string{
                "foo=bar baz baz",
                "foo:1 baz",
            },
        },
        expectation{
            input: "foo=bar baz baz foo:1 baz   foo:234.mds32",
            output: []string{
                "foo=bar baz baz",
                "foo:1 baz",
                "foo:234.mds32",
            },
        },
        expectation{
            input: "foo=bar baz baz foo:1 baz   foo:234.mds32  notfoo:baz  foo:bak foo=bar baz foo:nospace foo:bar",
            output: []string{
                "foo=bar baz baz",
                "foo:1 baz",
                "foo:234.mds32",
                "notfoo:baz",
                "foo:bak",
                "foo=bar baz",
                "foo:nospace",
                "foo:bar",
            },
        },
        expectation{
            input: "foo=bar",
            output: []string{
                "foo=bar",
            },
        },
        expectation{
            input: "foo",
            output: []string{
                "foo",
            },
        },
        expectation{
            input: "=bar",
            output: []string{
                "=bar",
            },
        },
        expectation{
            input: "foo=bar baz baz foo:::1 baz  ",
            output: []string{
                "foo=bar baz baz",
                "foo:::1 baz",
            },
        },
        expectation{
            input: "foo=bar baz baz   foo:::1 baz  ",
            output: []string{
                "foo=bar baz baz",
                "foo:::1 baz",
            },
        },
    }

    for i, expectation := range expectations {
        fmt.Println("  ==== TEST ", i)
        success := true
        res := parse(expectation.input)
        if len(res) != len(expectation.output) {
            log.Printf("invalid length of results for test %v\nwanted %#v\ngot    %#v", i, expectation.output, res)
            success = false
        }
        for e, r := range res {
            if expectation.output[e] != r {
                log.Printf("invalid result for test %v at index %v\nwanted %#v\ngot    %#v", i, e, expectation.output, res)
                success = false
            }
        }
        if success {
            fmt.Println("  ==== SUCCESS")
        } else {
            fmt.Println("  ==== FAILURE")
            break
        }
        fmt.Println()
    }
}

func parse(input string) (kvs []string) {
    var lastSpace int
    var nextLastSpace int
    var n int
    var since int
    for i, r := range input {
        if r == ' ' {
            nextLastSpace = i + 1
            if i > 0 && input[i-1] == ' ' {
                continue
            }
            lastSpace = i
        } else if r == '=' || r == ':' {
            if n == 0 {
                n++
                continue
            }
            n++
            if since < lastSpace {
                kvs = append(kvs, string(input[since:lastSpace]))
            }
            if lastSpace < nextLastSpace { // there was multiple in between spaces.
                since = nextLastSpace
            } else {
                since = lastSpace + 1
            }
        }
    }
    if since < len(input) { // still one entry
        var begin int
        var end int
        begin = since
        end = len(input)
        if lastSpace > since { // rm trailing spaces it ends with 'foo:whatever    '
            end = lastSpace
        } else if since < nextLastSpace { // rm starting spaces it ends with '   foo:whatever'
            begin = nextLastSpace
        }
        kvs = append(kvs, string(input[begin:end]))
    }
    return
}

【问题讨论】:

    标签: go fuzzing


    【解决方案1】:

    所以,我对模糊草稿设计进行了一些研究。以下是一些见解。

    首先,按照blog post 中的建议,您必须运行 Go 提示:

    go get golang.org/dl/gotip@latest
    gotip download
    

    gotip 命令充当“go 命令的直接替代品”,不会打乱您当前的安装。

    期望

    fuzzer 基本上会在某些函数的输入参数上生成一个变体语料库,并使用它们运行测试以发现错误。

    您无需自己编写任意数量的测试用例,而是向引擎提供示例输入,然后引擎会改变它们并使用新参数自动调用您的函数。然后,语料库将被缓存,以便作为回归测试的基础。

    如何应用模糊器?

    博客文章以及draft designdocumentation at tip 对此进行了相当不错的介绍

    testing 包现在有一个新类型 testing.F,您可以将其传递给 fuzz 目标。与单元测试和基准测试一样,模糊目标名称必须以 Fuzz 前缀开头。所以签名看起来像:

    func FuzzBlah(f *testing.F) {
        // ...
    }
    

    fuzz 目标主体本质上使用testing.F API 来:

    提供seed corpusF.Add

    种子语料库是用户指定的一组模糊目标的输入,默认情况下将使用 go test 运行。这些应该由有意义的输入组成,以测试包的行为,以及一组回归输入,用于模糊引擎识别的任何新发现的错误

    所以这些是您的 parse 函数的实际测试用例输入,您可以自己编写。

    func FuzzBlah(f *testing.F) {
        f.Add("foo=bar")
        f.Add("foo=bar baz baz foo:1 baz ")
        // and so on
    }
    

    使用 F.Fuzz 的模糊输入运行函数

    每个模糊目标调用一次f.FuzzFuzz 的参数是一个函数,它接受一个 testing.T 和 N 个与传递给 f.Add 相同类型的参数。如果您的示例测试只使用一个字符串,则为:

    func FuzzBlah(f *testing.F) {
        f.Add("foo=bar")
        f.Add("foo=bar baz baz foo:1 baz ")
        
        f.Fuzz(func(t *testing.T, input string) {
    
        })
    }
    

    然后,fuzz 函数的主体就是您想要测试的任何内容,例如您的 parse 函数。

    我认为,在理解和使用模糊器方面,关键是您不要测试输入和预期输出对。你可以通过单元测试来做到这一点。

    通过模糊测试,您可以测试代码不会因给定输入而中断。给定的输入是随机的,足以覆盖极端情况。这就是为什么官方的例子:

    • 呼叫t.Skip()以防预期失败
    • 运行反检查功能,例如Marshal 然后Unmarshal,或url.ParseQuery 然后query.Encode

    输入正确编组但随后未解组回原始值的情况是意外失败,以及模糊器比编写手动测试更容易找到的情况。

    总而言之,模糊目标可能是:

    func FuzzBlah(f *testing.F) {
        f.Add("foo=bar")
        f.Add("foo=bar baz baz foo:1 baz ")
        // and so on
        
        f.Fuzz(func(t *testing.T, input string) {
            out := parse(input)
            // bad output, skip
            if len(out) == 0 {
                t.Skip() 
            }
    
            // countercheck
            enc := encode(out)
            if enc != input {
                t.Errorf("countercheck failed")
            }
        })
    }
    

    导致测试失败的输入将被添加到语料库中,因此您可以修复代码并运行回归测试。

    运行它

    您只需使用-fuzz &lt;regex&gt; 标志运行go test。您可以指定模糊器运行的持续时间或次数:

    • -fuzztime &lt;duration&gt; 其中持续时间是 time.Duration string,例如10m
    • -fuzztime Nx 其中 N 是迭代次数,例如20x

    您的测试需要多长时间或多少次将取决于您要测试的代码。我相信 Go 团队会在适当的时候提供更多关于此的建议。

    总结一下:

    • gotip test -fuzz . -fuzztime 20x

    这也将在$GOCACHE/fuzz/ 的适当子目录中生成语料库。

    这应该足以让您入门。正如我在 cmets 中所说,该功能处于开发早期,因此可能存在错误并且可能缺少文档。随着更多信息的出现,我可能会更新此答案。

    【讨论】:

    • 哦,是的,谢谢,这是一个极好的答案。我想添加一些我在阅读您的答案和相应代码的过程中看到的提示。 F.Add 只接受类型的子集。 github.com/golang/go/blob/dev.fuzz/src/testing/fuzz.go#L231
    • 如果我们没有countercheck 喜欢演示功能。我们可以检查结果中预期的不变量。例如,该哑解析函数不得返回带有前缀/尾迹 ws 的元素。
    • 我把时间浪费在了无意义的问题上。应该工作那个。
    • 我还注意到代码None of the pointers to any input data should be retained between executions.github.com/golang/go/blob/dev.fuzz/src/testing/fuzz.go#L273 中的这条评论排除了任何依赖于全局(思考池)的代码
    • 再次感谢您参与其中,让我跳到源头和草稿。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-01
    • 1970-01-01
    相关资源
    最近更新 更多