blob: b72106b337392b5dc828ed3da1ab08dc6b34575b [file] [log] [blame]
// 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 (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"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 and
// in testdata.
// Seed values from 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
var corpus corpus
var maxSeedLen int
if len(seed) == 0 {
corpus.entries = []corpusEntry{{b: []byte{}}}
maxSeedLen = 0
} else {
corpus.entries = make([]corpusEntry, len(seed))
for i, v := range seed {
corpus.entries[i].b = v
if len(v) > maxSeedLen {
maxSeedLen = len(v)
}
}
}
// TODO(jayconrod,katiehockman): read corpus from GOFUZZCACHE.
// 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, error) {
mem, err := sharedMemTempFile(maxSeedLen)
if err != nil {
return nil, err
}
return &worker{
dir: dir,
binPath: binPath,
args: args,
env: env,
coordinator: c,
mem: mem,
}, nil
}
// Start workers.
workers := make([]*worker, parallel)
for i := range workers {
var err error
workers[i], err = newWorker()
if err != nil {
return err
}
}
workerErrs := make([]error, len(workers))
var wg sync.WaitGroup
wg.Add(len(workers))
for i := range workers {
go func(i int) {
defer wg.Done()
workerErrs[i] = workers[i].runFuzzing()
if cleanErr := workers[i].cleanup(); workerErrs[i] == nil {
workerErrs[i] = cleanErr
}
}(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 workerErrs {
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
}
// ReadCorpus reads the corpus from the testdata directory in this target's
// package.
func ReadCorpus(name string) ([][]byte, error) {
testdataDir := filepath.Join("testdata/corpus", name)
files, err := ioutil.ReadDir(testdataDir)
if os.IsNotExist(err) {
return nil, nil // No corpus to read
} else if err != nil {
return nil, fmt.Errorf("testing: reading seed corpus from testdata: %v", err)
}
var corpus [][]byte
for _, file := range files {
if file.IsDir() {
continue
}
bytes, err := ioutil.ReadFile(filepath.Join(testdataDir, file.Name()))
if err != nil {
return nil, fmt.Errorf("testing: failed to read corpus file: %v", err)
}
corpus = append(corpus, bytes)
}
return corpus, nil
}