Fuzz Testing in Golang

The on-rails way to test and build extremely safe code.

GolangTesting

“Fuzz testing” is an automated method for testing code. Inputs are auto-generated and fed into your code to find errors.

Go added an inbuilt fuzzer in version 1.18. You provide initial valid inputs, known as a “seed corpus”. The fuzzer uses coverage guidance to find edge cases and issues in your code. The process is automatic, the inputs are saved and re-runnable, and the coverage is high.

Here’s how you can fuzz your code.

Step 0 — Find a function you want to test

The first thing you’ll need is a function. I presume you have one in mind, but if not, you can set up a dummy function to follow this tutorial. Start a new Go project with:

bash
mkdir go_fuzzing
cd go_fuzzing
go mod init tutorial/fuzzing

This will add a go.mod file in your /go_fuzzing directory, setting up a module called tutorial/fuzzing. From here, you can add a new Golang file with:

bash
touch some_function.go

Open the file in your IDE of choice and add the following function:

go
package main

import (
 "fmt"
)

type Person struct {
 Name   string
 Age    int
 Active bool
}

func mapToStruct(data map[string]any) (*Person, error) {
 person := Person{
  Name:   data["name"].(string),
  Age:    data["age"].(int),
  Active: data["active"].(bool),
 }

 // Perform some operations based on the values
 if person.Active && person.Age < 1 {
  fmt.Printf("Welcome, active user %s!\n", person.Name)
  panic("babies can't be active")

 } else if person.Active && person.Age > 100 {
  fmt.Printf("Hello, %s. Please activate your account.\n", person.Name)
  panic("you're too old to be active")
 }

 fmt.Printf("User %s is %d years old.\n", person.Name, person.Age)

 return &person, nil
}

This function takes a map[string]any as input because this is how JSON is passed around in Golang. JSON is messy and changes frequently, so fuzzing is often used on JSON-derived inputs. I have added panic statements deliberately to cause unhandled errors for you to fix.

Step 1 — Write a Fuzzing function

Now you have a function; to fuzz it, you need to make a test file. If you’re on the terminal, from your root, you can run:

bash
touch some_function_test.go

The file containing your fuzz function needs a name ending of _test so it’s picked up by go test. I usually end the function in _fuzz_test to separate fuzzing from unit tests, but that isn’t necessary.

If you copied my example function above, you can paste the following code into this file:

go
package main

import (
 "encoding/json"
 "testing"
)

func FuzzMapToStruct(f *testing.F) {

 // The initial test cases used by the fuzzer to initiate the fuzzing process
 // These help the fuzzer understand the input space of the function
 seedCorpus := []map[string]any{
  map[string]any{"name": "John", "age": 30, "active": true},
  map[string]any{"name": "Jane", "age": 25, "active": false},
  map[string]any{"name": "Jim", "age": 10, "active": true},
 }

 // Add the initial test cases to the fuzzer
 for _, data := range seedCorpus {
  
  // Convert the map to a JSON string, error on failure
  jsonData, err := json.Marshal(data)
  if err != nil {
   f.Fatalf("failed to marshal data: %v", err)
  }

  // Add the JSON string to the fuzzer as a byte slice
  f.Add([]byte(jsonData))
 }

 f.Fuzz(func(t *testing.T, input []byte) {

  // Unmarshal the input into a map[string]any
  var data map[string]any

  err := json.Unmarshal(input, &data)
  if err != nil {
   t.Skip("failed to unmarshal data,skipping")
  }

  // Call the function with the unmarshalled data
  // This function will panic for various reasons, and you must fix it.
  mapToStruct(data)
 })
}

This begins by setting up a seed corpus of acceptable values. The fuzzer will use this to generate new inputs. The fuzzing function is started by the f.Fuzz line and is provided input in the form of a byte array: []byte. If the input cannot be unmarshalled into map[string]any it will be skipped. This avoids any none JSON-like inputs from getting into the function.

