mirror of
https://github.com/golang/go.git
synced 2025-12-08 06:10:04 +00:00
[dev.fuzz] testing,internal/fuzz: support structured inputs
This change makes several refactors to start supporting structured fuzzing. The mutator can still only mutate byte slices, and future changes will be made to support mutating other types. However, it does now support fuzzing more than one []byte. This change also makes it so that corpus entries are encoded in the new file format when being written to testdata or GOCACHE. Any existing GOCACHE data should be deleted from your local workstation to allow tests to pass locally. Change-Id: Iab8fe01a5dc870f0c53010b9d5b0b479bbdb310d Reviewed-on: https://go-review.googlesource.com/c/go/+/293810 Trust: Katie Hockman <katie@golang.org> Run-TryBot: Katie Hockman <katie@golang.org> TryBot-Result: Go Bot <gobot@golang.org> Reviewed-by: Jay Conrod <jayconrod@google.com>
This commit is contained in:
parent
5aacd47c00
commit
621a81aba0
10 changed files with 298 additions and 72 deletions
57
src/cmd/go/testdata/script/test_fuzz.txt
vendored
57
src/cmd/go/testdata/script/test_fuzz.txt
vendored
|
|
@ -116,6 +116,21 @@ stdout 'off by one error'
|
||||||
! stdout ^ok
|
! stdout ^ok
|
||||||
stdout FAIL
|
stdout FAIL
|
||||||
|
|
||||||
|
# Test panic with unsupported seed corpus
|
||||||
|
! go test -run FuzzUnsupported fuzz_add_test.go
|
||||||
|
! stdout ^ok
|
||||||
|
stdout FAIL
|
||||||
|
|
||||||
|
# Test panic with different number of args to f.Add
|
||||||
|
! go test -run FuzzAddDifferentNumber fuzz_add_test.go
|
||||||
|
! stdout ^ok
|
||||||
|
stdout FAIL
|
||||||
|
|
||||||
|
# Test panic with different type of args to f.Add
|
||||||
|
! go test -run FuzzAddDifferentType fuzz_add_test.go
|
||||||
|
! stdout ^ok
|
||||||
|
stdout FAIL
|
||||||
|
|
||||||
# Test fatal with testdata seed corpus
|
# Test fatal with testdata seed corpus
|
||||||
! go test -run FuzzFail corpustesting/fuzz_testdata_corpus_test.go
|
! go test -run FuzzFail corpustesting/fuzz_testdata_corpus_test.go
|
||||||
! stdout ^ok
|
! stdout ^ok
|
||||||
|
|
@ -128,6 +143,11 @@ stdout ok
|
||||||
! stdout FAIL
|
! stdout FAIL
|
||||||
! stdout 'fatal here'
|
! stdout 'fatal here'
|
||||||
|
|
||||||
|
# Test panic with malformed seed corpus
|
||||||
|
! go test -run FuzzFail corpustesting/fuzz_testdata_corpus_test.go
|
||||||
|
! stdout ^ok
|
||||||
|
stdout FAIL
|
||||||
|
|
||||||
# Test pass with file in other nested testdata directory
|
# Test pass with file in other nested testdata directory
|
||||||
go test -run FuzzInNestedDir corpustesting/fuzz_testdata_corpus_test.go
|
go test -run FuzzInNestedDir corpustesting/fuzz_testdata_corpus_test.go
|
||||||
stdout ok
|
stdout ok
|
||||||
|
|
@ -316,6 +336,24 @@ func FuzzNilPanic(f *testing.F) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FuzzUnsupported(f *testing.F) {
|
||||||
|
m := make(map[string]bool)
|
||||||
|
f.Add(m)
|
||||||
|
f.Fuzz(func(t *testing.T, b []byte) {})
|
||||||
|
}
|
||||||
|
|
||||||
|
func FuzzAddDifferentNumber(f *testing.F) {
|
||||||
|
f.Add([]byte("a"))
|
||||||
|
f.Add([]byte("a"), []byte("b"))
|
||||||
|
f.Fuzz(func(t *testing.T, b []byte) {})
|
||||||
|
}
|
||||||
|
|
||||||
|
func FuzzAddDifferentType(f *testing.F) {
|
||||||
|
f.Add(false)
|
||||||
|
f.Add(1234)
|
||||||
|
f.Fuzz(func(t *testing.T, b []byte) {})
|
||||||
|
}
|
||||||
|
|
||||||
-- corpustesting/fuzz_testdata_corpus_test.go --
|
-- corpustesting/fuzz_testdata_corpus_test.go --
|
||||||
package fuzz_testdata_corpus
|
package fuzz_testdata_corpus
|
||||||
|
|
||||||
|
|
@ -324,7 +362,7 @@ import "testing"
|
||||||
func fuzzFn(f *testing.F) {
|
func fuzzFn(f *testing.F) {
|
||||||
f.Helper()
|
f.Helper()
|
||||||
f.Fuzz(func(t *testing.T, b []byte) {
|
f.Fuzz(func(t *testing.T, b []byte) {
|
||||||
if string(b) == "12345\n" {
|
if string(b) == "12345" {
|
||||||
t.Fatal("fatal here")
|
t.Fatal("fatal here")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -338,13 +376,22 @@ func FuzzPass(f *testing.F) {
|
||||||
fuzzFn(f)
|
fuzzFn(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FuzzPanic(f *testing.F) {
|
||||||
|
f.Fuzz(func(t *testing.T, b []byte) {})
|
||||||
|
}
|
||||||
|
|
||||||
func FuzzInNestedDir(f *testing.F) {
|
func FuzzInNestedDir(f *testing.F) {
|
||||||
fuzzFn(f)
|
f.Fuzz(func(t *testing.T, b []byte) {})
|
||||||
}
|
}
|
||||||
|
|
||||||
-- corpustesting/testdata/corpus/FuzzFail/1 --
|
-- corpustesting/testdata/corpus/FuzzFail/1 --
|
||||||
12345
|
go test fuzz v1
|
||||||
|
[]byte("12345")
|
||||||
-- corpustesting/testdata/corpus/FuzzPass/1 --
|
-- corpustesting/testdata/corpus/FuzzPass/1 --
|
||||||
00000
|
go test fuzz v1
|
||||||
|
[]byte("00000")
|
||||||
|
-- corpustesting/testdata/corpus/FuzzPanic/1 --
|
||||||
|
malformed
|
||||||
-- corpustesting/testdata/corpus/FuzzInNestedDir/anotherdir/1 --
|
-- corpustesting/testdata/corpus/FuzzInNestedDir/anotherdir/1 --
|
||||||
12345
|
go test fuzz v1
|
||||||
|
[]byte("12345")
|
||||||
|
|
@ -24,6 +24,12 @@ go run check_testdata.go FuzzWithBug
|
||||||
# the target, and should fail when run without fuzzing.
|
# the target, and should fail when run without fuzzing.
|
||||||
! go test -parallel=1
|
! go test -parallel=1
|
||||||
|
|
||||||
|
# Running the fuzzer should find a crashing input quickly for fuzzing two types.
|
||||||
|
! go test -run=FuzzWithTwoTypes -fuzz=FuzzWithTwoTypes -fuzztime=5s -parallel=1
|
||||||
|
stdout 'testdata[/\\]corpus[/\\]FuzzWithTwoTypes[/\\]'
|
||||||
|
stdout 'these inputs caused a crash!'
|
||||||
|
go run check_testdata.go FuzzWithTwoTypes
|
||||||
|
|
||||||
! go test -run=FuzzWithNilPanic -fuzz=FuzzWithNilPanic -fuzztime=5s -parallel=1
|
! go test -run=FuzzWithNilPanic -fuzz=FuzzWithNilPanic -fuzztime=5s -parallel=1
|
||||||
stdout 'testdata[/\\]corpus[/\\]FuzzWithNilPanic[/\\]'
|
stdout 'testdata[/\\]corpus[/\\]FuzzWithNilPanic[/\\]'
|
||||||
stdout 'runtime.Goexit'
|
stdout 'runtime.Goexit'
|
||||||
|
|
@ -64,6 +70,14 @@ func FuzzWithNilPanic(f *testing.F) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FuzzWithTwoTypes(f *testing.F) {
|
||||||
|
f.Fuzz(func(t *testing.T, a, b []byte) {
|
||||||
|
if len(a) > 0 && len(b) > 0 {
|
||||||
|
panic("these inputs caused a crash!")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func FuzzWithBadExit(f *testing.F) {
|
func FuzzWithBadExit(f *testing.F) {
|
||||||
f.Add([]byte("aa"))
|
f.Add([]byte("aa"))
|
||||||
f.Fuzz(func(t *testing.T, b []byte) {
|
f.Fuzz(func(t *testing.T, b []byte) {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ go test -fuzz=FuzzA -fuzztime=5s -parallel=1 -log=fuzz
|
||||||
go run check_logs.go fuzz fuzz.worker
|
go run check_logs.go fuzz fuzz.worker
|
||||||
|
|
||||||
# Test that the mutator is good enough to find several unique mutations.
|
# Test that the mutator is good enough to find several unique mutations.
|
||||||
! go test -v -fuzz=Fuzz -parallel=1 -fuzztime=30s mutator_test.go
|
! go test -fuzz=Fuzz -parallel=1 -fuzztime=30s mutator_test.go
|
||||||
! stdout ok
|
! stdout ok
|
||||||
stdout FAIL
|
stdout FAIL
|
||||||
stdout 'mutator found enough unique mutations'
|
stdout 'mutator found enough unique mutations'
|
||||||
|
|
|
||||||
|
|
@ -20,14 +20,14 @@ var encVersion1 = "go test fuzz v1"
|
||||||
// corpus.
|
// corpus.
|
||||||
func marshalCorpusFile(vals ...interface{}) []byte {
|
func marshalCorpusFile(vals ...interface{}) []byte {
|
||||||
if len(vals) == 0 {
|
if len(vals) == 0 {
|
||||||
panic("must have at least one value to encode")
|
panic("must have at least one value to marshal")
|
||||||
}
|
}
|
||||||
b := bytes.NewBuffer([]byte(encVersion1))
|
b := bytes.NewBuffer([]byte(encVersion1))
|
||||||
// TODO(katiehockman): keep uint8 and int32 encoding where applicable,
|
// TODO(katiehockman): keep uint8 and int32 encoding where applicable,
|
||||||
// instead of changing to byte and rune respectively.
|
// instead of changing to byte and rune respectively.
|
||||||
for _, val := range vals {
|
for _, val := range vals {
|
||||||
switch t := val.(type) {
|
switch t := val.(type) {
|
||||||
case int, int8, int16, int64, uint, uint16, uint32, uint64, uintptr, float32, float64, bool:
|
case int, int8, int16, int64, uint, uint16, uint32, uint64, float32, float64, bool:
|
||||||
fmt.Fprintf(b, "\n%T(%v)", t, t)
|
fmt.Fprintf(b, "\n%T(%v)", t, t)
|
||||||
case string:
|
case string:
|
||||||
fmt.Fprintf(b, "\nstring(%q)", t)
|
fmt.Fprintf(b, "\nstring(%q)", t)
|
||||||
|
|
@ -47,7 +47,7 @@ func marshalCorpusFile(vals ...interface{}) []byte {
|
||||||
// unmarshalCorpusFile decodes corpus bytes into their respective values.
|
// unmarshalCorpusFile decodes corpus bytes into their respective values.
|
||||||
func unmarshalCorpusFile(b []byte) ([]interface{}, error) {
|
func unmarshalCorpusFile(b []byte) ([]interface{}, error) {
|
||||||
if len(b) == 0 {
|
if len(b) == 0 {
|
||||||
return nil, fmt.Errorf("cannot decode empty string")
|
return nil, fmt.Errorf("cannot unmarshal empty string")
|
||||||
}
|
}
|
||||||
lines := bytes.Split(b, []byte("\n"))
|
lines := bytes.Split(b, []byte("\n"))
|
||||||
if len(lines) < 2 {
|
if len(lines) < 2 {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,9 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -32,6 +34,8 @@ import (
|
||||||
// seed is a list of seed values added by the fuzz target with testing.F.Add and
|
// seed is a list of seed values added by the fuzz target with testing.F.Add and
|
||||||
// in testdata.
|
// in testdata.
|
||||||
//
|
//
|
||||||
|
// types is the list of types which make up a corpus entry.
|
||||||
|
//
|
||||||
// corpusDir is a directory where files containing values that crash the
|
// corpusDir is a directory where files containing values that crash the
|
||||||
// code being tested may be written.
|
// code being tested may be written.
|
||||||
//
|
//
|
||||||
|
|
@ -40,7 +44,7 @@ import (
|
||||||
//
|
//
|
||||||
// If a crash occurs, the function will return an error containing information
|
// If a crash occurs, the function will return an error containing information
|
||||||
// about the crash, which can be reported to the user.
|
// about the crash, which can be reported to the user.
|
||||||
func CoordinateFuzzing(ctx context.Context, parallel int, seed []CorpusEntry, corpusDir, cacheDir string) (err error) {
|
func CoordinateFuzzing(ctx context.Context, parallel int, seed []CorpusEntry, types []reflect.Type, corpusDir, cacheDir string) (err error) {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -49,12 +53,22 @@ func CoordinateFuzzing(ctx context.Context, parallel int, seed []CorpusEntry, co
|
||||||
}
|
}
|
||||||
|
|
||||||
sharedMemSize := 100 << 20 // 100 MB
|
sharedMemSize := 100 << 20 // 100 MB
|
||||||
corpus, err := readCache(seed, cacheDir)
|
// Make sure all of the seed corpus has marshalled data.
|
||||||
|
for i := range seed {
|
||||||
|
if seed[i].Data == nil {
|
||||||
|
seed[i].Data = marshalCorpusFile(seed[i].Values...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
corpus, err := readCache(seed, types, cacheDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(corpus.entries) == 0 {
|
if len(corpus.entries) == 0 {
|
||||||
corpus.entries = []CorpusEntry{{Data: []byte{}}}
|
var vals []interface{}
|
||||||
|
for _, t := range types {
|
||||||
|
vals = append(vals, zeroValue(t))
|
||||||
|
}
|
||||||
|
corpus.entries = append(corpus.entries, CorpusEntry{Data: marshalCorpusFile(vals...), Values: vals})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(jayconrod): do we want to support fuzzing different binaries?
|
// TODO(jayconrod): do we want to support fuzzing different binaries?
|
||||||
|
|
@ -224,10 +238,8 @@ type CorpusEntry = struct {
|
||||||
// Data is the raw data loaded from a corpus file.
|
// Data is the raw data loaded from a corpus file.
|
||||||
Data []byte
|
Data []byte
|
||||||
|
|
||||||
// TODO(jayconrod,katiehockman): support multiple values of different types
|
// Values is the unmarshaled values from a corpus file.
|
||||||
// added with f.Add with a Values []interface{} field. We'll need marhsalling
|
Values []interface{}
|
||||||
// and unmarshalling functions, and we'll need to figure out what to do
|
|
||||||
// in the mutator.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type crasherEntry struct {
|
type crasherEntry struct {
|
||||||
|
|
@ -262,35 +274,57 @@ type coordinator struct {
|
||||||
errC chan error
|
errC chan error
|
||||||
}
|
}
|
||||||
|
|
||||||
// readCache creates a combined corpus from seed values, values in the
|
// readCache creates a combined corpus from seed values and values in the cache
|
||||||
// corpus directory (in testdata), and values in the cache (in GOCACHE/fuzz).
|
// (in GOCACHE/fuzz).
|
||||||
//
|
//
|
||||||
// TODO(jayconrod,katiehockman): if a value in the cache has the wrong type,
|
|
||||||
// ignore it instead of reporting an error. Cached values may be used for
|
|
||||||
// the same package at a different version or in a different module.
|
|
||||||
// TODO(jayconrod,katiehockman): need a mechanism that can remove values that
|
// TODO(jayconrod,katiehockman): need a mechanism that can remove values that
|
||||||
// aren't useful anymore, for example, because they have the wrong type.
|
// aren't useful anymore, for example, because they have the wrong type.
|
||||||
func readCache(seed []CorpusEntry, cacheDir string) (corpus, error) {
|
func readCache(seed []CorpusEntry, types []reflect.Type, cacheDir string) (corpus, error) {
|
||||||
var c corpus
|
var c corpus
|
||||||
c.entries = append(c.entries, seed...)
|
c.entries = append(c.entries, seed...)
|
||||||
entries, err := ReadCorpus(cacheDir)
|
entries, err := ReadCorpus(cacheDir, types)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if _, ok := err.(*MalformedCorpusError); !ok {
|
||||||
|
// It's okay if some files in the cache directory are malformed and
|
||||||
|
// are not included in the corpus, but fail if it's an I/O error.
|
||||||
return corpus{}, err
|
return corpus{}, err
|
||||||
}
|
}
|
||||||
|
// TODO(jayconrod,katiehockman): consider printing some kind of warning
|
||||||
|
// indicating the number of files which were skipped because they are
|
||||||
|
// malformed.
|
||||||
|
}
|
||||||
c.entries = append(c.entries, entries...)
|
c.entries = append(c.entries, entries...)
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadCorpus reads the corpus from the testdata directory in this target's
|
// MalformedCorpusError is an error found while reading the corpus from the
|
||||||
// package.
|
// filesystem. All of the errors are stored in the errs list. The testing
|
||||||
func ReadCorpus(dir string) ([]CorpusEntry, error) {
|
// framework uses this to report malformed files in testdata.
|
||||||
|
type MalformedCorpusError struct {
|
||||||
|
errs []error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *MalformedCorpusError) Error() string {
|
||||||
|
var msgs []string
|
||||||
|
for _, s := range e.errs {
|
||||||
|
msgs = append(msgs, s.Error())
|
||||||
|
}
|
||||||
|
return strings.Join(msgs, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadCorpus reads the corpus from the provided dir. The returned corpus
|
||||||
|
// entries are guaranteed to match the given types. Any malformed files will
|
||||||
|
// be saved in a MalformedCorpusError and returned, along with the most recent
|
||||||
|
// error.
|
||||||
|
func ReadCorpus(dir string, types []reflect.Type) ([]CorpusEntry, error) {
|
||||||
files, err := ioutil.ReadDir(dir)
|
files, err := ioutil.ReadDir(dir)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return nil, nil // No corpus to read
|
return nil, nil // No corpus to read
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return nil, fmt.Errorf("testing: reading seed corpus from testdata: %v", err)
|
return nil, fmt.Errorf("reading seed corpus from testdata: %v", err)
|
||||||
}
|
}
|
||||||
var corpus []CorpusEntry
|
var corpus []CorpusEntry
|
||||||
|
var errs []error
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
// TODO(jayconrod,katiehockman): determine when a file is a fuzzing input
|
// TODO(jayconrod,katiehockman): determine when a file is a fuzzing input
|
||||||
// based on its name. We should only read files created by writeToCorpus.
|
// based on its name. We should only read files created by writeToCorpus.
|
||||||
|
|
@ -300,11 +334,30 @@ func ReadCorpus(dir string) ([]CorpusEntry, error) {
|
||||||
if file.IsDir() {
|
if file.IsDir() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
bytes, err := ioutil.ReadFile(filepath.Join(dir, file.Name()))
|
filename := filepath.Join(dir, file.Name())
|
||||||
|
data, err := ioutil.ReadFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("testing: failed to read corpus file: %v", err)
|
return nil, fmt.Errorf("failed to read corpus file: %v", err)
|
||||||
}
|
}
|
||||||
corpus = append(corpus, CorpusEntry{Name: file.Name(), Data: bytes})
|
vals, err := unmarshalCorpusFile(data)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("failed to unmarshal %q: %v", filename, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(vals) != len(types) {
|
||||||
|
errs = append(errs, fmt.Errorf("wrong number of values in corpus file %q: %d, want %d", filename, len(vals), len(types)))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for i := range types {
|
||||||
|
if reflect.TypeOf(vals[i]) != types[i] {
|
||||||
|
errs = append(errs, fmt.Errorf("mismatched types in corpus file %q: %v, want %v", filename, vals, types))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
corpus = append(corpus, CorpusEntry{Name: file.Name(), Data: data, Values: vals})
|
||||||
|
}
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return corpus, &MalformedCorpusError{errs: errs}
|
||||||
}
|
}
|
||||||
return corpus, nil
|
return corpus, nil
|
||||||
}
|
}
|
||||||
|
|
@ -325,3 +378,32 @@ func writeToCorpus(b []byte, dir string) (name string, err error) {
|
||||||
}
|
}
|
||||||
return name, nil
|
return name, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func zeroValue(t reflect.Type) interface{} {
|
||||||
|
for _, v := range zeroVals {
|
||||||
|
if reflect.TypeOf(v) == t {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic(fmt.Sprintf("unsupported type: %v", t))
|
||||||
|
}
|
||||||
|
|
||||||
|
var zeroVals []interface{} = []interface{}{
|
||||||
|
[]byte(""),
|
||||||
|
string(""),
|
||||||
|
false,
|
||||||
|
byte(0),
|
||||||
|
rune(0),
|
||||||
|
float32(0),
|
||||||
|
float64(0),
|
||||||
|
int(0),
|
||||||
|
int8(0),
|
||||||
|
int16(0),
|
||||||
|
int32(0),
|
||||||
|
int64(0),
|
||||||
|
uint(0),
|
||||||
|
uint8(0),
|
||||||
|
uint16(0),
|
||||||
|
uint32(0),
|
||||||
|
uint64(0),
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ package fuzz
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
@ -49,12 +50,38 @@ func min(a, b int) int {
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
// mutate performs several mutations directly onto the provided byte slice.
|
// mutate performs several mutations on the provided values.
|
||||||
func (m *mutator) mutate(ptrB *[]byte) {
|
func (m *mutator) mutate(vals []interface{}, maxBytes int) []interface{} {
|
||||||
// TODO(jayconrod,katiehockman): make this use zero allocations
|
// TODO(jayconrod,katiehockman): use as few allocations as possible
|
||||||
// TODO(katiehockman): pull some of these functions into helper methods
|
// TODO(katiehockman): pull some of these functions into helper methods and
|
||||||
// and test that each case is working as expected.
|
// test that each case is working as expected.
|
||||||
// TODO(katiehockman): perform more types of mutations.
|
// TODO(katiehockman): perform more types of mutations.
|
||||||
|
|
||||||
|
// maxPerVal will represent the maximum number of bytes that each value be
|
||||||
|
// allowed after mutating, giving an equal amount of capacity to each line.
|
||||||
|
// Allow a little wiggle room for the encoding.
|
||||||
|
maxPerVal := maxBytes/len(vals) - 100
|
||||||
|
|
||||||
|
// Pick a random value to mutate.
|
||||||
|
// TODO: consider mutating more than one value at a time.
|
||||||
|
i := m.rand(len(vals))
|
||||||
|
// TODO(katiehockman): support mutating other types
|
||||||
|
switch v := vals[i].(type) {
|
||||||
|
case []byte:
|
||||||
|
if len(v) > maxPerVal {
|
||||||
|
panic(fmt.Sprintf("cannot mutate bytes of length %d", len(v)))
|
||||||
|
}
|
||||||
|
b := make([]byte, 0, maxPerVal)
|
||||||
|
b = append(b, v...)
|
||||||
|
m.mutateBytes(&b)
|
||||||
|
vals[i] = b
|
||||||
|
return vals
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("type not supported for mutating: %T", vals[i]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mutator) mutateBytes(ptrB *[]byte) {
|
||||||
b := *ptrB
|
b := *ptrB
|
||||||
defer func() {
|
defer func() {
|
||||||
oldHdr := (*reflect.SliceHeader)(unsafe.Pointer(ptrB))
|
oldHdr := (*reflect.SliceHeader)(unsafe.Pointer(ptrB))
|
||||||
|
|
@ -103,6 +130,10 @@ func (m *mutator) mutate(ptrB *[]byte) {
|
||||||
dst = m.rand(len(b))
|
dst = m.rand(len(b))
|
||||||
}
|
}
|
||||||
n := m.chooseLen(len(b) - src)
|
n := m.chooseLen(len(b) - src)
|
||||||
|
if len(b)+n >= cap(b) {
|
||||||
|
iter--
|
||||||
|
continue
|
||||||
|
}
|
||||||
tmp := make([]byte, n)
|
tmp := make([]byte, n)
|
||||||
copy(tmp, b[src:])
|
copy(tmp, b[src:])
|
||||||
b = b[:len(b)+n]
|
b = b[:len(b)+n]
|
||||||
|
|
|
||||||
|
|
@ -453,6 +453,10 @@ func (ws *workerServer) fuzz(ctx context.Context, args fuzzArgs) fuzzResponse {
|
||||||
mem := <-ws.memMu
|
mem := <-ws.memMu
|
||||||
defer func() { ws.memMu <- mem }()
|
defer func() { ws.memMu <- mem }()
|
||||||
|
|
||||||
|
vals, err := unmarshalCorpusFile(mem.valueCopy())
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
|
@ -460,10 +464,11 @@ func (ws *workerServer) fuzz(ctx context.Context, args fuzzArgs) fuzzResponse {
|
||||||
// real heuristic once we have one.
|
// real heuristic once we have one.
|
||||||
return fuzzResponse{Interesting: true}
|
return fuzzResponse{Interesting: true}
|
||||||
default:
|
default:
|
||||||
b := mem.valueRef()
|
vals = ws.m.mutate(vals, cap(mem.valueRef()))
|
||||||
ws.m.mutate(&b)
|
b := marshalCorpusFile(vals...)
|
||||||
mem.setValueLen(len(b))
|
mem.setValueLen(len(b))
|
||||||
if err := ws.fuzzFn(CorpusEntry{Data: b}); err != nil {
|
mem.setValue(b)
|
||||||
|
if err := ws.fuzzFn(CorpusEntry{Values: vals}); err != nil {
|
||||||
return fuzzResponse{Err: err.Error()}
|
return fuzzResponse{Err: err.Error()}
|
||||||
}
|
}
|
||||||
// TODO(jayconrod,katiehockman): return early if we find an
|
// TODO(jayconrod,katiehockman): return early if we find an
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -59,6 +60,7 @@ var _ TB = (*F)(nil)
|
||||||
type corpusEntry = struct {
|
type corpusEntry = struct {
|
||||||
Name string
|
Name string
|
||||||
Data []byte
|
Data []byte
|
||||||
|
Values []interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup registers a function to be called when the test and all its
|
// Cleanup registers a function to be called when the test and all its
|
||||||
|
|
@ -183,20 +185,35 @@ func (f *F) TempDir() string {
|
||||||
// be a no-op if called after or within the Fuzz function. The args must match
|
// be a no-op if called after or within the Fuzz function. The args must match
|
||||||
// those in the Fuzz function.
|
// those in the Fuzz function.
|
||||||
func (f *F) Add(args ...interface{}) {
|
func (f *F) Add(args ...interface{}) {
|
||||||
if len(args) == 0 {
|
var values []interface{}
|
||||||
panic("testing: Add must have at least one argument")
|
for i := range args {
|
||||||
|
if t := reflect.TypeOf(args[i]); !supportedTypes[t] {
|
||||||
|
panic(fmt.Sprintf("testing: unsupported type to Add %v", t))
|
||||||
}
|
}
|
||||||
if len(args) != 1 {
|
values = append(values, args[i])
|
||||||
// TODO: support more than one argument
|
|
||||||
panic("testing: Add only supports one argument currently")
|
|
||||||
}
|
|
||||||
switch v := args[0].(type) {
|
|
||||||
case []byte:
|
|
||||||
f.corpus = append(f.corpus, corpusEntry{Data: v})
|
|
||||||
// TODO: support other types
|
|
||||||
default:
|
|
||||||
panic("testing: Add only supports []byte currently")
|
|
||||||
}
|
}
|
||||||
|
f.corpus = append(f.corpus, corpusEntry{Values: values})
|
||||||
|
}
|
||||||
|
|
||||||
|
// supportedTypes represents all of the supported types which can be fuzzed.
|
||||||
|
var supportedTypes = map[reflect.Type]bool{
|
||||||
|
reflect.TypeOf(([]byte)("")): true,
|
||||||
|
reflect.TypeOf((string)("")): true,
|
||||||
|
reflect.TypeOf((bool)(false)): true,
|
||||||
|
reflect.TypeOf((byte)(0)): true,
|
||||||
|
reflect.TypeOf((rune)(0)): true,
|
||||||
|
reflect.TypeOf((float32)(0)): true,
|
||||||
|
reflect.TypeOf((float64)(0)): true,
|
||||||
|
reflect.TypeOf((int)(0)): true,
|
||||||
|
reflect.TypeOf((int8)(0)): true,
|
||||||
|
reflect.TypeOf((int16)(0)): true,
|
||||||
|
reflect.TypeOf((int32)(0)): true,
|
||||||
|
reflect.TypeOf((int64)(0)): true,
|
||||||
|
reflect.TypeOf((uint)(0)): true,
|
||||||
|
reflect.TypeOf((uint8)(0)): true,
|
||||||
|
reflect.TypeOf((uint16)(0)): true,
|
||||||
|
reflect.TypeOf((uint32)(0)): true,
|
||||||
|
reflect.TypeOf((uint64)(0)): true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fuzz runs the fuzz function, ff, for fuzz testing. If ff fails for a set of
|
// Fuzz runs the fuzz function, ff, for fuzz testing. If ff fails for a set of
|
||||||
|
|
@ -210,15 +227,30 @@ func (f *F) Fuzz(ff interface{}) {
|
||||||
panic("testing: F.Fuzz called more than once")
|
panic("testing: F.Fuzz called more than once")
|
||||||
}
|
}
|
||||||
f.fuzzCalled = true
|
f.fuzzCalled = true
|
||||||
|
|
||||||
fn, ok := ff.(func(*T, []byte))
|
|
||||||
if !ok {
|
|
||||||
panic("testing: Fuzz function must have type func(*testing.T, []byte)")
|
|
||||||
}
|
|
||||||
f.Helper()
|
f.Helper()
|
||||||
|
|
||||||
|
// ff should be in the form func(*testing.T, ...interface{})
|
||||||
|
fn := reflect.ValueOf(ff)
|
||||||
|
fnType := fn.Type()
|
||||||
|
if fnType.Kind() != reflect.Func {
|
||||||
|
panic("testing: F.Fuzz must receive a function")
|
||||||
|
}
|
||||||
|
if fnType.NumIn() < 2 || fnType.In(0) != reflect.TypeOf((*T)(nil)) {
|
||||||
|
panic("testing: F.Fuzz function must receive at least two arguments, where the first argument is a *T")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the types of the function to compare against the corpus.
|
||||||
|
var types []reflect.Type
|
||||||
|
for i := 1; i < fnType.NumIn(); i++ {
|
||||||
|
t := fnType.In(i)
|
||||||
|
if !supportedTypes[t] {
|
||||||
|
panic(fmt.Sprintf("testing: unsupported type for fuzzing %v", t))
|
||||||
|
}
|
||||||
|
types = append(types, t)
|
||||||
|
}
|
||||||
|
|
||||||
// Load seed corpus
|
// Load seed corpus
|
||||||
c, err := f.fuzzContext.readCorpus(filepath.Join(corpusDir, f.name))
|
c, err := f.fuzzContext.readCorpus(filepath.Join(corpusDir, f.name), types)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
f.Fatal(err)
|
f.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
@ -231,6 +263,11 @@ func (f *F) Fuzz(ff interface{}) {
|
||||||
// TODO(jayconrod,katiehockman): dedupe testdata corpus with entries from f.Add
|
// TODO(jayconrod,katiehockman): dedupe testdata corpus with entries from f.Add
|
||||||
// TODO(jayconrod,katiehockman): handle T.Parallel calls within fuzz function.
|
// TODO(jayconrod,katiehockman): handle T.Parallel calls within fuzz function.
|
||||||
run := func(e corpusEntry) error {
|
run := func(e corpusEntry) error {
|
||||||
|
if e.Values == nil {
|
||||||
|
// Every code path should have already unmarshaled Data into Values.
|
||||||
|
// It's our fault if it didn't.
|
||||||
|
panic(fmt.Sprintf("corpus file %q was not unmarshaled", e.Name))
|
||||||
|
}
|
||||||
testName, ok, _ := f.testContext.match.fullName(&f.common, e.Name)
|
testName, ok, _ := f.testContext.match.fullName(&f.common, e.Name)
|
||||||
if !ok || shouldFailFast() {
|
if !ok || shouldFailFast() {
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -257,7 +294,13 @@ func (f *F) Fuzz(ff interface{}) {
|
||||||
t.chatty.Updatef(t.name, "=== RUN %s\n", t.name)
|
t.chatty.Updatef(t.name, "=== RUN %s\n", t.name)
|
||||||
}
|
}
|
||||||
f.inFuzzFn = true
|
f.inFuzzFn = true
|
||||||
go tRunner(t, func(t *T) { fn(t, e.Data) })
|
go tRunner(t, func(t *T) {
|
||||||
|
args := []reflect.Value{reflect.ValueOf(t)}
|
||||||
|
for _, v := range e.Values {
|
||||||
|
args = append(args, reflect.ValueOf(v))
|
||||||
|
}
|
||||||
|
fn.Call(args)
|
||||||
|
})
|
||||||
<-t.signal
|
<-t.signal
|
||||||
f.inFuzzFn = false
|
f.inFuzzFn = false
|
||||||
if t.Failed() {
|
if t.Failed() {
|
||||||
|
|
@ -273,7 +316,7 @@ func (f *F) Fuzz(ff interface{}) {
|
||||||
// actual fuzzing.
|
// actual fuzzing.
|
||||||
corpusTargetDir := filepath.Join(corpusDir, f.name)
|
corpusTargetDir := filepath.Join(corpusDir, f.name)
|
||||||
cacheTargetDir := filepath.Join(*fuzzCacheDir, f.name)
|
cacheTargetDir := filepath.Join(*fuzzCacheDir, f.name)
|
||||||
err := f.fuzzContext.coordinateFuzzing(*fuzzDuration, *parallel, f.corpus, corpusTargetDir, cacheTargetDir)
|
err := f.fuzzContext.coordinateFuzzing(*fuzzDuration, *parallel, f.corpus, types, corpusTargetDir, cacheTargetDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
f.result = FuzzResult{Error: err}
|
f.result = FuzzResult{Error: err}
|
||||||
f.Error(err)
|
f.Error(err)
|
||||||
|
|
@ -365,9 +408,9 @@ type fuzzCrashError interface {
|
||||||
// fuzzContext holds all fields that are common to all fuzz targets.
|
// fuzzContext holds all fields that are common to all fuzz targets.
|
||||||
type fuzzContext struct {
|
type fuzzContext struct {
|
||||||
importPath func() string
|
importPath func() string
|
||||||
coordinateFuzzing func(time.Duration, int, []corpusEntry, string, string) error
|
coordinateFuzzing func(time.Duration, int, []corpusEntry, []reflect.Type, string, string) error
|
||||||
runFuzzWorker func(func(corpusEntry) error) error
|
runFuzzWorker func(func(corpusEntry) error) error
|
||||||
readCorpus func(string) ([]corpusEntry, error)
|
readCorpus func(string, []reflect.Type) ([]corpusEntry, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// runFuzzTargets runs the fuzz targets matching the pattern for -run. This will
|
// runFuzzTargets runs the fuzz targets matching the pattern for -run. This will
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime/pprof"
|
"runtime/pprof"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -132,7 +133,7 @@ func (TestDeps) SetPanicOnExit0(v bool) {
|
||||||
testlog.SetPanicOnExit0(v)
|
testlog.SetPanicOnExit0(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (TestDeps) CoordinateFuzzing(timeout time.Duration, parallel int, seed []fuzz.CorpusEntry, corpusDir, cacheDir string) (err error) {
|
func (TestDeps) CoordinateFuzzing(timeout time.Duration, parallel int, seed []fuzz.CorpusEntry, types []reflect.Type, corpusDir, cacheDir string) (err error) {
|
||||||
// Fuzzing may be interrupted with a timeout or if the user presses ^C.
|
// Fuzzing may be interrupted with a timeout or if the user presses ^C.
|
||||||
// In either case, we'll stop worker processes gracefully and save
|
// In either case, we'll stop worker processes gracefully and save
|
||||||
// crashers and interesting values.
|
// crashers and interesting values.
|
||||||
|
|
@ -143,7 +144,7 @@ func (TestDeps) CoordinateFuzzing(timeout time.Duration, parallel int, seed []fu
|
||||||
ctx, stop := signal.NotifyContext(ctx, os.Interrupt)
|
ctx, stop := signal.NotifyContext(ctx, os.Interrupt)
|
||||||
defer stop()
|
defer stop()
|
||||||
defer cancel()
|
defer cancel()
|
||||||
err = fuzz.CoordinateFuzzing(ctx, parallel, seed, corpusDir, cacheDir)
|
err = fuzz.CoordinateFuzzing(ctx, parallel, seed, types, corpusDir, cacheDir)
|
||||||
if err == ctx.Err() {
|
if err == ctx.Err() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -168,6 +169,6 @@ func (TestDeps) RunFuzzWorker(fn func(fuzz.CorpusEntry) error) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (TestDeps) ReadCorpus(dir string) ([]fuzz.CorpusEntry, error) {
|
func (TestDeps) ReadCorpus(dir string, types []reflect.Type) ([]fuzz.CorpusEntry, error) {
|
||||||
return fuzz.ReadCorpus(dir)
|
return fuzz.ReadCorpus(dir, types)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -243,6 +243,7 @@ import (
|
||||||
"internal/race"
|
"internal/race"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"runtime/trace"
|
"runtime/trace"
|
||||||
|
|
@ -1324,11 +1325,13 @@ func (f matchStringOnly) ImportPath() string { return "
|
||||||
func (f matchStringOnly) StartTestLog(io.Writer) {}
|
func (f matchStringOnly) StartTestLog(io.Writer) {}
|
||||||
func (f matchStringOnly) StopTestLog() error { return errMain }
|
func (f matchStringOnly) StopTestLog() error { return errMain }
|
||||||
func (f matchStringOnly) SetPanicOnExit0(bool) {}
|
func (f matchStringOnly) SetPanicOnExit0(bool) {}
|
||||||
func (f matchStringOnly) CoordinateFuzzing(time.Duration, int, []corpusEntry, string, string) error {
|
func (f matchStringOnly) CoordinateFuzzing(time.Duration, int, []corpusEntry, []reflect.Type, string, string) error {
|
||||||
return errMain
|
return errMain
|
||||||
}
|
}
|
||||||
func (f matchStringOnly) RunFuzzWorker(func(corpusEntry) error) error { return errMain }
|
func (f matchStringOnly) RunFuzzWorker(func(corpusEntry) error) error { return errMain }
|
||||||
func (f matchStringOnly) ReadCorpus(string) ([]corpusEntry, error) { return nil, errMain }
|
func (f matchStringOnly) ReadCorpus(string, []reflect.Type) ([]corpusEntry, error) {
|
||||||
|
return nil, errMain
|
||||||
|
}
|
||||||
|
|
||||||
// Main is an internal function, part of the implementation of the "go test" command.
|
// Main is an internal function, part of the implementation of the "go test" command.
|
||||||
// It was exported because it is cross-package and predates "internal" packages.
|
// It was exported because it is cross-package and predates "internal" packages.
|
||||||
|
|
@ -1371,9 +1374,9 @@ type testDeps interface {
|
||||||
StartTestLog(io.Writer)
|
StartTestLog(io.Writer)
|
||||||
StopTestLog() error
|
StopTestLog() error
|
||||||
WriteProfileTo(string, io.Writer, int) error
|
WriteProfileTo(string, io.Writer, int) error
|
||||||
CoordinateFuzzing(time.Duration, int, []corpusEntry, string, string) error
|
CoordinateFuzzing(time.Duration, int, []corpusEntry, []reflect.Type, string, string) error
|
||||||
RunFuzzWorker(func(corpusEntry) error) error
|
RunFuzzWorker(func(corpusEntry) error) error
|
||||||
ReadCorpus(string) ([]corpusEntry, error)
|
ReadCorpus(string, []reflect.Type) ([]corpusEntry, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MainStart is meant for use by tests generated by 'go test'.
|
// MainStart is meant for use by tests generated by 'go test'.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue