Fuzzin' in Go

Go Blog recently announced a beta preview of fuzzing support in Go which gave me some new ideas on how to improve my development testing results. There are at least two classes of bugs which I think should surface as a result of applying fuzzing libraries:

  • panics as a result of:
    • nil variable usage
    • 3rd party library usage
  • timeouts

I’ll probably find other issues as well but making panic surface would save me a lot of time that I spend on debugging random JSON/REST payload mutations and interpretation of missing or empty JSON fields.

First off I need some code to test. I’ve decided to code a small piece of validation logic with a ton of bugs and then attempt to find at least some of them with fuzzing. Note that this chunk of code even once it’ll be executable without panic -ing is still worthless (you have been warned):

package main

import (
    "encoding/json"
    "log"
)

type Credentials struct {
    Username *string `json:"username"`
    Password *string `json:"password"`
}

// IsValid returns true if credentials are valid, false otherwise
func (c Credentials) IsValid() bool {
    for _, c := range []byte(*c.Username) {
        if c == byte('@') {
            return true
        }
    }
    if c.Password != nil && *c.Password == "" {
        // TODO: autogenerate password .)
        log.Fatal("User supplied empty password")
        return false
    }   
    return true
}   

type UserCredentials []*Credentials

// FilterValid filters invalid credentials out
func (uc UserCredentials) FilterValid() (UserCredentials, error) {
    validCredentials := UserCredentials{}
    
    for _, credentials := range uc {
        if *credentials.Username == "[email protected]" {
            validCredentials = append(validCredentials, credentials)
        }   
        if credentials.IsValid() {
            validCredentials = append(validCredentials, credentials)
        }   
    }   
    return validCredentials, nil
}   

It’s relatively simple on the eyes and full of obvious bugs. Now in order to make those bugs trigger I need to write a special kind of fuzzing test. Let’s walk through first attempt slowly.

// +build gofuzzbeta

First off this uses beta functionality that isn’t part of current stable Go release. In order for this to build properly the file needs to start with a build constraint to make this be included in build only if it’s supported.

package main

import (
    "encoding/json"
    "github.com/google/gofuzz"
    "github.com/stretchr/testify/assert"
    "testing"
)

Using just gofuzzbeta quickly turns to not be enough as I need to be able to generate input samples which is what github.com/google/gofuzz is for.

func prettyPrint(creds UserCredentials) string {
    s, _ := json.MarshalIndent(creds, "", "\t")
    return string(s)
}

prettyPrint is used to output samples for which fuzzing checks fail.

func FuzzValidate(f *testing.F) {

FuzzValidate is picked up when running all fuzzy tests the same way go test finds functions with Test prefix.

    gof := fuzz.New().NilChance(0.2).NumElements(1, 5)

To generate test cases I make use of gofuzz library. NilChance makes it generate nil values with a 20% probability and NumElements makes it generate lists of size in given range.

    for i := 0; i < 100; i++ {
        creds := UserCredentials{}
        gof.Fuzz(&creds)
        res, err := json.Marshal(creds)
        assert.Nil(f, err)
        f.Add(res)
    }

Next I generate a 100 credentials lists and use f.Add to add it to current fuzzing test samples. This will be consumed by f.Fuzz where I check if execution was successful and if not print out the offending input samples.

    f.Fuzz(func(t *testing.T, rawCredentials []byte) {
        var credentials UserCredentials
        err := json.Unmarshal(rawCredentials, &credentials)
        if err != nil {
            t.Fatalf("Failed unmarshalling input %v: %v", prettyPrint(credentials), err)
        }
        creds, err := credentials.FilterValid()
        if err != nil {
            t.Fatalf("Validate failed to execute for %v: %v", prettyPrint(creds), err)
        }
        if len(credentials) != len(creds) {
            t.Fatalf("Some credentials are not valid %v: %v", prettyPrint(creds), err)
        }
    })
}

f.Fuzz takes a function that is executed on all f.Add-ed samples. I passed in []byte because it didn’t want to consume my custom type. Hopefully this is something that can be improved on in the future.

If I run this now I get my first panic! (due to verbosity I’ll just paste the relevant part)

--- FAIL: FuzzValidate (0.00s)                                             
    --- FAIL: FuzzValidate/seed#0 (0.00s)                                  
panic: runtime error: invalid memory address or nil pointer dereference                      
goroutine 20 [running]:                                                    
...
panic({0x5ee2c0, 0x7c1760})                                                                    
        /home/simonm/sdk/gotip/src/runtime/panic.go:1038 +0x215                                
