Structure-aware Go fuzzing: How to fuzz with complex types

11th February, 2022
Adam Korczynski,
Security Engineering & Security Automation

In this blogpost I will introduce our go-fuzz-headers project that enables you to seed complex types with fuzzer-based data in Go.

Go 1.18 is here, and it is now as easy to write fuzz harnesses in Go as it is to write unit tests. In contrast to existing fuzzing engines, the Go 1.18 engine can provide wide range of types to a harness besides the standard byte array:

Some projects may need more complex types like structs, maps and slices. In this article we cover how you can create pseudo-random complex types completely deterministically in an automated fashion in Go 1.18 fuzz harnesses.

The problem

A while ago we were asked to fuzz a project that primarily had API’s that took structs as input. After researching the ecosystem we had to conclude that no solution existed that allowed us to automatically create pseudo-random structs based on the input from a coverage-guided fuzzing engine. This was before fuzzing was accepted into Go 1.18, and we, therefore, fuzzed with the go-fuzz engine. However, as the Go 1.18 fuzzing is also coverage-guided, users can easily create fuzzers for their targets that accept structs as arguments.

Without an automatic way of creating pseudo-random structs, a solution is naturally to insert values manually in the relevant struct as such:

type Demostruct struct {
    Field1 string
    Field2  int
}

func Fuzz(data []byte) int {
    if len(data)<3 {
        return 0
    }
    s := Demostruct{Field1: string(data[1:]), Field2: int(data[0])}
    targetFunc(s)
    return 1
}

This works fine for smaller structs and for some use cases it may even be the preferred method, but it becomes increasingly more complicated the bigger and more nested the struct becomes. Some structs would need more than a hundred fields to be handled manually, and the slightest change in these structs would require the fuzzer to be rewritten.

Introducing go-fuzz-headers

At Ada Logics we created go-fuzz-headers to make it easy to write fuzz drivers for targets with parameters like structs, maps and slices. We have used go-fuzz-headers extensively for about a year to find bugs in large open-source projects like Kubernetes, Vitess and Istio, and with Go 1.18 fuzzing being on the doorstep we are happy to share how the project can be used with the Go 1.18 fuzzing engine.

The more we used go-fuzz-headers internally, the more we realised that we could create a general solution for creating pseudo-random versions of complex types. The API, GenerateStruct(), we used to create pseudo-random versions of the first 10 structs was used to create pseudo-random versions of the next hundreds of structs from different projects as well. We extended the support to maps and slices, and the same general support was kept. As such, the go-fuzz-headers project today offers a series of high-level APIs that can be used for any type of struct, slice and map. We still continue to extend the project with other useful helpers like CreateFiles in case you need a pseudo-random file directory.

Creating pseudo-random structs with native Go fuzzing

Let’s look at an example of creating pseudo-random structs with Go 1.18 fuzzing. Let’s start with the basic template from which we start every fuzzer written for the Go 1.18 engine:

package fuzzing

import (
        "testing"
)

func Fuzz(f *testing.F) {
        f.Fuzz(func(t *testing.T, data []byte) {
                // Fuzz content here
        })
}

Because we need to pass a byte slice when we instantiate the fuzz consumer, in this fuzzer we choose a single argument in f.Fuzz(), namely a byte slice. As such, we proceed by instantiating the fuzz consumer:

package fuzzing

import (
        "testing"
        fuzz "github.com/AdaLogics/go-fuzz-headers"
)

func Fuzz(f *testing.F) {
        f.Fuzz(func(t *testing.T, data []byte) {
                fuzzConsumer := fuzz.NewConsumer(data)
        })
}

Next let’s create a DemoStruct and pass it to f.GenerateStruct() to allow the fuzzer to insert pseudo-random values:

package fuzzing

import (
        "testing"
        fuzz "github.com/AdaLogics/go-fuzz-headers"
)

func Fuzz(f *testing.F) {
        f.Fuzz(func(t *testing.T, data []byte) {
                fuzzConsumer := fuzz.NewConsumer(data)
                targetStruct := &Demostruct{}
                err := fuzzConsumer.GenerateStruct(targetStruct)
                if err != nil {
                        return
                }
        })
}

We have now created a fuzzer that will create a randomised Demostruct:

package fuzzing

import (
        "testing"
        fuzz "github.com/AdaLogics/go-fuzz-headers"
)

type Demostruct struct {
        Field1 string
        Field2  int
}