Aside — Fuzzing Inputs

The fuzzing function accepts a limited number of types. These are:

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

Step 2 — Run the Fuzzing function

Now you have your Fuzzing function set up, you can run the tests with:

bash
go test -fuzz=YourFuzzFunctionNameHere

This will run your fuzzing tests until a panic or until you stop them. If you want to set the time limit up front, you can run:

bash
go test -fuzz=YourFuzzFunctionNameHere -fuzztime=30s

Your options are:

  • seconds: s
  • minutes: m
  • hours: h

You can combine these together like this: 1h2m31s

Autogenerated Test Data

Every time your fuzzer finds a different code path through your function, it saves the input to $GOCACHE/fuzz. To see the inputs, visit your $GOCACHE folder. If you don’t know what it is, you can find it with:

bash
go env GOCACHE

To visit the GOCACHE you can use:

bash
cd <path-to-your-GOCACHE>/fuzz/

From here, if you list the directories in the file, you will see the names of the modules you’ve fuzzed. The directory structure matches the structure of the module. For example, if you have fuzzed a function held in:

bash
<your-module-root>/backend/api/<your-function-here>

The autogenerated data will follow the same path to reach your data.

Autogenerated Data That Causes Errors

If an error occurs, the fuzzer will attempt to find the minimum failing case and then save it as a regression test under:

bash
testdata/fuzz/<your-fuzzing-function-name-here>

This data can help you establish the use cases you must deal with. If you want to run just your failing autogenerated tests, use:

bash
go test -v -run=<your-fuzzing-function-name-here>

The -v flag will give you verbose details, and using go test without the -fuzz flag will run the failing autogenerated tests only.

Removing Autogenerated Data

If you make substantial changes to the data format of your seed corpus, you may invalidate past fuzz tests. This will result in output like this when you attempt to fuzz again:

yaml
=== RUN   YourFuzzTestHere
fuzz: elapsed: 1s, gathering baseline coverage: 0/729 completed
failure while testing seed corpus entry: YourFuzzTestHere/seed#0
fuzz: elapsed: 3s, gathering baseline coverage: 0/729 completed
    your_fuzz_test.go:184: Fuzzing complete
--- FAIL: YourFuzzTestHere (3.45s)
    --- FAIL: YourFuzzTestHere (0.00s)
        testing.go:1591: panic: ...(some issue)...

To avoid this, you have to delete your autogenerated data, which you can do by deleting the following directories:

bash
rm -r <path-to-your-GOCACHE>/fuzz/<path-to-your-fuzzFunction>
rm -r testdata/fuzz/<your-fuzzing-function-name-here>

This will remove all the previous tests and let you run from a clean slate after major changes.

Avoid Running Non-Fuzz Tests

You may have noticed that the -fuzz flag doesn’t prevent all the regular tests from being run first. This is nice for oversight but annoying during debugging. To prevent them from being printed, run:

bash
go test -run=^$ -fuzz=FuzzNormaliseCDR

The -run=^$ flag tells go test to run tests that match an empty string, i.e. none. ^ in regex is the start of a string, and $ is the end. ^$ means the start is the end.

Step 3 — Fix issues

If you’re fuzzing my example function, you will encounter many issues. As an exercise, try to fix all of these and get the fuzzer running for five minutes or more. Then, report back in the comments. How many issues did you find? How many autogenerated test cases do you have?

If you’re working on your own function, good luck, have fun! It’s fascinating to see what edge cases you haven’t considered.

Extras

Here is the official Go Tutorial for fuzzing:

Tutorial: Getting started with fuzzing - The Go Programming Language

and here is the official Go fuzzing overview:

Go Fuzzing - The Go Programming Language

Final Words

Thank you for reading. If you have any questions, drop them in the comments. I’m releasing a further article on “extreme fuzzing” to show you how to fuzz like a pro.

If you love the idea of fuzzing your code, follow this publication to read it next. Happy fuzzing!