// Copyright 2020 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package testing import ( "errors" "flag" "fmt" "os" "runtime" "time" ) func initFuzzFlags() { matchFuzz = flag.String("test.fuzz", "", "run the fuzz target matching `regexp`") isFuzzWorker = flag.Bool("test.fuzzworker", false, "coordinate with the parent process to fuzz random values") } var ( matchFuzz *string isFuzzWorker *bool ) // InternalFuzzTarget is an internal type but exported because it is cross-package; // it is part of the implementation of the "go test" command. type InternalFuzzTarget struct { Name string Fn func(f *F) } // F is a type passed to fuzz targets for fuzz testing. type F struct { common context *fuzzContext corpus []corpusEntry // corpus is the in-memory corpus result FuzzResult // result is the result of running the fuzz target fuzzFunc func(f *F) // fuzzFunc is the function which makes up the fuzz target } // corpus corpusEntry type corpusEntry struct { b []byte } func bytesToCorpus(bytes [][]byte) []corpusEntry { c := make([]corpusEntry, len(bytes)) for i, b := range bytes { c[i].b = b } return c } // Add will add the arguments to the seed corpus for the fuzz target. This will // be a no-op if called after or within the Fuzz function. The args must match // those in the Fuzz function. func (f *F) Add(args ...interface{}) { if len(args) == 0 { panic("testing: Add must have at least one argument") } if len(args) != 1 { // 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{v}) // TODO: support other types default: panic("testing: Add only supports []byte currently") } } // Fuzz runs the fuzz function, ff, for fuzz testing. If ff fails for a set of // arguments, those arguments will be added to the seed corpus. // // This is a terminal function which will terminate the currently running fuzz // target by calling runtime.Goexit. To run any code after this function, use // Cleanup. func (f *F) Fuzz(ff interface{}) { defer runtime.Goexit() // exit after this function fn, ok := ff.(func(*T, []byte)) if !ok { panic("testing: Fuzz function must have type func(*testing.T, []byte)") } // Load seed corpus c, err := f.context.readCorpus(f.name) if err != nil { f.Fatal(err) } f.corpus = append(f.corpus, bytesToCorpus(c)...) // TODO(jayconrod,katiehockman): dedupe testdata corpus with entries from f.Add var errStr string run := func(t *T, b []byte) { defer func() { err := recover() // If the function has recovered but the test hasn't finished, // it is due to a nil panic or runtime.GoExit. if !t.finished && err == nil { err = errNilPanicOrGoexit } if err != nil { t.Fail() t.output = []byte(fmt.Sprintf(" %s", err)) } f.inFuzzFn = false t.signal <- true // signal that the test has finished }() // TODO(katiehockman, jayconrod): consider replacing inFuzzFn with // general purpose flag that checks whether specific methods can be // called. f.inFuzzFn = true fn(t, b) t.finished = true } switch { case f.context.coordinateFuzzing != nil: // Fuzzing is enabled, and this is the test process started by 'go test'. // Act as the coordinator process, and coordinate workers to perform the // actual fuzzing. seed := make([][]byte, len(f.corpus)) for i, e := range f.corpus { seed[i] = e.b } err := f.context.coordinateFuzzing(*parallel, seed) f.setRan() f.finished = true f.result = FuzzResult{Error: err} // TODO(jayconrod,katiehockman): Aggregate statistics across workers // and set FuzzResult properly. case f.context.runFuzzWorker != nil: // Fuzzing is enabled, and this is a worker process. Follow instructions // from the coordinator. err := f.context.runFuzzWorker(func(input []byte) error { t := &T{ common: common{ signal: make(chan bool), w: f.w, chatty: f.chatty, }, context: newTestContext(1, nil), } go run(t, input) <-t.signal if t.Failed() { return errors.New(string(t.output)) } return nil }) if err != nil { // TODO(jayconrod,katiehockman): how should we handle a failure to // communicate with the coordinator? Might be caused by the coordinator // terminating early. fmt.Fprintf(os.Stderr, "testing: communicating with fuzz coordinator: %v\n", err) os.Exit(1) } f.setRan() f.finished = true default: // Fuzzing is not enabled. Only run the seed corpus. for _, c := range f.corpus { t := &T{ common: common{ signal: make(chan bool), w: f.w, chatty: f.chatty, }, context: newTestContext(1, nil), } go run(t, c.b) <-t.signal if t.Failed() { f.Fail() errStr += string(t.output) } f.setRan() } f.finished = true if f.Failed() { f.result = FuzzResult{Error: errors.New(errStr)} return } } } func (f *F) report() { if *isFuzzWorker { return } if f.Failed() { fmt.Fprintf(f.w, "--- FAIL: %s\n%s\n", f.name, f.result.String()) } else if f.chatty != nil { if f.Skipped() { f.chatty.Updatef(f.name, "SKIP\n") } else { f.chatty.Updatef(f.name, "PASS\n") } } } // run runs each fuzz target in its own goroutine with its own *F. func (f *F) run(ft InternalFuzzTarget) (ran, ok bool) { f = &F{ common: common{ signal: make(chan bool), name: ft.Name, chatty: f.chatty, w: f.w, }, context: f.context, } if f.chatty != nil { f.chatty.Updatef(ft.Name, "=== RUN %s\n", ft.Name) } go f.runTarget(ft.Fn) <-f.signal return f.ran, !f.failed } // runTarget runs the given target, handling panics and exits // within the test, and reporting errors. func (f *F) runTarget(fn func(*F)) { defer func() { err := recover() // If the function has recovered but the test hasn't finished, // it is due to a nil panic or runtime.GoExit. if !f.finished && err == nil { err = errNilPanicOrGoexit } if err != nil { f.Fail() f.result = FuzzResult{Error: fmt.Errorf(" %s", err)} } f.report() f.setRan() f.signal <- true // signal that the test has finished }() defer f.runCleanup(normalPanic) fn(f) f.finished = true } // FuzzResult contains the results of a fuzz run. type FuzzResult struct { N int // The number of iterations. T time.Duration // The total time taken. Crasher *corpusEntry // Crasher is the corpus entry that caused the crash Error error // Error is the error from the crash } func (r FuzzResult) String() string { s := "" if r.Error == nil { return s } s = fmt.Sprintf("%s", r.Error.Error()) if r.Crasher != nil { s += fmt.Sprintf("\ncrasher: %b", r.Crasher) } return s } // fuzzContext holds all fields that are common to all fuzz targets. type fuzzContext struct { runMatch *matcher fuzzMatch *matcher coordinateFuzzing func(int, [][]byte) error runFuzzWorker func(func([]byte) error) error readCorpus func(string) ([][]byte, error) } // runFuzzTargets runs the fuzz targets matching the pattern for -run. This will // only run the f.Fuzz function for each seed corpus without using the fuzzing // engine to generate or mutate inputs. func runFuzzTargets(deps testDeps, fuzzTargets []InternalFuzzTarget) (ran, ok bool) { ok = true if len(fuzzTargets) == 0 || *isFuzzWorker { return ran, ok } ctx := &fuzzContext{ runMatch: newMatcher(deps.MatchString, *match, "-test.run"), readCorpus: deps.ReadCorpus, } var fts []InternalFuzzTarget for _, ft := range fuzzTargets { if _, matched, _ := ctx.runMatch.fullName(nil, ft.Name); matched { fts = append(fts, ft) } } f := &F{ common: common{ w: os.Stdout, }, fuzzFunc: func(f *F) { for _, ft := range fts { // Run each fuzz target in it's own goroutine. ftRan, ftOk := f.run(ft) ran = ran || ftRan ok = ok && ftOk } }, context: ctx, } if Verbose() { f.chatty = newChattyPrinter(f.w) } f.fuzzFunc(f) return ran, ok } // runFuzzing runs the fuzz target matching the pattern for -fuzz. Only one such // fuzz target must match. This will run the fuzzing engine to generate and // mutate new inputs against the f.Fuzz function. // // If fuzzing is disabled (-test.fuzz is not set), runFuzzing // returns immediately. func runFuzzing(deps testDeps, fuzzTargets []InternalFuzzTarget) (ran, ok bool) { if len(fuzzTargets) == 0 || *matchFuzz == "" { return false, true } ctx := &fuzzContext{ fuzzMatch: newMatcher(deps.MatchString, *matchFuzz, "-test.fuzz"), readCorpus: deps.ReadCorpus, } if *isFuzzWorker { ctx.runFuzzWorker = deps.RunFuzzWorker } else { ctx.coordinateFuzzing = deps.CoordinateFuzzing } f := &F{ common: common{ signal: make(chan bool), w: os.Stdout, }, context: ctx, } var target *InternalFuzzTarget for i := range fuzzTargets { ft := &fuzzTargets[i] testName, matched, _ := ctx.fuzzMatch.fullName(&f.common, ft.Name) if !matched { continue } if target != nil { fmt.Fprintln(os.Stderr, "testing: warning: -fuzz matches more than one target, won't fuzz") return false, true } target = ft f.name = testName } if target == nil { return false, true } if Verbose() { f.chatty = newChattyPrinter(f.w) if !*isFuzzWorker { f.chatty.Updatef(f.name, "--- FUZZ: %s\n", f.name) } } go f.runTarget(target.Fn) <-f.signal return f.ran, !f.failed }