]> Cypherpunks.ru repositories - gostls13.git/commitdiff
[dev.fuzz] cmd/go: implement -fuzztime flag and support cancellation
authorJay Conrod <jayconrod@google.com>
Thu, 17 Dec 2020 22:25:42 +0000 (17:25 -0500)
committerJay Conrod <jayconrod@google.com>
Wed, 23 Dec 2020 16:33:50 +0000 (16:33 +0000)
fuzz.CoordinateFuzzing and RunFuzzWorker now accept a context.Context
parameter. They should terminate gracefully when the context is
cancelled. The worker should exit quickly without processing more
inputs. The coordinator should save interesting inputs to the cache.

The testing package can't import context directly, so it provides a
timeout argument to testdeps.CoordinateFuzzing instead. The testdeps
wrapper sets the timeout and installs an interrupt handler (for SIGINT
on POSIX and the equivalent on Windows) that cancels the context when
^C is pressed.

Note that on POSIX platforms, pressing ^C causes the shell to deliver
SIGINT to all processes in the active group: so 'go test', the
coordinator, and the workers should all react to that. On Windows,
pressing ^C only interrupts 'go test'. We may want to look at that
separately.

Change-Id: I924d3be2905f9685dae82ff3c047ca3d6b5e2357
Reviewed-on: https://go-review.googlesource.com/c/go/+/279487
Run-TryBot: Jay Conrod <jayconrod@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Katie Hockman <katie@golang.org>
Trust: Katie Hockman <katie@golang.org>
Trust: Jay Conrod <jayconrod@google.com>

14 files changed:
src/cmd/go/internal/test/flagdefs.go
src/cmd/go/internal/test/genflags.go
src/cmd/go/internal/test/testflag.go
src/cmd/go/testdata/script/test_fuzz.txt
src/cmd/go/testdata/script/test_fuzz_cache.txt
src/cmd/go/testdata/script/test_fuzz_fuzztime.txt [new file with mode: 0644]
src/cmd/go/testdata/script/test_fuzz_match.txt
src/cmd/go/testdata/script/test_fuzz_mutate.txt
src/cmd/go/testdata/script/test_fuzz_mutate_crash.txt
src/internal/fuzz/fuzz.go
src/internal/fuzz/worker.go
src/testing/fuzz.go
src/testing/internal/testdeps/deps.go
src/testing/testing.go

index 57e60e2c0c6664247a24ca02d1f5d322a2042cee..c32b89430b56f69ebde457ab4324fa81742e6e26 100644 (file)
@@ -20,6 +20,7 @@ var passFlagToTest = map[string]bool{
        "cpuprofile":           true,
        "failfast":             true,
        "fuzz":                 true,
+       "fuzztime":             true,
        "list":                 true,
        "memprofile":           true,
        "memprofilerate":       true,
index 5e83d53980c01741b950329c53266c3eca4ba9fd..ca16113bb88ed8080ffc5af9e5ced82a833ec522 100644 (file)
@@ -63,7 +63,7 @@ func testFlags() []string {
                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)
index cb25dc014ace74bf1293dc383b77e191cd7c9b98..2669aac831963af417e5162d189c27acb56ad5c1 100644 (file)
@@ -67,6 +67,7 @@ func init() {
        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, "")
 
index 5ab1c320d74e61c7a065327713d3f4c2ef7367dc..4a761d1fd9031af06703431d80e2c5d0bd7cdbbb 100644 (file)
@@ -1,5 +1,5 @@
 # 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
 
@@ -14,12 +14,12 @@ stdout ok
 ! 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
 
index 6fb443e1fdfd37e419f42e23be692316eaf14e9d..ad8334ae7dc18a2ff7c7d822361ea3b3ccea2915 100644 (file)
@@ -7,7 +7,7 @@ exists $GOCACHE
 ! 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.
diff --git a/src/cmd/go/testdata/script/test_fuzz_fuzztime.txt b/src/cmd/go/testdata/script/test_fuzz_fuzztime.txt
new file mode 100644 (file)
index 0000000..0fc2f74
--- /dev/null
@@ -0,0 +1,27 @@
+[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) {})
+}
index da7e7f13abea313c58903f417388678fb04580e2..6161438c2a2152f9819a25a66144a8bc08cc112e 100644 (file)
@@ -4,12 +4,12 @@ go test standalone_fuzz_test.go
 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\]'
@@ -27,7 +27,7 @@ stdout '^ok.*\[no tests to run\]'
 ! 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\]'
index b881292dc8ea2dc24616cc05fd1eeb71e4c75c84..cbd0838e7343684427b94b420ba35c5d78b84b9d 100644 (file)
@@ -7,7 +7,7 @@
 
 [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 --
index 3647bf1dbdaba1aadb6cf229bff16258b97013b3..6816950265212d7a0eb34a88075e86895ca5ed61 100644 (file)
@@ -12,7 +12,7 @@
 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
@@ -21,12 +21,12 @@ grep '\Aab\z' testdata/corpus/FuzzWithBug/fb8e20fc2e4c3f248c60c39bd652f3c1347298
 # 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
index 2ab16b1189529ec526da7e93793ace46ac49761c..aacc0536828713329925ba70ffb6d81543dbb280 100644 (file)
@@ -8,6 +8,7 @@
 package fuzz
 
 import (
+       "context"
        "crypto/sha256"
        "fmt"
        "io/ioutil"
@@ -15,7 +16,6 @@ import (
        "path/filepath"
        "runtime"
        "sync"
-       "time"
 )
 
 // CoordinateFuzzing creates several worker processes and communicates with
@@ -39,14 +39,13 @@ import (
 //
 // 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 {
@@ -121,26 +120,28 @@ func CoordinateFuzzing(parallel int, seed [][]byte, corpusDir, cacheDir string)
        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.
index 4658687106e7819570f66edcf18d147f45f94edf..ef2a9303ef8024121e62a045fcbcf03cfdfdb835 100644 (file)
@@ -5,6 +5,7 @@
 package fuzz
 
 import (
+       "context"
        "encoding/json"
        "errors"
        "fmt"
@@ -105,15 +106,26 @@ func (w *worker) runFuzzing() error {
                                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{
@@ -301,13 +313,13 @@ func (w *worker) stop() error {
 //
 // 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
@@ -370,21 +382,41 @@ type workerServer struct {
 // 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")
                }
@@ -398,11 +430,13 @@ func (ws *workerServer) serve() error {
 // 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}
index 996e361300c50beafb711fb5c2ed1e8a9511b6c8..4351704b58e7559f71a63bb45ac8b0a76f93f74b 100644 (file)
@@ -16,12 +16,14 @@ import (
 
 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
 
@@ -136,7 +138,7 @@ func (f *F) Fuzz(ff interface{}) {
                }
                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}
@@ -279,7 +281,7 @@ func (r FuzzResult) String() string {
 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)
 }
index dcca6032d0937ff9d0ee99d5cb1531ff99b9c04f..12da4f3863530a8a36708219e197c687f80370d5 100644 (file)
@@ -12,13 +12,17 @@ package testdeps
 
 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,
@@ -128,12 +132,51 @@ func (TestDeps) SetPanicOnExit0(v bool) {
        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) {
index e3e35fa13a1432a043a993ca2a29fa8f2de258f7..39316122a6d1e894e7fc28ca0370be3a2aeeaed7 100644 (file)
@@ -1353,17 +1353,19 @@ var errMain = errors.New("testing: unexpected use of func Main")
 
 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.
@@ -1406,7 +1408,7 @@ type testDeps interface {
        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)
 }
@@ -1448,6 +1450,12 @@ func (m *M) Run() (code int) {
                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()