blob: 930683000f3e27ecbd43f80dc5a61a14807b82dc [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 (
"crypto/sha256"
"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.
//
// 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, crashDir string) 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),
interestingC: make(chan fuzzResponse),
}
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 resp := <-c.interestingC:
// Some interesting input arrived from a worker.
if resp.Err != "" {
// This is a crasher, which should be written to testdata and
// reported to the user.
fileName, err := writeToCorpus(resp.Value, crashDir)
if err == nil {
err = fmt.Errorf(" Crash written to: %s\n%s", fileName, resp.Err)
}
// TODO(jayconrod,katiehockman): if -keepfuzzing, don't stop all
// of the workers, but still report to the user.
// Stop the rest of the workers and wait until they have
// stopped before returning this error.
close(c.doneC)
wg.Wait()
return err
} else if len(resp.Value) > 0 {
// This is not a crasher, but something interesting that should
// be added to the on disk corpus and prioritized for future
// workers to fuzz.
corpus.entries = append(corpus.entries, corpusEntry{b: resp.Value})
// TODO(jayconrod, katiehockman): Add this to the on disk corpus
// TODO(jayconrod, katiehockman): Prioritize fuzzing these values which expanded coverage
}
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
// interestingC is sent interesting values by the worker, which is received
// by the coordinator. The interesting value could be a crash or some
// value that increased coverage.
interestingC chan fuzzResponse
}
// ReadCorpus reads the corpus from the testdata directory in this target's
// package.
func ReadCorpus(dir string) ([][]byte, error) {
files, err := ioutil.ReadDir(dir)
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(dir, file.Name()))
if err != nil {
return nil, fmt.Errorf("testing: failed to read corpus file: %v", err)
}
corpus = append(corpus, bytes)
}
return corpus, nil
}
// writeToCorpus writes the given bytes to a new file in testdata. If the
// directory does not exist, it will create one. It returns the filename that
// was written, or an error if it failed.
func writeToCorpus(b []byte, crashDir string) (string, error) {
// TODO: Consider not writing a new file if one with those contents already
// exists. Perhaps the filename can be compared to those that already exist
// if all of the filenames are normalized, or by checking the contents of
// all other files.
if _, err := ioutil.ReadDir(crashDir); os.IsNotExist(err) {
// Make the seed corpus directory since it doesn't exist.
err = os.MkdirAll(crashDir, 0777)
if err != nil {
return "", err
}
} else if err != nil {
return "", err
}
sum := fmt.Sprintf("%x", sha256.Sum256(b))
name := filepath.Join(crashDir, sum)
err := ioutil.WriteFile(name, b, 0666)
if err != nil {
return "", err
}
return name, nil
}