Skip to content

Commit

Permalink
internal/fuzz: limit number of consecutive mutations
Browse files Browse the repository at this point in the history
This makes two changes: (1) mutator.mutate now only applies a single
mutation to the input, and (2) in workerServer.fuzz if, after five
mutations are applied to the input, no new coverage is found the input
is reset to its initial state. This process is repeated until new
coverage is found, or the fuzz call times out.

This results in finding new coverage expanding inputs which have less
divergence from the initial input they were mutated from, which makes
traversing certain types of call graphs significantly more efficient.

Fixes #49601
Fixes #48179
Fixes #47090

Change-Id: I74d18a56ca2669f20192951090b281f58ee0b5dc
Reviewed-on: https://go-review.googlesource.com/c/go/+/364214
Trust: Roland Shoemaker <[email protected]>
Trust: Katie Hockman <[email protected]>
Run-TryBot: Roland Shoemaker <[email protected]>
TryBot-Result: Go Bot <[email protected]>
Reviewed-by: Katie Hockman <[email protected]>
  • Loading branch information
rolandshoemaker committed Nov 17, 2021
1 parent 54b9cb8 commit ab75484
Show file tree
Hide file tree
Showing 2 changed files with 35 additions and 27 deletions.
39 changes: 15 additions & 24 deletions src/internal/fuzz/mutator.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,48 +125,44 @@ func (m *mutator) mutate(vals []interface{}, maxBytes int) {
}

func (m *mutator) mutateInt(v, maxValue int64) int64 {
numIters := 1 + m.r.exp2()
var max int64
for iter := 0; iter < numIters; iter++ {
for {
max = 100
switch m.rand(2) {
case 0:
// Add a random number
if v >= maxValue {
iter--
continue
}
if v > 0 && maxValue-v < max {
// Don't let v exceed maxValue
max = maxValue - v
}
v += int64(1 + m.rand(int(max)))
return v
case 1:
// Subtract a random number
if v <= -maxValue {
iter--
continue
}
if v < 0 && maxValue+v < max {
// Don't let v drop below -maxValue
max = maxValue + v
}
v -= int64(1 + m.rand(int(max)))
return v
}
}
return v
}

func (m *mutator) mutateUInt(v, maxValue uint64) uint64 {
numIters := 1 + m.r.exp2()
var max uint64
for iter := 0; iter < numIters; iter++ {
for {
max = 100
switch m.rand(2) {
case 0:
// Add a random number
if v >= maxValue {
iter--
continue
}
if v > 0 && maxValue-v < max {
Expand All @@ -175,31 +171,29 @@ func (m *mutator) mutateUInt(v, maxValue uint64) uint64 {
}

v += uint64(1 + m.rand(int(max)))
return v
case 1:
// Subtract a random number
if v <= 0 {
iter--
continue
}
if v < max {
// Don't let v drop below 0
max = v
}
v -= uint64(1 + m.rand(int(max)))
return v
}
}
return v
}

func (m *mutator) mutateFloat(v, maxValue float64) float64 {
numIters := 1 + m.r.exp2()
var max float64
for iter := 0; iter < numIters; iter++ {
for {
switch m.rand(4) {
case 0:
// Add a random number
if v >= maxValue {
iter--
continue
}
max = 100
Expand All @@ -208,10 +202,10 @@ func (m *mutator) mutateFloat(v, maxValue float64) float64 {
max = maxValue - v
}
v += float64(1 + m.rand(int(max)))
return v
case 1:
// Subtract a random number
if v <= -maxValue {
iter--
continue
}
max = 100
Expand All @@ -220,11 +214,11 @@ func (m *mutator) mutateFloat(v, maxValue float64) float64 {
max = maxValue + v
}
v -= float64(1 + m.rand(int(max)))
return v
case 2:
// Multiply by a random number
absV := math.Abs(v)
if v == 0 || absV >= maxValue {
iter--
continue
}
max = 10
Expand All @@ -233,16 +227,16 @@ func (m *mutator) mutateFloat(v, maxValue float64) float64 {
max = maxValue / absV
}
v *= float64(1 + m.rand(int(max)))
return v
case 3:
// Divide by a random number
if v == 0 {
iter--
continue
}
v /= float64(1 + m.rand(10))
return v
}
}
return v
}

type byteSliceMutator func(*mutator, []byte) []byte
Expand Down Expand Up @@ -279,15 +273,12 @@ func (m *mutator) mutateBytes(ptrB *[]byte) {
*ptrB = b
}()

numIters := 1 + m.r.exp2()
for iter := 0; iter < numIters; iter++ {
for {
mut := byteSliceMutators[m.rand(len(byteSliceMutators))]
mutated := mut(m, b)
if mutated == nil {
iter--
continue
if mutated := mut(m, b); mutated != nil {
b = mutated
return
}
b = mutated
}
}

Expand Down
23 changes: 20 additions & 3 deletions src/internal/fuzz/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,17 @@ func (ws *workerServer) serve(ctx context.Context) error {
}
}

// chainedMutations is how many mutations are applied before the worker
// resets the input to it's original state.
// NOTE: this number was picked without much thought. It is low enough that
// it seems to create a significant diversity in mutated inputs. We may want
// to consider looking into this more closely once we have a proper performance
// testing framework. Another option is to randomly pick the number of chained
// mutations on each invocation of the workerServer.fuzz method (this appears to
// be what libFuzzer does, although there seems to be no documentation which
// explains why this choice was made.)
const chainedMutations = 5

// fuzz runs the test function on random variations of the input value in shared
// memory for a limited duration or number of iterations.
//
Expand Down Expand Up @@ -699,11 +710,13 @@ func (ws *workerServer) fuzz(ctx context.Context, args fuzzArgs) (resp fuzzRespo
return resp
}

vals, err := unmarshalCorpusFile(mem.valueCopy())
originalVals, err := unmarshalCorpusFile(mem.valueCopy())
if err != nil {
resp.InternalErr = err.Error()
return resp
}
vals := make([]interface{}, len(originalVals))
copy(vals, originalVals)

shouldStop := func() bool {
return args.Limit > 0 && mem.header().count >= args.Limit
Expand Down Expand Up @@ -742,9 +755,13 @@ func (ws *workerServer) fuzz(ctx context.Context, args fuzzArgs) (resp fuzzRespo
select {
case <-ctx.Done():
return resp

default:
if mem.header().count%chainedMutations == 0 {
copy(vals, originalVals)
ws.m.r.save(&mem.header().randState, &mem.header().randInc)
}
ws.m.mutate(vals, cap(mem.valueRef()))

entry := CorpusEntry{Values: vals}
dur, cov, errMsg := fuzzOnce(entry)
if errMsg != "" {
Expand Down Expand Up @@ -1094,7 +1111,7 @@ func (wc *workerClient) fuzz(ctx context.Context, entryIn CorpusEntry, args fuzz
wc.m.r.restore(mem.header().randState, mem.header().randInc)
if !args.Warmup {
// Only mutate the valuesOut if fuzzing actually occurred.
for i := int64(0); i < resp.Count; i++ {
for i := int64(0); i < resp.Count%chainedMutations; i++ {
wc.m.mutate(valuesOut, cap(mem.valueRef()))
}
}
Expand Down

0 comments on commit ab75484

Please sign in to comment.