[short] skip [!fuzz-instrumented] skip # Test that when an interesting value is discovered (one that expands coverage), # the fuzzing engine minimizes it before writing it to the cache. # # The program below starts with a seed value of length 100, but more coverage # will be found for any value other than the seed. We should end with a value # in the cache of length 1 (the minimizer currently does not produce empty # strings). check_cache.go confirms that. # # We would like to verify that ALL values in the cache were minimized to a # length of 1, but this isn't always possible when new coverage is found in # functions called by testing or internal/fuzz in the background. go test -c -fuzz=. # Build using shared build cache for speed. env GOCACHE=$WORK/gocache exec ./fuzz.test$GOEXE -test.fuzzcachedir=$GOCACHE/fuzz -test.fuzz=FuzzMinCache -test.fuzztime=1000x go run check_cache/check_cache.go $GOCACHE/fuzz/FuzzMinCache # Test that minimization occurs for a crash that appears while minimizing a # newly found interesting input. There must be only one worker for this test to # be flaky like we want. ! exec ./fuzz.test$GOEXE -test.fuzzcachedir=$GOCACHE/fuzz -test.fuzz=FuzzMinimizerCrashInMinimization -test.run=^$ -test.fuzztime=10000x -test.parallel=1 ! stdout '^ok' stdout -count=1 'got the minimum size!' stdout -count=1 'bad input' stdout FAIL # Check that the input written to testdata will reproduce the error, and is the # smallest possible. go run check_testdata/check_testdata.go FuzzMinimizerCrashInMinimization 1 # Test that a nonrecoverable error that occurs while minimizing an interesting # input is reported correctly. ! exec ./fuzz.test$GOEXE -test.fuzzcachedir=$GOCACHE/fuzz -test.fuzz=FuzzMinimizerNonrecoverableCrashInMinimization -test.run=^$ -test.fuzztime=10000x -test.parallel=1 ! stdout '^ok' stdout -count=1 'fuzzing process hung or terminated unexpectedly while minimizing' stdout -count=1 'EOF' stdout FAIL # Check that the input written to testdata will reproduce the error. go run check_testdata/check_testdata.go FuzzMinimizerNonrecoverableCrashInMinimization 1 -- go.mod -- module fuzz go 1.17 -- y.go -- package fuzz import ( "bytes" "io" ) func Y(w io.Writer, s string) { if !bytes.Equal([]byte(s), []byte("y")) { w.Write([]byte("not equal")) } } -- fuzz_test.go -- package fuzz import ( "bytes" "os" "testing" ) func FuzzMinimizerCrashInMinimization(f *testing.F) { seed := bytes.Repeat([]byte{255}, 100) f.Add(seed) f.Fuzz(func(t *testing.T, b []byte) { if bytes.Equal(seed, b) { return } t.Error("bad input") if len(b) == 1 { t.Error("got the minimum size!") } }) } var fuzzing bool func FuzzMinimizerNonrecoverableCrashInMinimization(f *testing.F) { seed := bytes.Repeat([]byte{255}, 100) f.Add(seed) f.Fuzz(func(t *testing.T, b []byte) { if bytes.Equal(seed, b) { return } else if len(b) == 1 { os.Exit(1) } }) } func FuzzMinCache(f *testing.F) { seed := bytes.Repeat([]byte("a"), 20) f.Add(seed) f.Fuzz(func(t *testing.T, buf []byte) { if bytes.Equal(buf, seed) { return } }) } -- check_testdata/check_testdata.go -- //go:build ignore // +build ignore // check_testdata.go checks that the string written // is not longer than the provided length. package main import ( "bytes" "fmt" "io/ioutil" "os" "path/filepath" "regexp" "strconv" ) func main() { wantLen, err := strconv.Atoi(os.Args[2]) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } testName := os.Args[1] dir := filepath.Join("testdata/fuzz", testName) files, err := ioutil.ReadDir(dir) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } if len(files) == 0 { fmt.Fprintf(os.Stderr, "expect at least one failure to be written to testdata\n") os.Exit(1) } for _, f := range files { data, err := ioutil.ReadFile(filepath.Join(dir, f.Name())) if err != nil { panic(err) } var containsVal bool for _, line := range bytes.Split(data, []byte("\n")) { m := valRe.FindSubmatch(line) if m == nil { continue } containsVal = true s, err := strconv.Unquote(string(m[1])) if err != nil { panic(err) } if len(s) != wantLen { fmt.Fprintf(os.Stderr, "expect length %d, got %d (%q)\n", wantLen, len(s), line) os.Exit(1) } } if !containsVal { fmt.Fprintln(os.Stderr, "corpus file contained no values") os.Exit(1) } } } var valRe = regexp.MustCompile(`^\[\]byte\(([^)]+)\)$`) -- check_cache/check_cache.go -- //go:build ignore // +build ignore // check_cache.go checks that each file in the cached corpus has a []byte // of length at most 1. This verifies that at least one cached input is minimized. package main import ( "bytes" "fmt" "os" "path/filepath" "regexp" "strconv" ) func main() { dir := os.Args[1] ents, err := os.ReadDir(dir) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } for _, ent := range ents { name := filepath.Join(dir, ent.Name()) if good, err := checkCacheFile(name); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } else if good { os.Exit(0) } } fmt.Fprintln(os.Stderr, "no cached inputs were minimized") os.Exit(1) } func checkCacheFile(name string) (good bool, err error) { data, err := os.ReadFile(name) if err != nil { return false, err } for _, line := range bytes.Split(data, []byte("\n")) { m := valRe.FindSubmatch(line) if m == nil { continue } if s, err := strconv.Unquote(string(m[1])); err != nil { return false, err } else if len(s) <= 1 { return true, nil } } return false, nil } var valRe = regexp.MustCompile(`^\[\]byte\(([^)]+)\)$`)