var counter int

func init() {
        counter = 0
}

func Fuzz(f *testing.F) {
        f.Fuzz(func(t *testing.T, data []byte) {
                fuzzConsumer := fuzz.NewConsumer(data)
                targetStruct := &Demostruct{}
                err := fuzzConsumer.GenerateStruct(targetStruct)
                if err != nil {
                        return
                }
                if targetStruct.Field1=="" {
                        return
                }
                counter++
                if counter==10000 {
                        t.Fatalf("%+v\n", targetStruct)
                }
        })
}

Let’s run this fuzzer for a little while and see how the targetStruct looks:

gotip test -fuzz=Fuzz -v

After a few seconds, the following stacktrace is produced:

=== FUZZ  Fuzz
fuzz: elapsed: 0s, gathering baseline coverage: 0/7 completed
fuzz: elapsed: 0s, gathering baseline coverage: 7/7 completed, now fuzzing with 10 workers
fuzz: minimizing 36-byte failing input file
fuzz: elapsed: 3s, minimizing
--- FAIL: Fuzz (2.82s)
    --- FAIL: Fuzz (0.00s)
        fuzz_test.go:32: &{Field1:0000 Field2:48}
    
    Failing input written to testdata/fuzz/Fuzz/a08445a85261adfba84a424cd6d7da4588b7259e8f7ec673f3d6f175aef67e1a
    To re-run:
    go test -run=Fuzz/a08445a85261adfba84a424cd6d7da4588b7259e8f7ec673f3d6f175aef67e1a
FAIL
exit status 1
FAIL    fuzzing 2.831s

And there we go, we have written a fuzzer that inserts pseudo-random values into the DemoStruct, and these values are determined by the data byte slice.

What about nested structs?

You may have nested structs in your project, and the usage of GenerateStruct() would be no different for those cases. In other words, go-fuzz-headers will insert pseudo-random values into all fields, and the fields’ fields in a struct passed to GenerateStruct().

Other useful APIs

With go-fuzz-headers a few other very useful APIs are available besides for fuzzing structs, some of which are:

CreateFiles

Takes a path as an argument and creates a number of pseudo-random files and directories in that path. The files will not be created outside of the given path.

An example of using CreateFiles():

package examples

import (
    "io/ioutil"
    “testing”
    “os"

    fuzz "github.com/AdaLogics/go-fuzz-headers"
)

func Fuzz(f *testing.F) {
    f.Fuzz(func(t *testing.T, data []byte) {
        tmpDir, err := ioutil.TempDir("dir", "prefix")
        if err != nil {
            return 0
        }
        defer os.RemoveAll(tmpDir)
        fz := fuzz.NewConsumer(data)
        err = fz.CreateFiles(tmpDir)
        if err != nil {
            return 0
        }
        return 1
}

By passing a path to a temporary directory, all the files and directories created by the fuzzer can be removed easily in each iteration using defer os.RemoveAll(tmpDir).

FuzzMap

In case you need an API that creates pseudo-random maps for you in an automated way, you can use FuzzMap(). It is used as such:

package examples

import (
    "testing"

    fuzz "github.com/AdaLogics/go-fuzz-headers"
)

func Fuzz(f *testing.F) {
    f.Fuzz(func(t *testing.T, data []byte) {
        var m map[string]string
        fz := fuzz.NewConsumer(data)
        err := fz.FuzzMap(&m)
        if err != nil {
            return 0
        }
        return 1
}

This API will create a map of a pseudo-random length with pseudo-random values.

CreateSlice

A slice containing any type can be passed to CreateSlice() and a pseudo-random number of pseudo-random values will be inserted.

Usage:

package examples

import (
    "testing"

    fuzz "github.com/AdaLogics/go-fuzz-headers"
)

func Fuzz(f *testing.F) {
    f.Fuzz(func(t *testing.T, data []byte) {
        fz := fuzz.NewConsumer(data)
        var targetSlice []string
        err := fz.CreateSlice(&targetSlice)
        if err != nil {
            return 0
        }
        return 1
}

Closing thoughts

The ability to create complex structures whose data is seeded by the fuzzer is a powerful technique when it comes to fuzzing. We have created a library that allows you to do this called go-fuzz-headers which is easy to integrate into your fuzzing workflow. The library is open source and you can find all details in the repository https://github.com/AdaLogics/go-fuzz-headers#projects-that-use-go-fuzz-headers