[dev.fuzz] internal/fuzz: use coverage instrumentation while fuzzing

This change updates the go command behavior when
fuzzing to instrument the binary for code coverage,
and uses this coverage in the fuzzing engine to
determine if an input is interesting.

Unfortunately, we can't store and use the coverage
data for a given run of `go test` and re-use it
the next time we fuzz, since the edges could have
changed between builds. Instead, every entry in
the seed corpus and the on-disk corpus is run
by the workers before fuzzing begins, so that the
coordinator can get the baseline coverage for what
the fuzzing engine has already found (or what
the developers have already provided).

Users should run `go clean -fuzzcache` before
using this change, to clear out any existing
"interesting" values that were in the cache.
Previously, every single non-crashing input was
written to the on-disk corpus. Now, only inputs
that actually expand coverage are written.

This change includes a small hack in
cmd/go/internal/load/pkg.go which ensures that the Gcflags
that were explicitly set in cmd/go/internal/test/test.go
don't get cleared out.

Tests will be added in a follow-up change, since
they will be a bit more involved.

Change-Id: Ie659222d44475c6d68fa4a35d37c37cab3619d71
Reviewed-on: https://go-review.googlesource.com/c/go/+/312009
Trust: Katie Hockman <katie@golang.org>
Run-TryBot: Katie Hockman <katie@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Jay Conrod <jayconrod@google.com>
This commit is contained in:
Katie Hockman 2021-04-20 16:11:13 -04:00
parent 510e711dd3
commit 6ffb027483
9 changed files with 254 additions and 80 deletions

View file

