]> Cypherpunks repositories - gostls13.git/commitdiff
[dev.fuzz] internal/fuzz: coordinate fuzzing across workers
authorJay Conrod <jayconrod@google.com>
Fri, 2 Oct 2020 20:05:33 +0000 (16:05 -0400)
committerFilippo Valsorda <filippo@golang.org>
Fri, 4 Dec 2020 18:17:29 +0000 (19:17 +0100)
Package fuzz provides common fuzzing functionality for tests built
with "go test" and for programs that use fuzzing functionality in the
testing package.

Change-Id: I3901c6a993a9adb8a93733ae1838b86dd78c7036
Reviewed-on: https://go-review.googlesource.com/c/go/+/259259
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>

src/cmd/go/internal/test/flagdefs_test.go
src/cmd/go/internal/test/test.go
src/go/build/deps_test.go
src/internal/fuzz/fuzz.go [new file with mode: 0644]
src/internal/fuzz/sys_posix.go [new file with mode: 0644]
src/internal/fuzz/sys_windows.go [new file with mode: 0644]
src/internal/fuzz/worker.go [new file with mode: 0644]
src/testing/fuzz.go
src/testing/internal/testdeps/deps.go
src/testing/testing.go

index ab5440b3801f15af1124d3ce1738035a894926a0..50711ecff9c832747f1e0a68298dbc79378b0045 100644 (file)
@@ -17,7 +17,7 @@ func TestPassFlagToTestIncludesAllTestFlags(t *testing.T) {
                }
                name := strings.TrimPrefix(f.Name, "test.")
                switch name {
-               case "testlogfile", "paniconexit0":
+               case "testlogfile", "paniconexit0", "fuzzworker":
                        // These are internal flags.
                default:
                        if !passFlagToTest[name] {
index 109acb53da73c7c33da1b355c597556d131ecf33..5758910a5fb1ebb76efe1633c0de1ab4f026e25a 100644 (file)
@@ -467,7 +467,6 @@ See the documentation of the testing package for more information.
 `,
 }
 
-// TODO(katiehockman): complete the testing here
 var (
        testBench        string                            // -bench flag
        testC            bool                              // -c flag
index fa8ecf10f4281542d40a5b5bca6fdb010b64208c..e5f38d4fbcd674f014f133bc26b85553a1571f66 100644 (file)
@@ -467,7 +467,10 @@ var depsRules = `
        FMT, flag, runtime/debug, runtime/trace
        < testing;
 
-       internal/testlog, runtime/pprof, regexp
+       FMT, encoding/json
+       < internal/fuzz;
+
+       internal/fuzz, internal/testlog, runtime/pprof, regexp
        < testing/internal/testdeps;
 
        OS, flag, testing, internal/cfg
diff --git a/src/internal/fuzz/fuzz.go b/src/internal/fuzz/fuzz.go
new file mode 100644 (file)
index 0000000..4f1d204
--- /dev/null
@@ -0,0 +1,143 @@
+// 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 fuzz provides common fuzzing functionality for tests built with
+// "go test" and for programs that use fuzzing functionality in the testing
+// package.
+package fuzz
+
+import (
+       "os"
+       "runtime"
+       "sync"
+       "time"
+)
+
+// CoordinateFuzzing creates several worker processes and communicates with
+// them to test random inputs that could trigger crashes and expose bugs.
+// The worker processes run the same binary in the same directory with the
+// same environment variables as the coordinator process. Workers also run
+// with the same arguments as the coordinator, except with the -test.fuzzworker
+// flag prepended to the argument list.
+//
+// parallel is the number of worker processes to run in parallel. If parallel
+// is 0, CoordinateFuzzing will run GOMAXPROCS workers.
+//
+// seed is a list of seed values added by the fuzz target with testing.F.Add.
+// Seed values from testdata and GOFUZZCACHE should not be included in this
+// list; this function loads them separately.
+func CoordinateFuzzing(parallel int, seed [][]byte) error {
+       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
+
+       // TODO(jayconrod): do we want to support fuzzing different binaries?
+       dir := "" // same as self
+       binPath := os.Args[0]
+       args := append([]string{"-test.fuzzworker"}, os.Args[1:]...)
+       env := os.Environ() // same as self
+
+       c := &coordinator{
+               doneC:  make(chan struct{}),
+               inputC: make(chan corpusEntry),
+       }
+
+       newWorker := func() *worker {
+               return &worker{
+                       dir:         dir,
+                       binPath:     binPath,
+                       args:        args,
+                       env:         env,
+                       coordinator: c,
+               }
+       }
+
+       corpus := corpus{entries: make([]corpusEntry, len(seed))}
+       for i, v := range seed {
+               corpus.entries[i].b = v
+       }
+       if len(corpus.entries) == 0 {
+               // TODO(jayconrod,katiehockman): pick a good starting corpus when one is
+               // missing or very small.
+               corpus.entries = append(corpus.entries, corpusEntry{b: []byte{0}})
+       }
+
+       // TODO(jayconrod,katiehockman): read corpus from testdata.
+       // TODO(jayconrod,katiehockman): read corpus from GOFUZZCACHE.
+
+       // Start workers.
+       workers := make([]*worker, parallel)
+       runErrs := make([]error, parallel)
+       var wg sync.WaitGroup
+       wg.Add(parallel)
+       for i := 0; i < parallel; i++ {
+               go func(i int) {
+                       defer wg.Done()
+                       workers[i] = newWorker()
+                       runErrs[i] = workers[i].runFuzzing()
+               }(i)
+       }
+
+       // Main event loop.
+       stopC := time.After(duration)
+       i := 0
+       for {
+               select {
+               // TODO(jayconrod): handle interruptions like SIGINT.
+               // TODO(jayconrod,katiehockman): receive crashers and new corpus values
+               // from workers.
+
+               case <-stopC:
+                       // Time's up.
+                       close(c.doneC)
+
+               case <-c.doneC:
+                       // Wait for workers to stop and return.
+                       wg.Wait()
+                       for _, err := range runErrs {
+                               if err != nil {
+                                       return err
+                               }
+                       }
+                       return nil
+
+               case c.inputC <- corpus.entries[i]:
+                       // Sent the next input to any worker.
+                       // TODO(jayconrod,katiehockman): need a scheduling algorithm that chooses
+                       // which corpus value to send next (or generates something new).
+                       i = (i + 1) % len(corpus.entries)
+               }
+       }
+
+       // TODO(jayconrod,katiehockman): write crashers to testdata and other inputs
+       // to GOFUZZCACHE. If the testdata directory is outside the current module,
+       // always write to GOFUZZCACHE, since the testdata is likely read-only.
+}
+
+type corpus struct {
+       entries []corpusEntry
+}
+
+// TODO(jayconrod,katiehockman): decide whether and how to unify this type
+// with the equivalent in testing.
+type corpusEntry struct {
+       b []byte
+}
+
+// coordinator holds channels that workers can use to communicate with
+// the coordinator.
+type coordinator struct {
+       // doneC is closed to indicate fuzzing is done and workers should stop.
+       // doneC may be closed due to a time limit expiring or a fatal error in
+       // a worker.
+       doneC chan struct{}
+
+       // inputC is sent values to fuzz by the coordinator. Any worker may receive
+       // values from this channel.
+       inputC chan corpusEntry
+}
diff --git a/src/internal/fuzz/sys_posix.go b/src/internal/fuzz/sys_posix.go
new file mode 100644 (file)
index 0000000..259caa8
--- /dev/null
@@ -0,0 +1,25 @@
+// 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.
+
+// +build !windows
+
+package fuzz
+
+import (
+       "os"
+       "os/exec"
+)
+
+// setWorkerComm configures communciation channels on the cmd that will
+// run a worker process.
+func setWorkerComm(cmd *exec.Cmd, fuzzIn, fuzzOut *os.File) {
+       cmd.ExtraFiles = []*os.File{fuzzIn, fuzzOut}
+}
+
+// getWorkerComm returns communication channels in the worker process.
+func getWorkerComm() (fuzzIn, fuzzOut *os.File, err error) {
+       fuzzIn = os.NewFile(3, "fuzz_in")
+       fuzzOut = os.NewFile(4, "fuzz_out")
+       return fuzzIn, fuzzOut, nil
+}
diff --git a/src/internal/fuzz/sys_windows.go b/src/internal/fuzz/sys_windows.go
new file mode 100644 (file)
index 0000000..a675484
--- /dev/null
@@ -0,0 +1,49 @@
+// 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.
+
+// +build windows
+
+package fuzz
+
+import (
+       "fmt"
+       "os"
+       "os/exec"
+       "strconv"
+       "strings"
+       "syscall"
+)
+
+// setWorkerComm configures communciation channels on the cmd that will
+// run a worker process.
+func setWorkerComm(cmd *exec.Cmd, fuzzIn, fuzzOut *os.File) {
+       syscall.SetHandleInformation(syscall.Handle(fuzzIn.Fd()), syscall.HANDLE_FLAG_INHERIT, 1)
+       syscall.SetHandleInformation(syscall.Handle(fuzzOut.Fd()), syscall.HANDLE_FLAG_INHERIT, 1)
+       cmd.Env = append(cmd.Env, fmt.Sprintf("GO_TEST_FUZZ_WORKER_HANDLES=%x,%x", fuzzIn.Fd(), fuzzOut.Fd()))
+}
+
+// getWorkerComm returns communication channels in the worker process.
+func getWorkerComm() (fuzzIn *os.File, fuzzOut *os.File, err error) {
+       v := os.Getenv("GO_TEST_FUZZ_WORKER_HANDLES")
+       if v == "" {
+               return nil, nil, fmt.Errorf("GO_TEST_FUZZ_WORKER_HANDLES not set")
+       }
+       parts := strings.Split(v, ",")
+       if len(parts) != 2 {
+               return nil, nil, fmt.Errorf("GO_TEST_FUZZ_WORKER_HANDLES has invalid value")
+       }
+       base := 16
+       bitSize := 64
+       in, err := strconv.ParseInt(parts[0], base, bitSize)
+       if err != nil {
+               return nil, nil, fmt.Errorf("GO_TEST_FUZZ_WORKER_HANDLES has invalid value: %v", err)
+       }
+       out, err := strconv.ParseInt(parts[1], base, bitSize)
+       if err != nil {
+               return nil, nil, fmt.Errorf("GO_TEST_FUZZ_WORKER_HANDLES has invalid value: %v", err)
+       }
+       fuzzIn = os.NewFile(uintptr(in), "fuzz_in")
+       fuzzOut = os.NewFile(uintptr(out), "fuzz_out")
+       return fuzzIn, fuzzOut, nil
+}
diff --git a/src/internal/fuzz/worker.go b/src/internal/fuzz/worker.go
new file mode 100644 (file)
index 0000000..543e352
--- /dev/null
@@ -0,0 +1,381 @@
+// 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 fuzz
+
+import (
+       "encoding/json"
+       "errors"
+       "fmt"
+       "io"
+       "io/ioutil"
+       "os"
+       "os/exec"
+       "runtime"
+       "time"
+)
+
+const (
+       // workerFuzzDuration is the amount of time a worker can spend testing random
+       // variations of an input given by the coordinator.
+       workerFuzzDuration = 100 * time.Millisecond
+
+       // workerTimeoutDuration is the amount of time a worker can go without
+       // responding to the coordinator before being stopped.
+       workerTimeoutDuration = 1 * time.Second
+)
+
+// worker manages a worker process running a test binary.
+type worker struct {
+       dir     string   // working directory, same as package directory
+       binPath string   // path to test executable
+       args    []string // arguments for test executable
+       env     []string // environment for test executable
+
+       coordinator *coordinator
+
+       cmd     *exec.Cmd     // current worker process
+       client  *workerClient // used to communicate with worker process
+       waitErr error         // last error returned by wait, set before termC is closed.
+       termC   chan struct{} // closed by wait when worker process terminates
+}
+
+// runFuzzing runs the test binary to perform fuzzing.
+//
+// This function loops until w.coordinator.doneC is closed or some
+// fatal error is encountered. It receives inputs from w.coordinator.inputC,
+// then passes those on to the worker process. If the worker crashes,
+// runFuzzing restarts it and continues.
+func (w *worker) runFuzzing() error {
+       // Start the process.
+       if err := w.start(); err != nil {
+               // We couldn't start the worker process. We can't do anything, and it's
+               // likely that other workers can't either, so give up.
+               close(w.coordinator.doneC)
+               return err
+       }
+
+       inputC := w.coordinator.inputC // set to nil when processing input
+       fuzzC := make(chan struct{})   // sent when we finish processing an input.
+
+       // Main event loop.
+       for {
+               select {
+               case <-w.coordinator.doneC:
+                       // All workers were told to stop.
+                       return w.stop()
+
+               case <-w.termC:
+                       // Worker process terminated unexpectedly.
+                       // TODO(jayconrod,katiehockman): handle crasher.
+
+                       // Restart the process.
+                       if err := w.start(); err != nil {
+                               close(w.coordinator.doneC)
+                               return err
+                       }
+
+               case input := <-inputC:
+                       // Received input from coordinator.
+                       inputC = nil // block new inputs until we finish with this one.
+                       go func() {
+                               args := fuzzArgs{
+                                       Value:       input.b,
+                                       DurationSec: workerFuzzDuration.Seconds(),
+                               }
+                               _, err := w.client.fuzz(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): what happens if testing.F.Fuzz is never called?
+                                       // TODO(jayconrod): time out if the test process hangs.
+                               }
+
+                               fuzzC <- struct{}{}
+                       }()
+
+               case <-fuzzC:
+                       // Worker finished fuzzing.
+                       // TODO(jayconrod,katiehockman): gather statistics. Collect "interesting"
+                       // inputs and add to corpus.
+                       inputC = w.coordinator.inputC // unblock new inputs
+               }
+       }
+}
+
+// start runs a new worker process.
+//
+// If the process couldn't be started, start returns an error. Start won't
+// return later termination errors from the process if they occur.
+//
+// If the process starts successfully, start returns nil. stop must be called
+// once later to clean up, even if the process terminates on its own.
+//
+// When the process terminates, w.waitErr is set to the error (if any), and
+// w.termC is closed.
+func (w *worker) start() (err error) {
+       if w.cmd != nil {
+               panic("worker already started")
+       }
+       w.waitErr = nil
+       w.termC = nil
+
+       cmd := exec.Command(w.binPath, w.args...)
+       cmd.Dir = w.dir
+       cmd.Env = w.env
+       // TODO(jayconrod): set stdout and stderr to nil or buffer. A large number
+       // of workers may be very noisy, but for now, this output is useful for
+       // debugging.
+       cmd.Stdout = os.Stdout
+       cmd.Stderr = os.Stderr
+
+       // TODO(jayconrod): set up shared memory between the coordinator and worker to
+       // transfer values and coverage data. If the worker crashes, we need to be
+       // able to find the value that caused the crash.
+
+       // Create the "fuzz_in" and "fuzz_out" pipes so we can communicate with
+       // the worker. We don't use stdin and stdout, since the test binary may
+       // do something else with those.
+       //
+       // Each pipe has a reader and a writer. The coordinator writes to fuzzInW
+       // and reads from fuzzOutR. The worker inherits fuzzInR and fuzzOutW.
+       // The coordinator closes fuzzInR and fuzzOutW after starting the worker,
+       // since we have no further need of them.
+       fuzzInR, fuzzInW, err := os.Pipe()
+       if err != nil {
+               return err
+       }
+       defer fuzzInR.Close()
+       fuzzOutR, fuzzOutW, err := os.Pipe()
+       if err != nil {
+               fuzzInW.Close()
+               return err
+       }
+       defer fuzzOutW.Close()
+       setWorkerComm(cmd, fuzzInR, fuzzOutW)
+
+       // Start the worker process.
+       if err := cmd.Start(); err != nil {
+               fuzzInW.Close()
+               fuzzOutR.Close()
+               return err
+       }
+
+       // Worker started successfully.
+       // After this, w.client owns fuzzInW and fuzzOutR, so w.client.Close must be
+       // called later by stop.
+       w.cmd = cmd
+       w.termC = make(chan struct{})
+       w.client = newWorkerClient(fuzzInW, fuzzOutR)
+
+       go func() {
+               w.waitErr = w.cmd.Wait()
+               close(w.termC)
+       }()
+
+       return nil
+}
+
+// stop tells the worker process to exit by closing w.client, then blocks until
+// it terminates. If the worker doesn't terminate after a short time, stop
+// signals it with os.Interrupt (where supported), then os.Kill.
+//
+// stop returns the error the process terminated with, if any (same as
+// w.waitErr).
+//
+// stop must be called once after start returns successfully, even if the
+// worker process terminates unexpectedly.
+func (w *worker) stop() error {
+       if w.termC == nil {
+               panic("worker was not started successfully")
+       }
+       select {
+       case <-w.termC:
+               // Worker already terminated, perhaps unexpectedly.
+               if w.client == nil {
+                       panic("worker already stopped")
+               }
+               w.client.Close()
+               w.cmd = nil
+               w.client = nil
+               return w.waitErr
+       default:
+               // Worker still running.
+       }
+
+       // Tell the worker to stop by closing fuzz_in. It won't actually stop until it
+       // finishes with earlier calls.
+       closeC := make(chan struct{})
+       go func() {
+               w.client.Close()
+               close(closeC)
+       }()
+
+       sig := os.Interrupt
+       if runtime.GOOS == "windows" {
+               // Per https://golang.org/pkg/os/#Signal, “Interrupt is not implemented on
+               // Windows; using it with os.Process.Signal will return an error.”
+               // Fall back to Kill instead.
+               sig = os.Kill
+       }
+
+       t := time.NewTimer(workerTimeoutDuration)
+       for {
+               select {
+               case <-w.termC:
+                       // Worker terminated.
+                       t.Stop()
+                       <-closeC
+                       w.cmd = nil
+                       w.client = nil
+                       return w.waitErr
+
+               case <-t.C:
+                       // Timer fired before worker terminated.
+                       switch sig {
+                       case os.Interrupt:
+                               // Try to stop the worker with SIGINT and wait a little longer.
+                               w.cmd.Process.Signal(sig)
+                               sig = os.Kill
+                               t.Reset(workerTimeoutDuration)
+
+                       case os.Kill:
+                               // Try to stop the worker with SIGKILL and keep waiting.
+                               w.cmd.Process.Signal(sig)
+                               sig = nil
+                               t.Reset(workerTimeoutDuration)
+
+                       case nil:
+                               // Still waiting. Print a message to let the user know why.
+                               fmt.Fprintf(os.Stderr, "go: waiting for fuzz worker to terminate...\n")
+                       }
+               }
+       }
+}
+
+// RunFuzzWorker is called in a worker process to communicate with the
+// coordinator process in order to fuzz random inputs. RunFuzzWorker loops
+// until the coordinator tells it to stop.
+//
+// fn is a wrapper on the fuzz function. It may return an error to indicate
+// a given input "crashed". The coordinator will also record a crasher if
+// the function times out or terminates the process.
+//
+// RunFuzzWorker returns an error if it could not communicate with the
+// coordinator process.
+func RunFuzzWorker(fn func([]byte) error) error {
+       fuzzIn, fuzzOut, err := getWorkerComm()
+       if err != nil {
+               return err
+       }
+       srv := &workerServer{fn: fn}
+       return srv.serve(fuzzIn, fuzzOut)
+}
+
+// call is serialized and sent from the coordinator on fuzz_in. It acts as
+// a minimalist RPC mechanism. Exactly one of its fields must be set to indicate
+// which method to call.
+type call struct {
+       Fuzz *fuzzArgs
+}
+
+type fuzzArgs struct {
+       Value       []byte
+       DurationSec float64
+}
+
+type fuzzResponse struct{}
+
+// workerServer is a minimalist RPC server, run in fuzz worker processes.
+type workerServer struct {
+       fn func([]byte) error
+}
+
+// serve deserializes and executes RPCs on a given pair of pipes.
+//
+// serve returns errors communicating over the pipes. It does not return
+// errors from methods; those are passed through response values.
+func (ws *workerServer) serve(fuzzIn io.ReadCloser, fuzzOut io.WriteCloser) error {
+       enc := json.NewEncoder(fuzzOut)
+       dec := json.NewDecoder(fuzzIn)
+       for {
+               var c call
+               if err := dec.Decode(&c); err == io.EOF {
+                       return nil
+               } else if err != nil {
+                       return err
+               }
+
+               var resp interface{}
+               switch {
+               case c.Fuzz != nil:
+                       resp = ws.fuzz(*c.Fuzz)
+               default:
+                       return errors.New("no arguments provided for any call")
+               }
+
+               if err := enc.Encode(resp); err != nil {
+                       return err
+               }
+       }
+}
+
+// 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 {
+       // TODO(jayconrod, katiehockman): implement
+       return fuzzResponse{}
+}
+
+// workerClient is a minimalist RPC client, run in the fuzz coordinator.
+type workerClient struct {
+       fuzzIn  io.WriteCloser
+       fuzzOut io.ReadCloser
+       enc     *json.Encoder
+       dec     *json.Decoder
+}
+
+func newWorkerClient(fuzzIn io.WriteCloser, fuzzOut io.ReadCloser) *workerClient {
+       return &workerClient{
+               fuzzIn:  fuzzIn,
+               fuzzOut: fuzzOut,
+               enc:     json.NewEncoder(fuzzIn),
+               dec:     json.NewDecoder(fuzzOut),
+       }
+}
+
+// Close shuts down the connection to the RPC server (the worker process) by
+// closing fuzz_in. Close drains fuzz_out (avoiding a SIGPIPE in the worker),
+// and closes it after the worker process closes the other end.
+func (wc *workerClient) Close() error {
+       // Close fuzzIn. This signals to the server that there are no more calls,
+       // and it should exit.
+       if err := wc.fuzzIn.Close(); err != nil {
+               wc.fuzzOut.Close()
+               return err
+       }
+
+       // Drain fuzzOut and close it. When the server exits, the kernel will close
+       // its end of fuzzOut, and we'll get EOF.
+       if _, err := io.Copy(ioutil.Discard, wc.fuzzOut); err != nil {
+               wc.fuzzOut.Close()
+               return err
+       }
+       return wc.fuzzOut.Close()
+}
+
+// fuzz tells the worker to call the fuzz method. See workerServer.fuzz.
+func (wc *workerClient) fuzz(args fuzzArgs) (fuzzResponse, error) {
+       c := call{Fuzz: &args}
+       if err := wc.enc.Encode(c); err != nil {
+               return fuzzResponse{}, err
+       }
+       var resp fuzzResponse
+       if err := wc.dec.Decode(&resp); err != nil {
+               return fuzzResponse{}, err
+       }
+       return resp, nil
+}
index 11bbd8fb1642e4a711e34c48816db778c9f0dbc5..6773b7161d8aa2b08b4607b4ef9f78465933c4fe 100644 (file)
@@ -15,9 +15,13 @@ import (
 
 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
+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.
@@ -33,7 +37,6 @@ type F struct {
        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
-       fuzz     bool          // fuzz indicates whether the fuzzing engine should run
 }
 
 // corpus corpusEntry
@@ -88,7 +91,6 @@ func (f *F) Fuzz(ff interface{}) {
                                t.Fail()
                                t.output = []byte(fmt.Sprintf("    %s", err))
                        }
-                       f.setRan()
                        f.inFuzzFn = false
                        t.signal <- true // signal that the test has finished
                }()