github.com/crnkofe/blog/fuzzin.UserCredentials.FilterValid({0xc00009fb00, 0x5, 0xc0000528e0})  
        /home/simonm/github/blog/fuzzin/bad_code.go:35 +0x77 
...

This happens on the if *credentials.Username == "[email protected]" { which (besides having a hardcoded admin) fails to check for Username field. I decide to add a unit test for nil case and fix the issue. nil Username bug also pops up in IsValid.

func TestValidateNilUsername(t *testing.T) {
    naiveInput := UserCredentials{
        {
            Username: nil,
        },
    }
    validCredentials, err := UserCredentials(naiveInput).FilterValid()
    assert.Nil(t, err)
    assert.Equal(t, 0, len(validCredentials))
}

Thinking if I should also exclude this input from fuzzer sample generator I think there’s actually no benefit since the issue was panicking code which after fixed should no longer panic.

--- FAIL: FuzzValidate (0.00s)
    --- FAIL: FuzzValidate/seed#1 (0.00s)
panic: runtime error: invalid memory address or nil pointer dereference
goroutine 22 [running]:
...
panic({0x5fbaa0, 0x7d8830})
        /home/simonm/sdk/gotip/src/runtime/panic.go:1038 +0x215
github.com/crnkofe/blog/fuzzin.UserCredentials.FilterValid({0xc00009fd70, 0x5, 0x20})
        /home/simonm/github/blog/fuzzin/bad_code.go:39 +0x77
...

This one is basically on the same line as first Username checker. It crashes due to me allowing nil in the UserCredentials list. A smart thing at at this point might have been to simply make the list non-nilable but I decide to go against my intuition since I can get more bugs that way.

func TestValidateNilCredentials(t *testing.T) {                                                                               
    naiveInput := UserCredentials{                                                                                    
        nil,                                                                                                          
    }                                                                                                                 
    validCredentials, err := UserCredentials(naiveInput).FilterValid()                                                
    assert.Nil(t, err)                                                                                                
    assert.Equal(t, 0, len(validCredentials))                                                                         
}

Back to fuzzing! Next error I get something interesting:

gotip test .
2021/06/13 20:34:33 User supplied empty password
FAIL    github.com/crnkofe/blog/fuzzin  0.008s
FAIL

The log.Fatal simply kills the fuzzer which is kind of expected but also annoying. As a dev I want a list of things to fix and not be locked into an endless try-fix routine. According to some discussions on this subject capturing log.Fatal would require me to do some mocking but in this particular case I’m not getting payed to do this so I skip the part but write a unit test nontheless.

func TestValidateEmptyPassword(t *testing.T) {
    emptyUsername := ""
    emptyPassword := ""
    naiveInput := UserCredentials{
        {
            Username: &emptyUsername,
            Password: &emptyPassword,
        },
    }
    validCredentials, err := UserCredentials(naiveInput).FilterValid()
    assert.Nil(t, err)
    assert.Equal(t, 0, len(validCredentials))
}

I decide to quietly remove the part with the log.Fatal and TODO. Rerunning the fuzzer oddly enough gives me no panic but I now do get some strange unexpected things:

   --- FAIL: FuzzValidate/seed#91 (0.00s)
        bad_code_fuzz_combo_test.go:39: Some credentials are not valid []: <nil>

It turns out this is due to fuzzer generating samples with empty string Username or Password. I make a few quick hacks to simply eliminate such input from being sent into fuzzer:

        sanitizedUserCredentials := UserCredentials{}
        // skip empty input
        for _, el := range creds {
            if el == nil || (*el == Credentials{}) {
                continue
            }   
            if el.Username == nil || el.Password == nil {
                continue
            }   
            if *el.Username == "" || *el.Password == "" {
                continue
            }   
            sanitizedUserCredentials = append(sanitizedUserCredentials, el) 
        }

And I’m done!

ok      github.com/crnkofe/blog/fuzzin  0.408s

Or am I?

Or am I?

Well to be honest I’d never use that code in production and I wasn’t expecting the admin hack to get caught by the fuzzer. It’d have to be extremely lucky to generate exactly that particular username as input which would cause not just to have a valid case but also a duplicated valid case. Email and password validation are also really bad but don’t cause visible failures during fuzzing.

Overall I’m somewhat satisfied with the ability to fuzz inputs. There are plenty of cases where I can see it produce bugs faster than I can reverse engineer, especially for untested legacy code. It’d be nice if fuzzing was supported by IDEs (GoLand if possible please) and if I could somehow workaround the need to do de/serialization when executing f.Fuzz.

Code: https://github.com/crnkofe/blog/tree/master/fuzzin

Recent posts: