diff --git a/README.md b/README.md index 386c2a4..642c7db 100644 --- a/README.md +++ b/README.md @@ -68,4 +68,22 @@ f.Fuzz(&myObject) // Type will correspond to whether A or B info is set. See more examples in ```example_test.go```. +You can use this library for easier [go-fuzz](https://github.com/dvyukov/go-fuzz)ing. +go-fuzz provides the user a byte-slice, which should be converted to different inputs +for the tested function. This library can help convert the byte slice. Consider for +example a fuzz test for a the function `mypackage.MyFunc` that takes an int arguments: +```go +// +build gofuzz +package mypackage + +import "github.com/google/go-fuzz" + +func Fuzz(data []byte) int { + var i int + fuzz.NewFromGoFuzz(data).Fuzz(&i) + MyFunc(i) + return 0 +} +``` + Happy testing! diff --git a/bytesource/bytesource.go b/bytesource/bytesource.go new file mode 100644 index 0000000..5bb3659 --- /dev/null +++ b/bytesource/bytesource.go @@ -0,0 +1,81 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package bytesource provides a rand.Source64 that is determined by a slice of bytes. +package bytesource + +import ( + "bytes" + "encoding/binary" + "io" + "math/rand" +) + +// ByteSource implements rand.Source64 determined by a slice of bytes. The random numbers are +// generated from each 8 bytes in the slice, until the last bytes are consumed, from which a +// fallback pseudo random source is created in case more random numbers are required. +// It also exposes a `bytes.Reader` API, which lets callers consume the bytes directly. +type ByteSource struct { + *bytes.Reader + fallback rand.Source +} + +// New returns a new ByteSource from a given slice of bytes. +func New(input []byte) *ByteSource { + s := &ByteSource{ + Reader: bytes.NewReader(input), + fallback: rand.NewSource(0), + } + if len(input) > 0 { + s.fallback = rand.NewSource(int64(s.consumeUint64())) + } + return s +} + +func (s *ByteSource) Uint64() uint64 { + // Return from input if it was not exhausted. + if s.Len() > 0 { + return s.consumeUint64() + } + + // Input was exhausted, return random number from fallback (in this case fallback should not be + // nil). Try first having a Uint64 output (Should work in current rand implementation), + // otherwise return a conversion of Int63. + if s64, ok := s.fallback.(rand.Source64); ok { + return s64.Uint64() + } + return uint64(s.fallback.Int63()) +} + +func (s *ByteSource) Int63() int64 { + return int64(s.Uint64() >> 1) +} + +func (s *ByteSource) Seed(seed int64) { + s.fallback = rand.NewSource(seed) + s.Reader = bytes.NewReader(nil) +} + +// consumeUint64 reads 8 bytes from the input and convert them to a uint64. It assumes that the the +// bytes reader is not empty. +func (s *ByteSource) consumeUint64() uint64 { + var bytes [8]byte + _, err := s.Read(bytes[:]) + if err != nil && err != io.EOF { + panic("failed reading source") // Should not happen. + } + return binary.BigEndian.Uint64(bytes[:]) +} diff --git a/bytesource/bytesource_test.go b/bytesource/bytesource_test.go new file mode 100644 index 0000000..609cf5e --- /dev/null +++ b/bytesource/bytesource_test.go @@ -0,0 +1,109 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package bytesource + +import ( + "math/rand" + "testing" +) + +func TestByteSource(t *testing.T) { + t.Parallel() + + randFromSource := func(s ...byte) *rand.Rand { + return rand.New(New(s)) + } + + randFromSeed := func(seed int64) *rand.Rand { + var s1 ByteSource + s1.Seed(seed) + return rand.New(&s1) + } + + t.Run("Inputs with identical 8 byte prefix", func(t *testing.T) { + rand1 := randFromSource(1, 2, 3, 4, 5, 6, 7, 8, 9) + rand2 := randFromSource(1, 2, 3, 4, 5, 6, 7, 8, 9) + if rand1.Int() != rand2.Int() { + t.Errorf("Inputs with identical 9 byte prefix result in different 1st output.") + } + if rand1.Int() != rand2.Int() { + t.Errorf("Inputs with identical 9 byte prefix result in different 2nd output.") + } + }) + + t.Run("Inputs with different 8 byte prefix", func(t *testing.T) { + rand2 := randFromSource(1, 2, 3, 4, 5, 6, 7, 1, 9) + rand1 := randFromSource(1, 2, 3, 4, 5, 6, 7, 8, 9) + if rand1.Int() != rand2.Int() { + t.Errorf("Inputs with identical 9th byte prefix result in different 1st output.") + } + if rand1.Int() == rand2.Int() { + t.Errorf("Inputs with different 8 bytes prefix result in identical 2nd output.") + } + }) + + t.Run("Multiple invocation", func(t *testing.T) { + // First random from input byte, second from random source. + r := randFromSource(1, 2, 3, 4, 6, 7, 8, 9) + if r.Int() == r.Int() { + t.Errorf("Two random numbers are identical.") + } + // First and second numbers from random source. + r = randFromSource(1) + if r.Int() == r.Int() { + t.Errorf("Two random numbers are identical.") + } + }) + + t.Run("Seed", func(t *testing.T) { + if randFromSeed(42).Int() != randFromSeed(42).Int() { + t.Error("Two random numbers from the same seed differ.") + } + if randFromSeed(42).Int() == randFromSeed(43).Int() { + t.Error("Two random numbers from different seeds are identical.") + } + }) +} + +func TestByteSourceValues(t *testing.T) { + t.Parallel() + + // Data in chunks of 8 bytes. + data := []byte{ + 99, 12, 23, 12, 65, 34, 12, 12, + 99, 12, 23, 12, 25, 34, 15, 13, + 99, 12, 23, 42, 25, 34, 11, 14, + 99, 12, 54, 12, 25, 34, 99, 11, + } + + r := rand.New(New(data)) + + got := []int{r.Int(), r.Int(), r.Int(), r.Int(), r.Int()} + + want := []int{ + 3568552425102051206, + 3568552489526560135, + 3568569467532292485, + 7616166771204380295, + 5210010188159375967, + } + + for i := range got { + if want[i] != got[i] { + t.Errorf("want[%d] = %d, got: %d", i, want[i], got[i]) + } + } +} diff --git a/fuzz.go b/fuzz.go index 049d7db..4562c9f 100644 --- a/fuzz.go +++ b/fuzz.go @@ -22,6 +22,8 @@ import ( "reflect" "regexp" "time" + + "github.com/google/gofuzz/bytesource" ) // fuzzFuncMap is a map from a type to a fuzzFunc that handles that type. @@ -61,6 +63,34 @@ func NewWithSeed(seed int64) *Fuzzer { return f } +// NewFromGoFuzz is a helper function that enables using gofuzz (this +// project) with go-fuzz (https://github.com/dvyukov/go-fuzz) for continuous +// fuzzing. Essentially, it enables translating the fuzzing bytes from +// go-fuzz to any Go object using this library. +// +// This implementation promises a constant translation from a given slice of +// bytes to the fuzzed objects. This promise will remain over future +// versions of Go and of this library. +// +// Note: the returned Fuzzer should not be shared between multiple goroutines, +// as its deterministic output will no longer be available. +// +// Example: use go-fuzz to test the function `MyFunc(int)` in the package +// `mypackage`. Add the file: "mypacakge_fuzz.go" with the content: +// +// // +build gofuzz +// package mypacakge +// import "github.com/google/go-fuzz" +// func Fuzz(data []byte) int { +// var i int +// fuzz.NewFromGoFuzz(data).Fuzz(&i) +// MyFunc(i) +// return 0 +// } +func NewFromGoFuzz(data []byte) *Fuzzer { + return New().RandSource(bytesource.New(data)) +} + // Funcs adds each entry in fuzzFuncs as a custom fuzzing function. // // Each entry in fuzzFuncs must be a function taking two parameters. diff --git a/fuzz_test.go b/fuzz_test.go index 963ea59..e7371fc 100644 --- a/fuzz_test.go +++ b/fuzz_test.go @@ -553,3 +553,16 @@ func Test_charRange_choose(t *testing.T) { } }) } + +func TestNewFromGoFuzz(t *testing.T) { + t.Parallel() + + input := []byte{1, 2, 3} + + var got int + NewFromGoFuzz(input).Fuzz(&got) + + if want := 5563767293437588600; want != got { + t.Errorf("Fuzz(%q) = %d, want: %d", input, got, want) + } +}