@@ -100,30 +102,76 @@ func (f *F) Fuzz(ff interface{}) {
                t.finished = true
        }
 
-       // Run the seed corpus first
-       for _, c := range f.corpus {
-               t := &T{
-                       common: common{
-                               signal: make(chan bool),
-                               w:      f.w,
-                               chatty: f.chatty,
-                       },
-                       context: newTestContext(1, nil),
+       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
                }
-               go run(t, c.b)
-               <-t.signal
-               if t.Failed() {
-                       f.Fail()
-                       errStr += string(t.output)
+               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.finished = true
-       if f.Failed() {
-               f.result = FuzzResult{Error: errors.New(errStr)}
-               return
-       }
+               f.setRan()
+               f.finished = true
 
-       // TODO: if f.fuzz is set, run fuzzing engine
+       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() {
@@ -202,8 +250,10 @@ func (r FuzzResult) String() string {
 
 // fuzzContext holds all fields that are common to all fuzz targets.
 type fuzzContext struct {
-       runMatch  *matcher
-       fuzzMatch *matcher
+       runMatch          *matcher
+       fuzzMatch         *matcher
+       coordinateFuzzing func(int, [][]byte) error
+       runFuzzWorker     func(func([]byte) error) error
 }
 
 // RunFuzzTargets is an internal function but exported because it is cross-package;
@@ -218,7 +268,7 @@ func RunFuzzTargets(matchString func(pat, str string) (bool, error), fuzzTargets
 // engine to generate or mutate inputs.
 func runFuzzTargets(matchString func(pat, str string) (bool, error), fuzzTargets []InternalFuzzTarget) (ran, ok bool) {
        ok = true
-       if len(fuzzTargets) == 0 {
+       if len(fuzzTargets) == 0 || *isFuzzWorker {
                return ran, ok
        }
        ctx := &fuzzContext{runMatch: newMatcher(matchString, *match, "-test.run")}
@@ -249,25 +299,21 @@ func runFuzzTargets(matchString func(pat, str string) (bool, error), fuzzTargets
        return ran, ok
 }
 
-// RunFuzzing is an internal function but exported because it is cross-package;
-// it is part of the implementation of the "go test" command.
-func RunFuzzing(matchString func(pat, str string) (bool, error), fuzzTargets []InternalFuzzTarget) (ok bool) {
-       _, ok = runFuzzing(matchString, fuzzTargets)
-       return 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.
-func runFuzzing(matchString func(pat, str string) (bool, error), fuzzTargets []InternalFuzzTarget) (ran, ok bool) {
-       if len(fuzzTargets) == 0 {
+//
+// 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(matchString, *matchFuzz, "-test.fuzz"),
-       }
-       if *matchFuzz == "" {
-               return false, true
+       ctx := &fuzzContext{fuzzMatch: newMatcher(deps.MatchString, *matchFuzz, "-test.fuzz")}
+       if *isFuzzWorker {
+               ctx.runFuzzWorker = deps.RunFuzzWorker
+       } else {
+               ctx.coordinateFuzzing = deps.CoordinateFuzzing
        }
        f := &F{
                common: common{
@@ -275,7 +321,6 @@ func runFuzzing(matchString func(pat, str string) (bool, error), fuzzTargets []I
                        w:      os.Stdout,
                },
                context: ctx,
-               fuzz:    true,
        }
        var (
                ft    InternalFuzzTarget
index 3608d332946e2ebee0558a35c35c7930a65e923a..9665092f4c4da040d8b79fd082c102f57cc9a2d3 100644 (file)
@@ -12,6 +12,7 @@ package testdeps
 
 import (
        "bufio"
+       "internal/fuzz"
        "internal/testlog"
        "io"
        "regexp"
@@ -126,3 +127,11 @@ func (TestDeps) StopTestLog() error {
 func (TestDeps) SetPanicOnExit0(v bool) {
        testlog.SetPanicOnExit0(v)
 }
+
+func (TestDeps) CoordinateFuzzing(parallel int, seed [][]byte) error {
+       return fuzz.CoordinateFuzzing(parallel, seed)
+}
+
+func (TestDeps) RunFuzzWorker(fn func([]byte) error) error {
+       return fuzz.RunFuzzWorker(fn)
+}
index 7cf3323d51a6d1bf90dbaf19543b01244b43f557..2c2e77dc4bd315a95a5da9d5540e98db111d48dc 100644 (file)
@@ -1324,6 +1324,8 @@ 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) error       { return errMain }
+func (f matchStringOnly) RunFuzzWorker(func([]byte) error) error      { return 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.
@@ -1366,6 +1368,8 @@ type testDeps interface {
        StartTestLog(io.Writer)
        StopTestLog() error
        WriteProfileTo(string, io.Writer, int) error
+       CoordinateFuzzing(int, [][]byte) error
+       RunFuzzWorker(func([]byte) error) error
 }
 
 // MainStart is meant for use by tests generated by 'go test'.
@@ -1431,7 +1435,7 @@ func (m *M) Run() (code int) {
                return
        }
 
-       fuzzingRan, fuzzingOk := runFuzzing(m.deps.MatchString, m.fuzzTargets)
+       fuzzingRan, fuzzingOk := runFuzzing(m.deps, m.fuzzTargets)
        if *matchFuzz != "" && !fuzzingRan {
                fmt.Fprintln(os.Stderr, "testing: warning: no targets to fuzz")
        }