[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 go test -c -fuzz=. # Build using shared build cache for speed. env GOCACHE=$WORK/gocache # 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=FuzzMinimizerCrashInMinimization -test.fuzztime=10000x -test.parallel=1 ! stdout '^ok' stdout -count=1 'got the minimum size!' stdout -count=1 'flaky failure' 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 50 # 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=FuzzMinimizerNonrecoverableCrashInMinimization -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 100 -- 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" "io" "os" "strings" "testing" "unicode/utf8" ) func FuzzMinimizerCrashInMinimization(f *testing.F) { seed := strings.Repeat("A", 1000) f.Add(seed) i := 3 f.Fuzz(func(t *testing.T, s string) { if len(s) < 50 || len(s) > 1100 { // Make sure that b is large enough that it can be minimized return } if s != seed { // This should hit a new edge, and the interesting input // should attempt minimization Y(io.Discard, s) } if i > 0 { // Don't let it fail right away. i-- } else if utf8.RuneCountInString(s) == len(s) && len(s) <= 100 { // Make sure this only fails if the number of bytes in the // marshaled string is the same as the unmarshaled string, // so that we can check the length of the testdata file. t.Error("flaky failure") if len(s) == 50 { t.Error("got the minimum size!") } } }) } func FuzzMinimizerNonrecoverableCrashInMinimization(f *testing.F) { seed := strings.Repeat("A", 1000) f.Add(seed) i := 3 f.Fuzz(func(t *testing.T, s string) { if len(s) < 50 || len(s) > 1100 { return } if s != seed { Y(io.Discard, s) } if i > 0 { i-- } else if utf8.RuneCountInString(s) == len(s) && len(s) <= 100 { os.Exit(19) } }) } 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 ( "fmt" "io/ioutil" "os" "path/filepath" "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) } fname := files[0].Name() contents, err := ioutil.ReadFile(filepath.Join(dir, fname)) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } contentsLen := len(contents) - len(`go test fuzz v1 string("") `) if got, want := contentsLen, wantLen; got > want { fmt.Fprintf(os.Stderr, "expect length <= %d, got %d\n", want, got) os.Exit(1) } fmt.Fprintf(os.Stderr, "%s\n", contents) } -- 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\(([^)]+)\)$`)