utls/fuzz/mutators_byteslice_test.go
Jay Conrod f3f14acf5a [dev.fuzz] internal/fuzz: avoid marshaling input before calling fuzz function
Previously, before each call to the fuzz function, the worker process
marshalled the mutated input into shared memory. If the worker process
terminates unexpectedly, it's important that the coordinator can find
the crashing input in shared memory.

Profiling shows this marshalling is very expensive though. This change
takes another strategy. Instead of marshaling each mutated input, the
worker process no longer modifies the input in shared memory at
all. Instead, it saves its PRNG state in shared memory and increments
a counter before each fuzz function call. If the worker process
terminates, the coordinator can reconstruct the crashing value using
this information.

This change gives a ~10x increase in execs/s for a trivial fuzz
function with -parallel=1.

Change-Id: I18cf326c252727385dc53ea2518922b1f6ae36b6
Reviewed-on: https://go-review.googlesource.com/c/go/+/334149
Trust: Jay Conrod <jayconrod@google.com>
Trust: Katie Hockman <katie@golang.org>
Run-TryBot: Jay Conrod <jayconrod@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Katie Hockman <katie@golang.org>
2021-07-20 00:06:06 +00:00

179 lines
4.1 KiB
Go

// Copyright 2021 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
import (
"bytes"
"testing"
)
type mockRand struct {
counter int
b bool
}
func (mr *mockRand) uint32() uint32 {
c := mr.counter
mr.counter++
return uint32(c)
}
func (mr *mockRand) intn(n int) int {
c := mr.counter
mr.counter++
return c % n
}
func (mr *mockRand) uint32n(n uint32) uint32 {
c := mr.counter
mr.counter++
return uint32(c) % n
}
func (mr *mockRand) exp2() int {
c := mr.counter
mr.counter++
return c
}
func (mr *mockRand) bool() bool {
b := mr.b
mr.b = !mr.b
return b
}
func (mr *mockRand) save(*uint64, *uint64) {
panic("unimplemented")
}
func (mr *mockRand) restore(uint64, uint64) {
panic("unimplemented")
}
func TestByteSliceMutators(t *testing.T) {
for _, tc := range []struct {
name string
mutator func(*mutator, []byte) []byte
input []byte
expected []byte
}{
{
name: "byteSliceRemoveBytes",
mutator: byteSliceRemoveBytes,
input: []byte{1, 2, 3, 4},
expected: []byte{4},
},
{
name: "byteSliceInsertRandomBytes",
mutator: byteSliceInsertRandomBytes,
input: make([]byte, 4, 8),
expected: []byte{3, 4, 5, 0, 0, 0, 0},
},
{
name: "byteSliceDuplicateBytes",
mutator: byteSliceDuplicateBytes,
input: append(make([]byte, 0, 13), []byte{1, 2, 3, 4}...),
expected: []byte{1, 1, 2, 3, 4, 2, 3, 4},
},
{
name: "byteSliceOverwriteBytes",
mutator: byteSliceOverwriteBytes,
input: []byte{1, 2, 3, 4},
expected: []byte{1, 1, 3, 4},
},
{
name: "byteSliceBitFlip",
mutator: byteSliceBitFlip,
input: []byte{1, 2, 3, 4},
expected: []byte{3, 2, 3, 4},
},
{
name: "byteSliceXORByte",
mutator: byteSliceXORByte,
input: []byte{1, 2, 3, 4},
expected: []byte{3, 2, 3, 4},
},
{
name: "byteSliceSwapByte",
mutator: byteSliceSwapByte,
input: []byte{1, 2, 3, 4},
expected: []byte{2, 1, 3, 4},
},
{
name: "byteSliceArithmeticUint8",
mutator: byteSliceArithmeticUint8,
input: []byte{1, 2, 3, 4},
expected: []byte{255, 2, 3, 4},
},
{
name: "byteSliceArithmeticUint16",
mutator: byteSliceArithmeticUint16,
input: []byte{1, 2, 3, 4},
expected: []byte{1, 3, 3, 4},
},
{
name: "byteSliceArithmeticUint32",
mutator: byteSliceArithmeticUint32,
input: []byte{1, 2, 3, 4},
expected: []byte{2, 2, 3, 4},
},
{
name: "byteSliceArithmeticUint64",
mutator: byteSliceArithmeticUint64,
input: []byte{1, 2, 3, 4, 5, 6, 7, 8},
expected: []byte{2, 2, 3, 4, 5, 6, 7, 8},
},
{
name: "byteSliceOverwriteInterestingUint8",
mutator: byteSliceOverwriteInterestingUint8,
input: []byte{1, 2, 3, 4},
expected: []byte{255, 2, 3, 4},
},
{
name: "byteSliceOverwriteInterestingUint16",
mutator: byteSliceOverwriteInterestingUint16,
input: []byte{1, 2, 3, 4},
expected: []byte{255, 127, 3, 4},
},
{
name: "byteSliceOverwriteInterestingUint32",
mutator: byteSliceOverwriteInterestingUint32,
input: []byte{1, 2, 3, 4},
expected: []byte{250, 0, 0, 250},
},
{
name: "byteSliceInsertConstantBytes",
mutator: byteSliceInsertConstantBytes,
input: append(make([]byte, 0, 8), []byte{1, 2, 3, 4}...),
expected: []byte{3, 3, 3, 1, 2, 3, 4},
},
{
name: "byteSliceOverwriteConstantBytes",
mutator: byteSliceOverwriteConstantBytes,
input: []byte{1, 2, 3, 4},
expected: []byte{3, 3, 3, 4},
},
{
name: "byteSliceShuffleBytes",
mutator: byteSliceShuffleBytes,
input: []byte{1, 2, 3, 4},
expected: []byte{2, 3, 1, 4},
},
{
name: "byteSliceSwapBytes",
mutator: byteSliceSwapBytes,
input: append(make([]byte, 0, 9), []byte{1, 2, 3, 4}...),
expected: []byte{2, 1, 3, 4},
},
} {
t.Run(tc.name, func(t *testing.T) {
m := &mutator{r: &mockRand{}}
b := tc.mutator(m, tc.input)
if !bytes.Equal(b, tc.expected) {
t.Errorf("got %x, want %x", b, tc.expected)
}
})
}
}