"cpuprofile": true,
"failfast": true,
"fuzz": true,
+ "fuzztime": true,
"list": true,
"memprofile": true,
"memprofilerate": true,
name := strings.TrimPrefix(f.Name, "test.")
switch name {
- case "testlogfile", "paniconexit0":
+ case "testlogfile", "paniconexit0", "fuzzcachedir", "fuzzworker":
// These flags are only for use by cmd/go.
default:
names = append(names, name)
cf.String("run", "", "")
cf.Bool("short", false, "")
cf.DurationVar(&testTimeout, "timeout", 10*time.Minute, "")
+ cf.Duration("fuzztime", 0, "")
cf.StringVar(&testTrace, "trace", "", "")
cf.BoolVar(&testV, "v", false, "")
# Test that calling f.Error in a fuzz target causes a non-zero exit status.
-! go test -fuzz Fuzz error_fuzz_test.go
+! go test -fuzz=Fuzz -fuzztime=5s -parallel=1 error_fuzz_test.go
! stdout ^ok
stdout FAIL
! stdout FAIL
# Test that calling f.Fatal while fuzzing causes a non-zero exit status.
-! go test -fuzz Fuzz fatal_fuzz_test.go
+! go test -fuzz=Fuzz -fuzztime=5s -parallel=1 fatal_fuzz_test.go
! stdout ^ok
stdout FAIL
# Test that successful fuzzing exits cleanly.
-go test -fuzz Fuzz success_fuzz_test.go
+go test -fuzz=Fuzz -fuzztime=5s -parallel=1 success_fuzz_test.go
stdout ok
! stdout FAIL
! exists $GOCACHE/fuzz
# Fuzzing should write interesting values to the cache.
-go test -fuzz=FuzzY -parallel=1 .
+go test -fuzz=FuzzY -fuzztime=5s -parallel=1 .
go run ./contains_files $GOCACHE/fuzz/example.com/y/FuzzY
# 'go clean -cache' should not delete the fuzz cache.
--- /dev/null
+[short] skip
+
+# There are no seed values, so 'go test' should finish quickly.
+go test
+
+# Fuzzing should exit 0 when after fuzztime, even if timeout is short.
+go test -timeout=10ms -fuzz=FuzzFast -fuzztime=5s -parallel=1
+
+# We should see the same behavior when invoking the test binary directly.
+go test -c
+exec ./fuzz.test$GOEXE -test.timeout=10ms -test.fuzz=FuzzFast -test.fuzztime=5s -test.parallel=1 -test.fuzzcachedir=$WORK/cache
+
+# Timeout should not cause inputs to be written as crashers.
+! exists testdata/corpus
+
+-- go.mod --
+module fuzz
+
+go 1.16
+-- fuzz_test.go --
+package fuzz_test
+
+import "testing"
+
+func FuzzFast(f *testing.F) {
+ f.Fuzz(func (*testing.T, []byte) {})
+}
stdout '^ok'
# Matches only for fuzzing.
-go test -fuzz Fuzz standalone_fuzz_test.go
+go test -fuzz Fuzz -fuzztime 5s -parallel 1 standalone_fuzz_test.go
! stdout '^ok.*\[no tests to run\]'
stdout '^ok'
# Matches none for fuzzing but will run the fuzz target as a test.
-go test -fuzz ThisWillNotMatch standalone_fuzz_test.go
+go test -fuzz ThisWillNotMatch -fuzztime 5s -parallel 1 standalone_fuzz_test.go
! stdout '^ok.*\[no tests to run\]'
stdout ok
stdout '\[no targets to fuzz\]'
! stdout '\[no targets to fuzz\]'
# Matches more than one fuzz target for fuzzing.
-go test -fuzz Fuzz multiple_fuzz_test.go
+go test -fuzz Fuzz -fuzztime 5s -parallel 1 multiple_fuzz_test.go
# The tests should run, but not be fuzzed
! stdout '\[no tests to run\]'
! stdout '\[no targets to fuzz\]'
[short] skip
-go test -fuzz=FuzzA -parallel=1 -log=fuzz
+go test -fuzz=FuzzA -fuzztime=5s -parallel=1 -log=fuzz
go run check_logs.go fuzz fuzz.worker
-- go.mod --
go test -parallel=1
# Running the fuzzer should find a crashing input quickly.
-! go test -fuzz=FuzzWithBug -parallel=1
+! go test -fuzz=FuzzWithBug -fuzztime=5s -parallel=1
stdout 'testdata[/\\]corpus[/\\]FuzzWithBug[/\\]fb8e20fc2e4c3f248c60c39bd652f3c1347298bb977b8b4d5903b85055620603'
stdout 'this input caused a crash!'
grep '\Aab\z' testdata/corpus/FuzzWithBug/fb8e20fc2e4c3f248c60c39bd652f3c1347298bb977b8b4d5903b85055620603
# the target, and should fail when run without fuzzing.
! go test -parallel=1
-! go test -run=FuzzWithNilPanic -fuzz=FuzzWithNilPanic -parallel=1
+! go test -run=FuzzWithNilPanic -fuzz=FuzzWithNilPanic -fuzztime=5s -parallel=1
stdout 'testdata[/\\]corpus[/\\]FuzzWithNilPanic[/\\]f45de51cdef30991551e41e882dd7b5404799648a0a00753f44fc966e6153fc1'
stdout 'runtime.Goexit'
grep '\Aac\z' testdata/corpus/FuzzWithNilPanic/f45de51cdef30991551e41e882dd7b5404799648a0a00753f44fc966e6153fc1
-! go test -run=FuzzWithBadExit -fuzz=FuzzWithBadExit -parallel=1
+! go test -run=FuzzWithBadExit -fuzz=FuzzWithBadExit -fuzztime=5s -parallel=1
stdout 'testdata[/\\]corpus[/\\]FuzzWithBadExit[/\\]70ba33708cbfb103f1a8e34afef333ba7dc021022b2d9aaa583aabb8058d8d67'
stdout 'unexpectedly'
grep '\Aad\z' testdata/corpus/FuzzWithBadExit/70ba33708cbfb103f1a8e34afef333ba7dc021022b2d9aaa583aabb8058d8d67
package fuzz
import (
+ "context"
"crypto/sha256"
"fmt"
"io/ioutil"
"path/filepath"
"runtime"
"sync"
- "time"
)
// CoordinateFuzzing creates several worker processes and communicates with
//
// If a crash occurs, the function will return an error containing information
// about the crash, which can be reported to the user.
-func CoordinateFuzzing(parallel int, seed [][]byte, corpusDir, cacheDir string) (err error) {
+func CoordinateFuzzing(ctx context.Context, parallel int, seed [][]byte, corpusDir, cacheDir string) (err error) {
+ if err := ctx.Err(); err != nil {
+ return err
+ }
if parallel == 0 {
parallel = runtime.GOMAXPROCS(0)
}
- // TODO(jayconrod): support fuzzing indefinitely or with a given duration.
- // The value below is just a placeholder until we figure out how to handle
- // interrupts.
- duration := 5 * time.Second
corpus, err := readCorpusAndCache(seed, corpusDir, cacheDir)
if err != nil {
defer func() {
close(c.doneC)
wg.Wait()
- if err == nil {
- for _, err = range workerErrs {
- if err != nil {
- // Return the first error found.
- return
+ if err == nil || err == ctx.Err() {
+ for _, werr := range workerErrs {
+ if werr != nil {
+ // Return the first error found, replacing ctx.Err() if a more
+ // interesting error is found.
+ err = werr
}
}
}
}()
// Main event loop.
- stopC := time.After(duration)
i := 0
for {
select {
- // TODO(jayconrod): handle interruptions like SIGINT.
-
- case <-stopC:
- // Time's up.
- return nil
+ case <-ctx.Done():
+ // Interrupted, cancelled, or timed out.
+ // TODO(jayconrod,katiehockman): On Windows, ^C only interrupts 'go test',
+ // not the coordinator or worker processes. 'go test' will stop running
+ // actions, but it won't interrupt its child processes. This makes it
+ // difficult to stop fuzzing on Windows without a timeout.
+ return ctx.Err()
case crasher := <-c.crasherC:
// A worker found a crasher. Write it to testdata and return it.
package fuzz
import (
+ "context"
"encoding/json"
"errors"
"fmt"
args := fuzzArgs{Duration: workerFuzzDuration}
value, resp, err := w.client.fuzz(input.b, args)
if err != nil {
- // TODO(jayconrod): if we get an error here, something failed between
- // main and the call to testing.F.Fuzz. The error here won't
- // be useful. Collect stderr, clean it up, and return that.
- // TODO(jayconrod): we can get EPIPE if w.stop is called concurrently
- // and it kills the worker process. Suppress this message in
- // that case.
+ // Error communicating with worker.
+ select {
+ case <-w.termC:
+ // Worker terminated, perhaps unexpectedly.
+ // We expect I/O errors due to partially sent or received RPCs,
+ // so ignore this error.
+ case <-w.coordinator.doneC:
+ // Timeout or interruption. Worker may also be interrupted.
+ // Again, ignore I/O errors.
+ default:
+ // TODO(jayconrod): if we get an error here, something failed between
+ // main and the call to testing.F.Fuzz. The error here won't
+ // be useful. Collect stderr, clean it up, and return that.
+ // TODO(jayconrod): we can get EPIPE if w.stop is called concurrently
+ // and it kills the worker process. Suppress this message in
+ // that case.
+ fmt.Fprintf(os.Stderr, "communicating with worker: %v\n", err)
+ }
// TODO(jayconrod): what happens if testing.F.Fuzz is never called?
// TODO(jayconrod): time out if the test process hangs.
- fmt.Fprintf(os.Stderr, "communicating with worker: %v\n", err)
} else if resp.Err != "" {
// The worker found a crasher. Inform the coordinator.
crasher := crasherEntry{
//
// RunFuzzWorker returns an error if it could not communicate with the
// coordinator process.
-func RunFuzzWorker(fn func([]byte) error) error {
+func RunFuzzWorker(ctx context.Context, fn func([]byte) error) error {
comm, err := getWorkerComm()
if err != nil {
return err
}
srv := &workerServer{workerComm: comm, fuzzFn: fn}
- return srv.serve()
+ return srv.serve(ctx)
}
// call is serialized and sent from the coordinator on fuzz_in. It acts as
// serve returns errors that occurred when communicating over pipes. serve
// does not return errors from method calls; those are passed through serialized
// responses.
-func (ws *workerServer) serve() error {
+func (ws *workerServer) serve(ctx context.Context) error {
+ // Stop handling messages when ctx.Done() is closed. This normally happens
+ // when the worker process receives a SIGINT signal, which on POSIX platforms
+ // is sent to the process group when ^C is pressed.
+ //
+ // Ordinarily, the coordinator process may stop a worker by closing fuzz_in.
+ // We simulate that and interrupt a blocked read here.
+ doneC := make(chan struct{})
+ defer func() { close(doneC) }()
+ go func() {
+ select {
+ case <-ctx.Done():
+ ws.fuzzIn.Close()
+ case <-doneC:
+ }
+ }()
+
enc := json.NewEncoder(ws.fuzzOut)
dec := json.NewDecoder(ws.fuzzIn)
for {
var c call
- if err := dec.Decode(&c); err == io.EOF {
- return nil
- } else if err != nil {
- return err
+ if err := dec.Decode(&c); err != nil {
+ if ctx.Err() != nil {
+ return ctx.Err()
+ } else if err == io.EOF {
+ return nil
+ } else {
+ return err
+ }
}
var resp interface{}
switch {
case c.Fuzz != nil:
- resp = ws.fuzz(*c.Fuzz)
+ resp = ws.fuzz(ctx, *c.Fuzz)
default:
return errors.New("no arguments provided for any call")
}
// fuzz runs the test function on random variations of a given input value for
// a given amount of time. fuzz returns early if it finds an input that crashes
// the fuzz function or an input that expands coverage.
-func (ws *workerServer) fuzz(args fuzzArgs) fuzzResponse {
- t := time.NewTimer(args.Duration)
+func (ws *workerServer) fuzz(ctx context.Context, args fuzzArgs) fuzzResponse {
+ ctx, cancel := context.WithTimeout(ctx, args.Duration)
+ defer cancel()
+
for {
select {
- case <-t.C:
+ case <-ctx.Done():
// TODO(jayconrod,katiehockman): this value is not interesting. Use a
// real heuristic once we have one.
return fuzzResponse{Interesting: true}
func initFuzzFlags() {
matchFuzz = flag.String("test.fuzz", "", "run the fuzz target matching `regexp`")
+ fuzzDuration = flag.Duration("test.fuzztime", 0, "time to spend fuzzing; default (0) is to run indefinitely")
fuzzCacheDir = flag.String("test.fuzzcachedir", "", "directory where interesting fuzzing inputs are stored")
isFuzzWorker = flag.Bool("test.fuzzworker", false, "coordinate with the parent process to fuzz random values")
}
var (
matchFuzz *string
+ fuzzDuration *time.Duration
fuzzCacheDir *string
isFuzzWorker *bool
}
corpusTargetDir := filepath.Join(corpusDir, f.name)
cacheTargetDir := filepath.Join(*fuzzCacheDir, f.name)
- err := f.context.coordinateFuzzing(*parallel, seed, corpusTargetDir, cacheTargetDir)
+ err := f.context.coordinateFuzzing(*fuzzDuration, *parallel, seed, corpusTargetDir, cacheTargetDir)
if err != nil {
f.Fail()
f.result = FuzzResult{Error: err}
type fuzzContext struct {
runMatch *matcher
fuzzMatch *matcher
- coordinateFuzzing func(int, [][]byte, string, string) error
+ coordinateFuzzing func(time.Duration, int, [][]byte, string, string) error
runFuzzWorker func(func([]byte) error) error
readCorpus func(string) ([][]byte, error)
}
import (
"bufio"
+ "context"
"internal/fuzz"
"internal/testlog"
"io"
+ "os"
+ "os/signal"
"regexp"
"runtime/pprof"
"strings"
"sync"
+ "time"
)
// TestDeps is an implementation of the testing.testDeps interface,
testlog.SetPanicOnExit0(v)
}
-func (TestDeps) CoordinateFuzzing(parallel int, seed [][]byte, corpusDir, cacheDir string) error {
- return fuzz.CoordinateFuzzing(parallel, seed, corpusDir, cacheDir)
+func (TestDeps) CoordinateFuzzing(timeout time.Duration, parallel int, seed [][]byte, corpusDir, cacheDir string) error {
+ // Fuzzing may be interrupted with a timeout or if the user presses ^C.
+ // In either case, we'll stop worker processes gracefully and save
+ // crashers and interesting values.
+ ctx := context.Background()
+ cancel := func() {}
+ if timeout > 0 {
+ ctx, cancel = context.WithTimeout(ctx, timeout)
+ }
+ interruptC := make(chan os.Signal, 1)
+ signal.Notify(interruptC, os.Interrupt)
+ go func() {
+ <-interruptC
+ cancel()
+ }()
+ defer close(interruptC)
+
+ err := fuzz.CoordinateFuzzing(ctx, parallel, seed, corpusDir, cacheDir)
+ if err == ctx.Err() {
+ return nil
+ }
+ return err
}
func (TestDeps) RunFuzzWorker(fn func([]byte) error) error {
- return fuzz.RunFuzzWorker(fn)
+ // Worker processes may or may not receive a signal when the user presses ^C
+ // On POSIX operating systems, a signal sent to a process group is delivered
+ // to all processes in that group. This is not the case on Windows.
+ // If the worker is interrupted, return quickly and without error.
+ // If only the coordinator process is interrupted, it tells each worker
+ // process to stop by closing its "fuzz_in" pipe.
+ ctx, cancel := context.WithCancel(context.Background())
+ interruptC := make(chan os.Signal, 1)
+ signal.Notify(interruptC, os.Interrupt)
+ go func() {
+ <-interruptC
+ cancel()
+ }()
+ defer close(interruptC)
+
+ err := fuzz.RunFuzzWorker(ctx, fn)
+ if err == ctx.Err() {
+ return nil
+ }
+ return nil
}
func (TestDeps) ReadCorpus(dir string) ([][]byte, error) {
type matchStringOnly func(pat, str string) (bool, error)
-func (f matchStringOnly) MatchString(pat, str string) (bool, error) { return f(pat, str) }
-func (f matchStringOnly) StartCPUProfile(w io.Writer) error { return errMain }
-func (f matchStringOnly) StopCPUProfile() {}
-func (f matchStringOnly) WriteProfileTo(string, io.Writer, int) error { return errMain }
-func (f matchStringOnly) ImportPath() string { return "" }
-func (f matchStringOnly) StartTestLog(io.Writer) {}
-func (f matchStringOnly) StopTestLog() error { return errMain }
-func (f matchStringOnly) SetPanicOnExit0(bool) {}
-func (f matchStringOnly) CoordinateFuzzing(int, [][]byte, string, string) error { return errMain }
-func (f matchStringOnly) RunFuzzWorker(func([]byte) error) error { return errMain }
-func (f matchStringOnly) ReadCorpus(string) ([][]byte, error) { return nil, errMain }
+func (f matchStringOnly) MatchString(pat, str string) (bool, error) { return f(pat, str) }
+func (f matchStringOnly) StartCPUProfile(w io.Writer) error { return errMain }
+func (f matchStringOnly) StopCPUProfile() {}
+func (f matchStringOnly) WriteProfileTo(string, io.Writer, int) error { return errMain }
+func (f matchStringOnly) ImportPath() string { return "" }
+func (f matchStringOnly) StartTestLog(io.Writer) {}
+func (f matchStringOnly) StopTestLog() error { return errMain }
+func (f matchStringOnly) SetPanicOnExit0(bool) {}
+func (f matchStringOnly) CoordinateFuzzing(time.Duration, int, [][]byte, string, string) error {
+ return errMain
+}
+func (f matchStringOnly) RunFuzzWorker(func([]byte) error) error { return errMain }
+func (f matchStringOnly) ReadCorpus(string) ([][]byte, error) { return nil, errMain }
// 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.
StartTestLog(io.Writer)
StopTestLog() error
WriteProfileTo(string, io.Writer, int) error
- CoordinateFuzzing(int, [][]byte, string, string) error
+ CoordinateFuzzing(time.Duration, int, [][]byte, string, string) error
RunFuzzWorker(func([]byte) error) error
ReadCorpus(string) ([][]byte, error)
}
m.exitCode = 2
return
}
+ if *fuzzDuration < 0 {
+ fmt.Fprintln(os.Stderr, "testing: -fuzztime can only be given a positive duration, or zero to run indefinitely")
+ flag.Usage()
+ m.exitCode = 2
+ return
+ }
if *matchFuzz != "" && *fuzzCacheDir == "" {
fmt.Fprintln(os.Stderr, "testing: internal error: -test.fuzzcachedir must be set if -test.fuzz is set")
flag.Usage()