@ -206,6 +206,7 @@ type PackageInternal struct {
BuildInfo string // add this info to package main BuildInfo string // add this info to package main
TestmainGo *[]byte // content for _testmain.go TestmainGo *[]byte // content for _testmain.go
Embed map[string][]string // //go:embed comment mapping Embed map[string][]string // //go:embed comment mapping
FlagsSet bool // whether the flags have been set
Asmflags []string // -asmflags for this package Asmflags []string // -asmflags for this package
Gcflags []string // -gcflags for this package Gcflags []string // -gcflags for this package
@ -2493,6 +2494,14 @@ func CheckPackageErrors(pkgs []*Package) {
func setToolFlags(pkgs ...*Package) { func setToolFlags(pkgs ...*Package) {
for _, p := range PackageList(pkgs) { for _, p := range PackageList(pkgs) {
// TODO(jayconrod,katiehockman): See if there's a better way to do this.
if p.Internal.FlagsSet {
// The flags have already been set, so don't re-run this and
// potentially clear existing flags.
continue
} else {
p.Internal.FlagsSet = true
}
p.Internal.Asmflags = BuildAsmflags.For(p) p.Internal.Asmflags = BuildAsmflags.For(p)
p.Internal.Gcflags = BuildGcflags.For(p) p.Internal.Gcflags = BuildGcflags.For(p)
p.Internal.Ldflags = BuildLdflags.For(p) p.Internal.Ldflags = BuildLdflags.For(p)

View file

@ -764,6 +764,15 @@ func runTest(ctx context.Context, cmd *base.Command, args []string) {
} }
} }
fuzzFlags := work.FuzzInstrumentFlags()
if testFuzz != "" && fuzzFlags != nil {
// Inform the compiler that it should instrument the binary at
// build-time when fuzzing is enabled.
for _, p := range load.PackageList(pkgs) {
p.Internal.Gcflags = append(p.Internal.Gcflags, fuzzFlags...)
}
}
// Prepare build + run + print actions for all packages being tested. // Prepare build + run + print actions for all packages being tested.
for _, p := range pkgs { for _, p := range pkgs {
// sync/atomic import is inserted by the cover tool. See #18486 // sync/atomic import is inserted by the cover tool. See #18486

View file

@ -63,6 +63,14 @@ func BuildInit() {
} }
} }
func FuzzInstrumentFlags() []string {
if cfg.Goarch != "amd64" && cfg.Goarch != "arm64" {
// Instrumentation is only supported on 64-bit architectures.
return nil
}
return []string{"-d=libfuzzer"}
}
func instrumentInit() { func instrumentInit() {
if !cfg.BuildRace && !cfg.BuildMSan { if !cfg.BuildRace && !cfg.BuildMSan {
return return

View file

@ -20,6 +20,7 @@ exec ./fuzz.test$GOEXE -test.timeout=10ms -test.fuzz=FuzzFast -test.fuzztime=5s
# This fuzz function creates a file with a unique name ($pid.$count) on each run. # This fuzz function creates a file with a unique name ($pid.$count) on each run.
# We count the files to find the number of runs. # We count the files to find the number of runs.
mkdir count mkdir count
env GOCACHE=$WORK/tmp
go test -fuzz=FuzzCount -fuzztime=1000x go test -fuzz=FuzzCount -fuzztime=1000x
go run count_files.go go run count_files.go
stdout '^1000$' stdout '^1000$'

View file

@ -20,27 +20,27 @@ stdout FAIL
stdout 'mutator found enough unique mutations' stdout 'mutator found enough unique mutations'
# Test that minimization is working for recoverable errors. # Test that minimization is working for recoverable errors.
! go test -fuzz=FuzzMinimizerRecoverable -run=FuzzMinimizerRecoverable -fuzztime=10s minimizer_test.go ! go test -fuzz=FuzzMinimizerRecoverable -run=FuzzMinimizerRecoverable -fuzztime=1000x minimizer_test.go
! stdout '^ok' ! stdout '^ok'
stdout 'got the minimum size!' stdout 'got the minimum size!'
stdout 'contains a letter' stdout 'contains a letter'
stdout FAIL stdout FAIL
# Check that the bytes written to testdata are of length 100 (the minimum size) # Check that the bytes written to testdata are of length 50 (the minimum size)
go run check_testdata.go FuzzMinimizerRecoverable 100 go run check_testdata.go FuzzMinimizerRecoverable 50
# Test that re-running the minimized value causes a crash. # Test that re-running the minimized value causes a crash.
! go test -run=FuzzMinimizerRecoverable minimizer_test.go ! go test -run=FuzzMinimizerRecoverable minimizer_test.go
# Test that minimization is working for non-recoverable errors. # Test that minimization is working for non-recoverable errors.
! go test -fuzz=FuzzMinimizerNonrecoverable -run=FuzzMinimizerNonrecoverable -fuzztime=10s minimizer_test.go ! go test -fuzz=FuzzMinimizerNonrecoverable -run=FuzzMinimizerNonrecoverable -fuzztime=1000x minimizer_test.go
! stdout '^ok' ! stdout '^ok'
stdout 'got the minimum size!' stdout 'got the minimum size!'
stdout 'contains a letter' stdout 'contains a letter'
stdout FAIL stdout FAIL
# Check that the bytes written to testdata are of length 100 (the minimum size) # Check that the bytes written to testdata are of length 50 (the minimum size)
go run check_testdata.go FuzzMinimizerNonrecoverable 100 go run check_testdata.go FuzzMinimizerNonrecoverable 50
# Test that minimization can be cancelled by fuzztime and the latest crash will # Test that minimization can be cancelled by fuzztime and the latest crash will
# still be logged and written to testdata. # still be logged and written to testdata.
@ -48,7 +48,7 @@ go run check_testdata.go FuzzMinimizerNonrecoverable 100
! stdout '^ok' ! stdout '^ok'
stdout 'testdata[/\\]corpus[/\\]FuzzNonMinimizable[/\\]' stdout 'testdata[/\\]corpus[/\\]FuzzNonMinimizable[/\\]'
! stdout 'got the minimum size!' # it shouldn't have had enough time to minimize it ! stdout 'got the minimum size!' # it shouldn't have had enough time to minimize it
stdout 'at least 100 bytes' stdout 'at least 20 bytes'
stdout FAIL stdout FAIL
# TODO(jayconrod,katiehockman): add a test which verifies that the right bytes # TODO(jayconrod,katiehockman): add a test which verifies that the right bytes
@ -113,32 +113,32 @@ import (
func FuzzMinimizerRecoverable(f *testing.F) { func FuzzMinimizerRecoverable(f *testing.F) {
f.Fuzz(func(t *testing.T, b []byte) { f.Fuzz(func(t *testing.T, b []byte) {
if len(b) < 100 { if len(b) < 50 {
// Make sure that b is large enough that it can be minimized // Make sure that b is large enough that it can be minimized
return return
} }
// Given the randomness of the mutations, this should allow the // Given the randomness of the mutations, this should allow the
// minimizer to trim down the value a bit. // minimizer to trim down the value a bit.
if bytes.ContainsAny(b, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") { if bytes.ContainsAny(b, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") {
if len(b) == 100 { if len(b) == 50 {
t.Logf("got the minimum size!") t.Log("got the minimum size!")
} }
t.Errorf("contains a letter") t.Error("contains a letter")
} }
}) })
} }
func FuzzMinimizerNonrecoverable(f *testing.F) { func FuzzMinimizerNonrecoverable(f *testing.F) {
f.Fuzz(func(t *testing.T, b []byte) { f.Fuzz(func(t *testing.T, b []byte) {
if len(b) < 100 { if len(b) < 50 {
// Make sure that b is large enough that it can be minimized // Make sure that b is large enough that it can be minimized
return return
} }
// Given the randomness of the mutations, this should allow the // Given the randomness of the mutations, this should allow the
// minimizer to trim down the value quite a bit. // minimizer to trim down the value quite a bit.
if bytes.ContainsAny(b, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") { if bytes.ContainsAny(b, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") {
if len(b) == 100 { if len(b) == 50 {
t.Logf("got the minimum size!") t.Log("got the minimum size!")
} }
panic("contains a letter") panic("contains a letter")
} }
@ -147,15 +147,15 @@ func FuzzMinimizerNonrecoverable(f *testing.F) {
func FuzzNonMinimizable(f *testing.F) { func FuzzNonMinimizable(f *testing.F) {
f.Fuzz(func(t *testing.T, b []byte) { f.Fuzz(func(t *testing.T, b []byte) {
if len(b) < 10 { if len(b) < 20 {
// Make sure that b is large enough that minimization will try to run. // Make sure that b is large enough that minimization will try to run.
return return
} }
time.Sleep(3 * time.Second) panic("at least 20 bytes")
if len(b) == 10 { if len(b) == 20 {
t.Logf("got the minimum size!") t.Log("got the minimum size!")
} }
panic("at least 100 bytes") time.Sleep(4 * time.Second)
}) })
} }

View file

@ -26,6 +26,25 @@ func coverage() []byte {
return res return res
} }
// coverageCopy returns a copy of the current bytes provided by coverage().
// TODO(jayconrod,katiehockman): consider using a shared buffer instead, to
// make fewer costly allocations.
func coverageCopy() []byte {
cov := coverage()
ret := make([]byte, len(cov))
copy(ret, cov)
return ret
}
// resetCovereage sets all of the counters for each edge of the instrumented
// source code to 0.
func resetCoverage() {
cov := coverage()
for i := range cov {
cov[i] = 0
}
}
// _counters and _ecounters mark the start and end, respectively, of where // _counters and _ecounters mark the start and end, respectively, of where
// the 8-bit coverage counters reside in memory. They're known to cmd/link, // the 8-bit coverage counters reside in memory. They're known to cmd/link,
// which specially assigns their addresses for this purpose. // which specially assigns their addresses for this purpose.

View file

@ -210,23 +210,36 @@ func CoordinateFuzzing(ctx context.Context, opts CoordinateFuzzingOpts) (err err
// TODO(jayconrod,katiehockman): if -keepfuzzing, report the error to // TODO(jayconrod,katiehockman): if -keepfuzzing, report the error to
// the user and restart the crashed worker. // the user and restart the crashed worker.
stop(err) stop(err)
} else if result.isInteresting { } else if result.coverageData != nil {
foundNew := c.updateCoverage(result.coverageData)
if foundNew && !c.coverageOnlyRun() {
// Found an interesting value that expanded coverage. // Found an interesting value that expanded coverage.
// This is not a crasher, but we should minimize it, add it to the // This is not a crasher, but we should add it to the
// on-disk corpus, and prioritize it for future fuzzing. // on-disk corpus, and prioritize it for future fuzzing.
// TODO(jayconrod, katiehockman): Prioritize fuzzing these values which // TODO(jayconrod, katiehockman): Prioritize fuzzing these
// expanded coverage. // values which expanded coverage, perhaps based on the
// number of new edges that this result expanded.
// TODO(jayconrod, katiehockman): Don't write a value that's already // TODO(jayconrod, katiehockman): Don't write a value that's already
// in the corpus. // in the corpus.
c.interestingCount++
c.corpus.entries = append(c.corpus.entries, result.entry) c.corpus.entries = append(c.corpus.entries, result.entry)
if opts.CacheDir != "" { if opts.CacheDir != "" {
if _, err := writeToCorpus(result.entry.Data, opts.CacheDir); err != nil { if _, err := writeToCorpus(result.entry.Data, opts.CacheDir); err != nil {
stop(err) stop(err)
} }
} }
} else if c.coverageOnlyRun() {
c.covOnlyInputs--
if c.covOnlyInputs == 0 {
// The coordinator has finished getting a baseline for
// coverage. Tell all of the workers to inialize their
// baseline coverage data (by setting interestingCount
// to 0).
c.interestingCount = 0
} }
}
if inputC == nil && !stopping { }
if inputC == nil && !stopping && !c.coverageOnlyRun() {
// inputC was disabled earlier because we hit the limit on the number // inputC was disabled earlier because we hit the limit on the number
// of inputs to fuzz (nextInput returned false). // of inputs to fuzz (nextInput returned false).
// Workers can do less work than requested though, so we might be // Workers can do less work than requested though, so we might be
@ -246,7 +259,13 @@ func CoordinateFuzzing(ctx context.Context, opts CoordinateFuzzingOpts) (err err
case inputC <- input: case inputC <- input:
// Send the next input to any worker. // Send the next input to any worker.
if input, ok = c.nextInput(); !ok { if c.corpusIndex == 0 && c.coverageOnlyRun() {
// The coordinator is currently trying to run all of the corpus
// entries to gather baseline coverage data, and all of the
// inputs have been passed to inputC. Block any more inputs from
// being passed to the workers for now.
inputC = nil
} else if input, ok = c.nextInput(); !ok {
inputC = nil inputC = nil
} }
@ -310,6 +329,17 @@ type fuzzInput struct {
// countRequested is the number of values to test. If non-zero, the worker // countRequested is the number of values to test. If non-zero, the worker
// will stop after testing this many values, if it hasn't already stopped. // will stop after testing this many values, if it hasn't already stopped.
countRequested int64 countRequested int64
// coverageOnly indicates whether this input is for a coverage-only run. If
// true, the input should not be fuzzed.
coverageOnly bool
// interestingCount reflects the coordinator's current interestingCount
// value.
interestingCount int64
// coverageData reflects the coordinator's current coverageData.
coverageData []byte
} }
type fuzzResult struct { type fuzzResult struct {
@ -319,9 +349,8 @@ type fuzzResult struct {
// crasherMsg is an error message from a crash. It's "" if no crash was found. // crasherMsg is an error message from a crash. It's "" if no crash was found.
crasherMsg string crasherMsg string
// isInteresting is true if the worker found new coverage. We should minimize // coverageData is set if the worker found new coverage.
// the value, cache it, and prioritize it for further fuzzing. coverageData []byte
isInteresting bool
// countRequested is the number of values the coordinator asked the worker // countRequested is the number of values the coordinator asked the worker
// to test. 0 if there was no limit. // to test. 0 if there was no limit.
@ -354,6 +383,14 @@ type coordinator struct {
// count is the number of values fuzzed so far. // count is the number of values fuzzed so far.
count int64 count int64
// interestingCount is the number of unique interesting values which have
// been found this execution.
interestingCount int64
// covOnlyInputs is the number of entries in the corpus which still need to
// be sent to a worker to gather baseline coverage data.
covOnlyInputs int
// duration is the time spent fuzzing inside workers, not counting time // duration is the time spent fuzzing inside workers, not counting time
// starting up or tearing down. // starting up or tearing down.
duration time.Duration duration time.Duration
@ -370,6 +407,8 @@ type coordinator struct {
// TODO(jayconrod,katiehockman): need a scheduling algorithm that chooses // TODO(jayconrod,katiehockman): need a scheduling algorithm that chooses
// which corpus value to send next (or generates something new). // which corpus value to send next (or generates something new).
corpusIndex int corpusIndex int
coverageData []byte
} }
func newCoordinator(opts CoordinateFuzzingOpts) (*coordinator, error) { func newCoordinator(opts CoordinateFuzzingOpts) (*coordinator, error) {
@ -383,6 +422,7 @@ func newCoordinator(opts CoordinateFuzzingOpts) (*coordinator, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
covOnlyInputs := len(corpus.entries)
if len(corpus.entries) == 0 { if len(corpus.entries) == 0 {
var vals []interface{} var vals []interface{}
for _, t := range opts.Types { for _, t := range opts.Types {
@ -396,6 +436,22 @@ func newCoordinator(opts CoordinateFuzzingOpts) (*coordinator, error) {
inputC: make(chan fuzzInput), inputC: make(chan fuzzInput),
resultC: make(chan fuzzResult), resultC: make(chan fuzzResult),
corpus: corpus, corpus: corpus,
covOnlyInputs: covOnlyInputs,
}
cov := coverageCopy()
if len(cov) == 0 {
fmt.Fprintf(c.opts.Log, "warning: coverage-guided fuzzing is not supported on this platform\n")
c.covOnlyInputs = 0
} else {
// Set c.coverageData to a clean []byte full of zeros.
c.coverageData = make([]byte, len(cov))
}
if c.covOnlyInputs > 0 {
// Set c.interestingCount to -1 so the workers know when the coverage
// run is finished and can update their local coverage data.
c.interestingCount = -1
} }
return c, nil return c, nil
@ -409,9 +465,15 @@ func (c *coordinator) updateStats(result fuzzResult) {
} }
func (c *coordinator) logStats() { func (c *coordinator) logStats() {
// TODO(jayconrod,katiehockman): consider printing the amount of coverage
// that has been reached so far (perhaps a percentage of edges?)
elapsed := time.Since(c.startTime) elapsed := time.Since(c.startTime)
if c.coverageOnlyRun() {
fmt.Fprintf(c.opts.Log, "gathering baseline coverage, elapsed: %.1fs, workers: %d, left: %d\n", elapsed.Seconds(), c.opts.Parallel, c.covOnlyInputs)
} else {
rate := float64(c.count) / elapsed.Seconds() rate := float64(c.count) / elapsed.Seconds()
fmt.Fprintf(c.opts.Log, "elapsed: %.1fs, execs: %d (%.0f/sec), workers: %d\n", elapsed.Seconds(), c.count, rate, c.opts.Parallel) fmt.Fprintf(c.opts.Log, "fuzzing, elapsed: %.1fs, execs: %d (%.0f/sec), workers: %d, interesting: %d\n", elapsed.Seconds(), c.count, rate, c.opts.Parallel, c.interestingCount)
}
} }
// nextInput returns the next value that should be sent to workers. // nextInput returns the next value that should be sent to workers.
@ -423,22 +485,54 @@ func (c *coordinator) nextInput() (fuzzInput, bool) {
// Workers already testing all requested inputs. // Workers already testing all requested inputs.
return fuzzInput{}, false return fuzzInput{}, false
} }
input := fuzzInput{
e := c.corpus.entries[c.corpusIndex] entry: c.corpus.entries[c.corpusIndex],
interestingCount: c.interestingCount,
coverageData: c.coverageData,
}
c.corpusIndex = (c.corpusIndex + 1) % (len(c.corpus.entries)) c.corpusIndex = (c.corpusIndex + 1) % (len(c.corpus.entries))
var n int64
if c.coverageOnlyRun() {
// This is a coverage-only run, so this input shouldn't be fuzzed,
// and shouldn't be included in the count of generated values.
input.coverageOnly = true
return input, true
}
if c.opts.Count > 0 { if c.opts.Count > 0 {
n = c.opts.Count / int64(c.opts.Parallel) input.countRequested = c.opts.Count / int64(c.opts.Parallel)
if c.opts.Count%int64(c.opts.Parallel) > 0 { if c.opts.Count%int64(c.opts.Parallel) > 0 {
n++ input.countRequested++
} }
remaining := c.opts.Count - c.count - c.countWaiting remaining := c.opts.Count - c.count - c.countWaiting
if n > remaining { if input.countRequested > remaining {
n = remaining input.countRequested = remaining
} }
c.countWaiting += n c.countWaiting += input.countRequested
} }
return fuzzInput{entry: e, countRequested: n}, true return input, true
}
func (c *coordinator) coverageOnlyRun() bool {
return c.covOnlyInputs > 0
}
// updateCoverage updates c.coverageData for all edges that have a higher
// counter value in newCoverage. It return true if a new edge was hit.
func (c *coordinator) updateCoverage(newCoverage []byte) bool {
if len(newCoverage) != len(c.coverageData) {
panic(fmt.Sprintf("num edges changed at runtime: %d, expected %d", len(newCoverage), len(c.coverageData)))
}
newEdge := false
for i := range newCoverage {
if newCoverage[i] > c.coverageData[i] {
if c.coverageData[i] == 0 {
newEdge = true
}
c.coverageData[i] = newCoverage[i]
}
}
return newEdge
} }
// readCache creates a combined corpus from seed values and values in the cache // readCache creates a combined corpus from seed values and values in the cache

View file

@ -269,7 +269,7 @@ func (m *mutator) mutateBytes(ptrB *[]byte) {
case 1: case 1:
// Insert a range of random bytes. // Insert a range of random bytes.
pos := m.rand(len(b) + 1) pos := m.rand(len(b) + 1)
n := m.chooseLen(10) n := m.chooseLen(1024)
if len(b)+n >= cap(b) { if len(b)+n >= cap(b) {
iter-- iter--
continue continue

View file

@ -98,6 +98,10 @@ func (w *worker) coordinate(ctx context.Context) error {
// TODO(jayconrod,katiehockman): record and return stderr. // TODO(jayconrod,katiehockman): record and return stderr.
} }
// interestingCount starts at -1, like the coordinator does, so that the
// worker client's coverage data is updated after a coverage-only run.
interestingCount := int64(-1)
// Main event loop. // Main event loop.
for { for {
select { select {
@ -134,13 +138,19 @@ func (w *worker) coordinate(ctx context.Context) error {
return fmt.Errorf("fuzzing process exited unexpectedly due to an internal failure: %w", err) return fmt.Errorf("fuzzing process exited unexpectedly due to an internal failure: %w", err)
} }
// Worker exited non-zero or was terminated by a non-interrupt signal // Worker exited non-zero or was terminated by a non-interrupt signal
// (for example, SIGSEGV). // (for example, SIGSEGV) while fuzzing.
return fmt.Errorf("fuzzing process terminated unexpectedly: %w", err) return fmt.Errorf("fuzzing process terminated unexpectedly: %w", err)
// TODO(jayconrod,katiehockman): if -keepfuzzing, restart worker.
// TODO(jayconrod,katiehockman): record and return stderr. // TODO(jayconrod,katiehockman): record and return stderr.
case input := <-w.coordinator.inputC: case input := <-w.coordinator.inputC:
// Received input from coordinator. // Received input from coordinator.
args := fuzzArgs{Count: input.countRequested, Duration: workerFuzzDuration} args := fuzzArgs{Count: input.countRequested, Duration: workerFuzzDuration, CoverageOnly: input.coverageOnly}
if interestingCount < input.interestingCount {
// The coordinator's coverage data has changed, so send the data
// to the client.
args.CoverageData = input.coverageData
}
value, resp, err := w.client.fuzz(ctx, input.entry.Data, args) value, resp, err := w.client.fuzz(ctx, input.entry.Data, args)
if err != nil { if err != nil {
// Error communicating with worker. // Error communicating with worker.
@ -162,7 +172,6 @@ func (w *worker) coordinate(ctx context.Context) error {
// Since we expect I/O errors around interrupts, ignore this error. // Since we expect I/O errors around interrupts, ignore this error.
return nil return nil
} }
// Unexpected termination. Attempt to minimize, then inform the // Unexpected termination. Attempt to minimize, then inform the
// coordinator about the crash. // coordinator about the crash.
// TODO(jayconrod,katiehockman): if -keepfuzzing, restart worker. // TODO(jayconrod,katiehockman): if -keepfuzzing, restart worker.
@ -191,12 +200,12 @@ func (w *worker) coordinate(ctx context.Context) error {
count: resp.Count, count: resp.Count,
duration: resp.Duration, duration: resp.Duration,
} }
if resp.Crashed { if resp.Err != "" {
result.entry = CorpusEntry{Data: value} result.entry = CorpusEntry{Data: value}
result.crasherMsg = resp.Err result.crasherMsg = resp.Err
} else if resp.Interesting { } else if resp.CoverageData != nil {
result.entry = CorpusEntry{Data: value} result.entry = CorpusEntry{Data: value}
result.isInteresting = true result.coverageData = resp.CoverageData
} }
w.coordinator.resultC <- result w.coordinator.resultC <- result
} }
@ -454,6 +463,14 @@ type fuzzArgs struct {
// Count is the number of values to test, without spending more time // Count is the number of values to test, without spending more time
// than Duration. // than Duration.
Count int64 Count int64
// CoverageOnly indicates whether this is a coverage-only run (ie. fuzzing
// should not occur).
CoverageOnly bool
// CoverageData is the coverage data. If set, the worker should update its
// local coverage data prior to fuzzing.
CoverageData []byte
} }
// fuzzResponse contains results from workerServer.fuzz. // fuzzResponse contains results from workerServer.fuzz.
@ -464,16 +481,12 @@ type fuzzResponse struct {
// Count is the number of values tested. // Count is the number of values tested.
Count int64 Count int64
// Interesting indicates the value in shared memory may be interesting to // CoverageData is set if the value in shared memory expands coverage
// the coordinator (for example, because it expanded coverage). // and therefore may be interesting to the coordinator.
Interesting bool CoverageData []byte
// Crashed indicates the value in shared memory caused a crash. // Err is the error string caused by the value in shared memory, which is
Crashed bool // non-empty if the value in shared memory caused a crash.
// Err is the error string caused by the value in shared memory. This alone
// cannot be used to determine whether this value caused a crash, since a
// crash can occur without any output (e.g. with t.Fail()).
Err string Err string
} }
@ -506,6 +519,11 @@ type workerServer struct {
workerComm workerComm
m *mutator m *mutator
// coverageData is the local coverage data for the worker. It is
// periodically updated to reflect the data in the coordinator when new
// edges are hit.
coverageData []byte
// fuzzFn runs the worker's fuzz function on the given input and returns // fuzzFn runs the worker's fuzz function on the given input and returns
// an error if it finds a crasher (the process may also exit or crash). // an error if it finds a crasher (the process may also exit or crash).
fuzzFn func(CorpusEntry) error fuzzFn func(CorpusEntry) error
@ -580,6 +598,9 @@ func (ws *workerServer) serve(ctx context.Context) error {
// a given amount of time. fuzz returns early if it finds an input that crashes // a given amount of time. fuzz returns early if it finds an input that crashes
// the fuzz function or an input that expands coverage. // the fuzz function or an input that expands coverage.
func (ws *workerServer) fuzz(ctx context.Context, args fuzzArgs) (resp fuzzResponse) { func (ws *workerServer) fuzz(ctx context.Context, args fuzzArgs) (resp fuzzResponse) {
if args.CoverageData != nil {
ws.coverageData = args.CoverageData
}
start := time.Now() start := time.Now()
defer func() { resp.Duration = time.Since(start) }() defer func() { resp.Duration = time.Since(start) }()
@ -593,42 +614,55 @@ func (ws *workerServer) fuzz(ctx context.Context, args fuzzArgs) (resp fuzzRespo
panic(err) panic(err)
} }
if args.CoverageOnly {
// Reset the coverage each time before running the fuzzFn.
resetCoverage()
ws.fuzzFn(CorpusEntry{Values: vals})
resp.CoverageData = coverageCopy()
return resp
}
cov := coverage()
if len(cov) != len(ws.coverageData) {
panic(fmt.Sprintf("num edges changed at runtime: %d, expected %d", len(cov), len(ws.coverageData)))
}
for { for {
select { select {
case <-fuzzCtx.Done(): case <-fuzzCtx.Done():
// TODO(jayconrod,katiehockman): this value is not interesting. Use a
// real heuristic once we have one.
resp.Interesting = true
return resp return resp
default: default:
resp.Count++ resp.Count++
ws.m.mutate(vals, cap(mem.valueRef())) ws.m.mutate(vals, cap(mem.valueRef()))
writeToMem(vals, mem) writeToMem(vals, mem)
resetCoverage()
if err := ws.fuzzFn(CorpusEntry{Values: vals}); err != nil { if err := ws.fuzzFn(CorpusEntry{Values: vals}); err != nil {
// TODO(jayconrod,katiehockman): consider making the maximum minimization // TODO(jayconrod,katiehockman): consider making the maximum
// time customizable with a go command flag. // minimization time customizable with a go command flag.
minCtx, minCancel := context.WithTimeout(ctx, time.Minute) minCtx, minCancel := context.WithTimeout(ctx, time.Minute)
defer minCancel() defer minCancel()
if minErr := ws.minimizeInput(minCtx, vals, mem); minErr != nil { if minErr := ws.minimizeInput(minCtx, vals, mem); minErr != nil {
// Minimization found a different error, so use that one. // Minimization found a different error, so use that one.
err = minErr err = minErr
} }
resp.Crashed = true
resp.Err = err.Error() resp.Err = err.Error()
if resp.Err == "" { if resp.Err == "" {
resp.Err = "fuzz function failed with no output" resp.Err = "fuzz function failed with no output"
} }
return resp return resp
} }
if args.Count > 0 && resp.Count == args.Count { for i := range cov {
// TODO(jayconrod,katiehockman): this value is not interesting. Use a if ws.coverageData[i] == 0 && cov[i] > ws.coverageData[i] {
// real heuristic once we have one. // TODO(jayconrod,katie): minimize this.
resp.Interesting = true // This run hit a new edge. Only allocate a new slice as a
// copy of cov if we are returning, since it is expensive.
resp.CoverageData = coverageCopy()
return resp
}
}
if args.Count > 0 && resp.Count == args.Count {
return resp return resp
} }
// TODO(jayconrod,katiehockman): return early if we find an
// interesting value.
} }
} }
} }