]> Cypherpunks repositories - gostls13.git/commitdiff
[dev.fuzz] internal/fuzz: add minimization of []byte
authorKatie Hockman <katie@golang.org>
Wed, 3 Mar 2021 19:16:07 +0000 (14:16 -0500)
committerKatie Hockman <katie@golang.org>
Thu, 18 Mar 2021 16:07:17 +0000 (16:07 +0000)
This works by minimizing for a maximum of one minute. We may consider
making this customizable in the future.

This only minimizes []byte inputs which caused a recoverable error. In
the future, it should support minimizing other appopriate types, and
minimizing types which caused non-recoverable errors (though this is
much more expensive).

The code in internal/fuzz/worker.go is copied from, or heavily inspired
by, code originally authored by Dmitry Vyukov and Josh Bleecher Snyder
as part of the go-fuzz project. Thanks to them for their contributions.
See https://github.com/dvyukov/go-fuzz.

Change-Id: I93dbac7ff874d6d0c1b9b9dda23930ae9921480c
Reviewed-on: https://go-review.googlesource.com/c/go/+/298909
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>
src/cmd/go/testdata/script/test_fuzz_mutator.txt
src/internal/fuzz/worker.go

index 4a33eba339ded0d12bb4e6a2a00f05b48cfd9c27..c5a7a86b84f10aebae6a47bf3b05c5694032557f 100644 (file)
@@ -14,11 +14,21 @@ go test -fuzz=FuzzA -fuzztime=5s -parallel=1 -log=fuzz
 go run check_logs.go fuzz fuzz.worker
 
 # Test that the mutator is good enough to find several unique mutations.
-! go test -fuzz=Fuzz -parallel=1 -fuzztime=30s mutator_test.go
+! go test -fuzz=FuzzMutator -parallel=1 -fuzztime=30s mutator_test.go
 ! stdout '^ok'
 stdout FAIL
 stdout 'mutator found enough unique mutations'
 
+# Test that minimization is working.
+! go test -fuzz=FuzzMinimizer -run=FuzzMinimizer -parallel=1 -fuzztime=5s minimizer_test.go
+! stdout ok
+# TODO(jayconrod,katiehockman): Once logging is fixed, add this test in.
+# stdout 'got the minimum size!'
+stdout FAIL
+
+# Test that re-running the minimized value causes a crash.
+! go test -run=FuzzMinimizer minimizer_test.go
+
 -- go.mod --
 module m
 
@@ -67,6 +77,31 @@ func FuzzB(f *testing.F) {
        })
 }
 
+-- minimizer_test.go --
+package fuzz_test
+
+import (
+       "bytes"
+       "testing"
+)
+
+func FuzzMinimizer(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
+               }
+               if len(b) == 100 {
+                       t.Logf("got the minimum size!")
+               }
+               if bytes.ContainsAny(b, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") {
+                       // Given the randomness of the mutations, this should allow the
+                       // minimizer to trim down the value quite a bit.
+                       t.Errorf("contains a letter")
+               }
+       })
+}
+
 -- check_logs.go --
 // +build ignore
 
