stdout 'mutator found enough unique mutations'
# Test that minimization is working for recoverable errors.
-! go test -v -fuzz=FuzzMinimizerRecoverable -run=FuzzMinimizerRecoverable -parallel=1 -fuzztime=10s minimizer_test.go
+! go test -fuzz=FuzzMinimizerRecoverable -run=FuzzMinimizerRecoverable -fuzztime=10s minimizer_test.go
! stdout '^ok'
stdout 'got the minimum size!'
stdout 'contains a letter'
stdout FAIL
+# Check that the bytes written to testdata are of length 100 (the minimum size)
+go run check_testdata.go FuzzMinimizerRecoverable 100
+
# Test that re-running the minimized value causes a crash.
! go test -run=FuzzMinimizerRecoverable minimizer_test.go
+# Test that minimization is working for non-recoverable errors.
+! go test -fuzz=FuzzMinimizerNonrecoverable -run=FuzzMinimizerNonrecoverable -fuzztime=10s minimizer_test.go
+! stdout '^ok'
+stdout 'got the minimum size!'
+stdout 'contains a letter'
+stdout FAIL
+
+# Check that the bytes written to testdata are of length 100 (the minimum size)
+go run check_testdata.go FuzzMinimizerNonrecoverable 100
+
+# Test that minimization can be cancelled by fuzztime and the latest crash will
+# still be logged and written to testdata.
+! go test -fuzz=FuzzNonMinimizable -run=FuzzNonMinimizable -parallel=1 -fuzztime=5s minimizer_test.go
+! stdout '^ok'
+stdout 'testdata[/\\]corpus[/\\]FuzzNonMinimizable[/\\]'
+! stdout 'got the minimum size!' # it shouldn't have had enough time to minimize it
+stdout 'at least 100 bytes'
+stdout FAIL
+
+# TODO(jayconrod,katiehockman): add a test which verifies that the right bytes
+# are written to testdata in the case of an interrupt during minimization.
+
-- go.mod --
module m
import (
"bytes"
"testing"
+ "time"
)
func FuzzMinimizerRecoverable(f *testing.F) {
// Make sure that b is large enough that it can be minimized
return
}
- if len(b) == 100 {
- t.Logf("got the minimum size!")
- }
+ // Given the randomness of the mutations, this should allow the
+ // minimizer to trim down the value a bit.
if bytes.ContainsAny(b, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") {
- // Given the randomness of the mutations, this should allow the
- // minimizer to trim down the value quite a bit.
+ if len(b) == 100 {
+ t.Logf("got the minimum size!")
+ }
t.Errorf("contains a letter")
}
})
}
+func FuzzMinimizerNonrecoverable(f *testing.F) {
+ f.Fuzz(func(t *testing.T, b []byte) {
+ if len(b) < 100 {
+ // Make sure that b is large enough that it can be minimized
+ return
+ }
+ // Given the randomness of the mutations, this should allow the
+ // minimizer to trim down the value quite a bit.
+ if bytes.ContainsAny(b, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") {
+ if len(b) == 100 {
+ t.Logf("got the minimum size!")
+ }
+ panic("contains a letter")
+ }
+ })
+}
+
+func FuzzNonMinimizable(f *testing.F) {
+ f.Fuzz(func(t *testing.T, b []byte) {
+ if len(b) < 10 {
+ // Make sure that b is large enough that minimization will try to run.
+ return
+ }
+ time.Sleep(3 * time.Second)
+ if len(b) == 10 {
+ t.Logf("got the minimum size!")
+ }
+ panic("at least 100 bytes")
+ })
+}
+
-- check_logs.go --
// +build ignore
}
return nil
}
+-- check_testdata.go --
+// +build ignore
+
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strconv"
+)
+func main() {
+ target := os.Args[1]
+ numBytes, err := strconv.Atoi(os.Args[2])
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+
+ // Open the file in testdata (there should only be one)
+ dir := fmt.Sprintf("testdata/corpus/%s", target)
+ files, err := ioutil.ReadDir(dir)
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+ if len(files) != 1 {
+ fmt.Fprintf(os.Stderr, "expected one file, got %d", len(files))
+ os.Exit(1)
+ }
+ got, err := ioutil.ReadFile(filepath.Join(dir, files[0].Name()))
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+
+ // Make sure that there were exactly 100 bytes written to the corpus entry
+ prefix := []byte("[]byte(")
+ i := bytes.Index(got, prefix)
+ gotBytes := got[i+len(prefix) : len(got)-1]
+ s, err := strconv.Unquote(string(gotBytes))
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+ if want, got := numBytes, len(s); want != got {
+ fmt.Fprintf(os.Stderr, "want %d bytes, got %d", want, got)
+ os.Exit(1)
+ }
+}
-- mutator_test.go --
package fuzz_test
return nil
}
- // Unexpected termination. Inform the coordinator about the crash.
+ // Unexpected termination. Attempt to minimize, then inform the
+ // coordinator about the crash.
// TODO(jayconrod,katiehockman): if -keepfuzzing, restart worker.
- mem := <-w.memMu
- value := mem.valueCopy()
- w.memMu <- mem
+ // TODO(jayconrod,katiehockman): consider informing the
+ // coordinator that this worker is minimizing, in order to block
+ // the other workers from receiving more inputs.
message := fmt.Sprintf("fuzzing process terminated unexpectedly: %v", w.waitErr)
- w.coordinator.resultC <- fuzzResult{
- entry: CorpusEntry{Data: value},
- crasherMsg: message,
+ err = w.waitErr
+ res, minimized, minErr := w.minimize(ctx)
+ if !minimized {
+ // Minimization did not find a smaller crashing value, so
+ // return the one we already found.
+ res = fuzzResult{
+ entry: CorpusEntry{Data: value},
+ crasherMsg: message,
+ }
+ }
+ if minErr != nil {
+ err = minErr
}
- return w.waitErr
+ w.coordinator.resultC <- res
+ return err
}
-
result := fuzzResult{
countRequested: input.countRequested,
count: resp.Count,
}
}
+// minimize asks a workerServer to attempt to minimize what is currently in
+// shared memory. It runs for a maxium of 1 minute. The worker must be stopped
+// when minimize is called.
+func (w *worker) minimize(ctx context.Context) (res fuzzResult, minimized bool, retErr error) {
+ fmt.Fprint(w.coordinator.opts.Log, "found a crash, currently minimizing for up to 1 minute\n")
+ defer func() {
+ w.stop()
+ if retErr == nil {
+ retErr = w.waitErr
+ }
+ }()
+ // In case we can't minimize it at all, save the last crash value that we
+ // found to send to the coordinator once the time is up.
+ minimizeDeadline := time.Now().Add(time.Minute)
+ for rem := time.Until(minimizeDeadline); rem > 0; {
+ // Restart the worker.
+ if err := w.start(); err != nil {
+ return res, minimized, err
+ }
+ args := minimizeArgs{Duration: rem}
+ value, err := w.client.minimize(ctx, args)
+ if err == nil {
+ // Minimization finished successfully, meaning that it
+ // couldn't find any smaller inputs that caused a crash,
+ // so stop trying.
+ return res, minimized, nil
+ }
+ // Minimization will return an error for a non-recoverable problem, so
+ // a non-nil error is expected. However, make sure it didn't fail for
+ // some other reason which should cause us to stop minimizing.
+ if ctx.Err() != nil || w.interrupted || isInterruptError(w.waitErr) {
+ return res, minimized, nil
+ }
+
+ // The bytes in memory caused a legitimate crash, so stop the worker and
+ // save this value and error message.
+ w.stop()
+ message := fmt.Sprintf("fuzzing process terminated unexpectedly: %v", w.waitErr)
+ res = fuzzResult{
+ entry: CorpusEntry{Data: value},
+ crasherMsg: message,
+ }
+ minimized = true
+ }
+ return res, minimized, nil
+ // TODO(jayconrod,katiehockman): while minimizing, every panic message is
+ // logged to STDOUT. We should probably suppress all but the last one to
+ // lower the noise.
+}
+
// start runs a new worker process.
//
// If the process couldn't be started, start returns an error. Start won't
// a minimalist RPC mechanism. Exactly one of its fields must be set to indicate
// which method to call.
type call struct {
- Ping *pingArgs
- Fuzz *fuzzArgs
+ Ping *pingArgs
+ Fuzz *fuzzArgs
+ Minimize *minimizeArgs
}
+// minimizeArgs contains arguments to workerServer.minimize. The value to
+// minimize is already in shared memory.
+type minimizeArgs struct {
+ Duration time.Duration
+}
+
+// minimizeResponse contains results from workerServer.minimize.
+type minimizeResponse struct{}
+
// fuzzArgs contains arguments to workerServer.fuzz. The value to fuzz is
// passed in shared memory.
type fuzzArgs struct {
switch {
case c.Fuzz != nil:
resp = ws.fuzz(ctx, *c.Fuzz)
+ case c.Minimize != nil:
+ resp = ws.minimize(ctx, *c.Minimize)
case c.Ping != nil:
resp = ws.ping(ctx, *c.Ping)
default:
// time customizable with a go command flag.
minCtx, minCancel := context.WithTimeout(ctx, time.Minute)
defer minCancel()
- if minErr := ws.minimize(minCtx, vals, mem); minErr != nil {
+ if minErr := ws.minimizeInput(minCtx, vals, mem); minErr != nil {
// Minimization found a different error, so use that one.
err = minErr
}
}
}
-// minimize applies a series of minimizing transformations on the provided
+func (ws *workerServer) minimize(ctx context.Context, args minimizeArgs) minimizeResponse {
+ mem := <-ws.memMu
+ defer func() { ws.memMu <- mem }()
+ vals, err := unmarshalCorpusFile(mem.valueCopy())
+ if err != nil {
+ panic(err)
+ }
+ ctx, cancel := context.WithTimeout(ctx, args.Duration)
+ defer cancel()
+ ws.minimizeInput(ctx, vals, mem)
+ return minimizeResponse{}
+}
+
+// minimizeInput applies a series of minimizing transformations on the provided
// vals, ensuring that each minimization still causes an error in fuzzFn. Before
// every call to fuzzFn, it marshals the new vals and writes it to the provided
-// mem just in case an unrecoverable error occurs. It runs for a maximum of one
-// minute, and returns the last error it found.
-func (ws *workerServer) minimize(ctx context.Context, vals []interface{}, mem *sharedMem) (retErr error) {
+// mem just in case an unrecoverable error occurs. It uses the context to
+// determine how long to run, stopping once closed. It returns the last error it
+// found.
+func (ws *workerServer) minimizeInput(ctx context.Context, vals []interface{}, mem *sharedMem) (retErr error) {
// Make sure the last crashing value is written to mem.
defer writeToMem(vals, mem)
// interrupted.
var errSharedMemClosed = errors.New("internal error: shared memory was closed and unmapped")
+// minimize tells the worker to call the minimize method. See
+// workerServer.minimize.
+func (wc *workerClient) minimize(ctx context.Context, args minimizeArgs) (valueOut []byte, err error) {
+ wc.mu.Lock()
+ defer wc.mu.Unlock()
+
+ var resp minimizeResponse
+ c := call{Minimize: &args}
+ err = wc.call(ctx, c, &resp)
+ mem, ok := <-wc.memMu
+ if !ok {
+ return nil, errSharedMemClosed
+ }
+ valueOut = mem.valueCopy()
+ wc.memMu <- mem
+
+ return valueOut, err
+}
+
// fuzz tells the worker to call the fuzz method. See workerServer.fuzz.
func (wc *workerClient) fuzz(ctx context.Context, valueIn []byte, args fuzzArgs) (valueOut []byte, resp fuzzResponse, err error) {
wc.mu.Lock()