mirror of https://github.com/golang/go.git
371 lines
9.4 KiB
Go
371 lines
9.4 KiB
Go
// 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
|
|
}
|