@@ -170,7 +205,7 @@ import (
 
 // TODO(katiehockman): re-work this test once we have a better fuzzing engine
 // (ie. more mutations, and compiler instrumentation)
-func Fuzz(f *testing.F) {
+func FuzzMutator(f *testing.F) {
        // TODO(katiehockman): simplify this once we can dedupe crashes (e.g.
        // replace map with calls to panic, and simply count the number of crashes
        // that were added to testdata)
index d42044bb915d4a702711a43ec07da29a1f8c6e11..70d76d6fc619f8055eb4505bc0143f7823e68ee8 100644 (file)
@@ -492,7 +492,7 @@ func (ws *workerServer) serve(ctx context.Context) error {
 // 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(ctx context.Context, args fuzzArgs) fuzzResponse {
-       ctx, cancel := context.WithTimeout(ctx, args.Duration)
+       fuzzCtx, cancel := context.WithTimeout(ctx, args.Duration)
        defer cancel()
        mem := <-ws.memMu
        defer func() { ws.memMu <- mem }()
@@ -503,16 +503,19 @@ func (ws *workerServer) fuzz(ctx context.Context, args fuzzArgs) fuzzResponse {
        }
        for {
                select {
-               case <-ctx.Done():
+               case <-fuzzCtx.Done():
                        // TODO(jayconrod,katiehockman): this value is not interesting. Use a
                        // real heuristic once we have one.
                        return fuzzResponse{Interesting: true}
                default:
                        vals = ws.m.mutate(vals, cap(mem.valueRef()))
-                       b := marshalCorpusFile(vals...)
-                       mem.setValueLen(len(b))
-                       mem.setValue(b)
+                       writeToMem(vals, mem)
                        if err := ws.fuzzFn(CorpusEntry{Values: vals}); err != nil {
+                               if minErr := ws.minimize(ctx, vals, mem); minErr != nil {
+                                       // Minimization found a different error, so use that one.
+                                       writeToMem(vals, mem)
+                                       err = minErr
+                               }
                                return fuzzResponse{Crashed: true, Err: err.Error()}
                        }
                        // TODO(jayconrod,katiehockman): return early if we find an
@@ -521,6 +524,110 @@ func (ws *workerServer) fuzz(ctx context.Context, args fuzzArgs) fuzzResponse {
        }
 }
 
+// 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) error {
+       // TODO(jayconrod,katiehockman): consider making the maximum minimization
+       // time customizable with a go command flag.
+       ctx, cancel := context.WithTimeout(ctx, time.Minute)
+       defer cancel()
+       var retErr error
+
+       // tryMinimized will run the fuzz function for the values in vals at the
+       // time the function is called. If err is nil, then the minimization was
+       // unsuccessful, since we expect an error to still occur.
+       tryMinimized := func(i int, prevVal interface{}) error {
+               err := ws.fuzzFn(CorpusEntry{Values: vals})
+               if err == nil {
+                       // The fuzz function succeeded, so return the value at index i back
+                       // to the previously failing input.
+                       vals[i] = prevVal
+               } else {
+                       // The fuzz function failed, so save the most recent error.
+                       retErr = err
+               }
+               return err
+       }
+       for valI := range vals {
+               switch v := vals[valI].(type) {
+               case bool, byte, rune:
+                       continue // can't minimize
+               case string, int, int8, int16, int64, uint, uint16, uint32, uint64, float32, float64:
+                       // TODO(jayconrod,katiehockman): support minimizing other types
+               case []byte:
+                       // First, try to cut the tail.
+                       for n := 1024; n != 0; n /= 2 {
+                               for len(v) > n {
+                                       if ctx.Done() != nil {
+                                               return retErr
+                                       }
+                                       vals[valI] = v[:len(v)-n]
+                                       if tryMinimized(valI, v) != nil {
+                                               break
+                                       }
+                                       // Set v to the new value to continue iterating.
+                                       v = v[:len(v)-n]
+                               }
+                       }
+
+                       // Then, try to remove each individual byte.
+                       tmp := make([]byte, len(v))
+                       for i := 0; i < len(v)-1; i++ {
+                               if ctx.Done() != nil {
+                                       return retErr
+                               }
+                               candidate := tmp[:len(v)-1]
+                               copy(candidate[:i], v[:i])
+                               copy(candidate[i:], v[i+1:])
+                               vals[valI] = candidate
+                               if tryMinimized(valI, v) != nil {
+                                       continue
+                               }
+                               // Update v to delete the value at index i.
+                               copy(v[i:], v[i+1:])
+                               v = v[:len(candidate)]
+                               // v[i] is now different, so decrement i to redo this iteration
+                               // of the loop with the new value.
+                               i--
+                       }
+
+                       // Then, try to remove each possible subset of bytes.
+                       for i := 0; i < len(v)-1; i++ {
+                               copy(tmp, v[:i])
+                               for j := len(v); j > i+1; j-- {
+                                       if ctx.Done() != nil {
+                                               return retErr
+                                       }
+                                       candidate := tmp[:len(v)-j+i]
+                                       copy(candidate[i:], v[j:])
+                                       vals[valI] = candidate
+                                       if tryMinimized(valI, v) != nil {
+                                               continue
+                                       }
+                                       // Update v and reset the loop with the new length.
+                                       copy(v[i:], v[j:])
+                                       v = v[:len(candidate)]
+                                       j = len(v)
+                               }
+                       }
+                       // TODO(jayconrod,katiehockman): consider adding canonicalization
+                       // which replaces each individual byte with '0'
+               default:
+                       panic("unreachable")
+               }
+       }
+       return retErr
+}
+
+func writeToMem(vals []interface{}, mem *sharedMem) {
+       b := marshalCorpusFile(vals...)
+       mem.setValueLen(len(b))
+       mem.setValue(b)
+}
+
 // ping does nothing. The coordinator calls this method to ensure the worker
 // has called F.Fuzz and can communicate.
 func (ws *workerServer) ping(ctx context.Context, args pingArgs) pingResponse {