From f3d0cb176c97ce037d36eb1402e6d349c4fcc1a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Ber=C4=8Di=C4=8D?= Date: Fri, 13 Dec 2019 15:59:25 +0100 Subject: [PATCH] go/storage: Add backend api fuzzing --- .changelog/2246.feature.md | 4 + go/Makefile | 30 ++++--- go/common/fuzz/fuzz.go | 129 ++++++++++++++++++++++++++++++ go/storage/fuzz/.gitignore | 5 ++ go/storage/fuzz/fuzz.go | 62 ++++++++++++++ go/storage/fuzz/gencorpus/main.go | 39 +++++++++ 6 files changed, 258 insertions(+), 11 deletions(-) create mode 100644 .changelog/2246.feature.md create mode 100644 go/storage/fuzz/.gitignore create mode 100644 go/storage/fuzz/fuzz.go create mode 100644 go/storage/fuzz/gencorpus/main.go diff --git a/.changelog/2246.feature.md b/.changelog/2246.feature.md new file mode 100644 index 00000000000..507b195d276 --- /dev/null +++ b/.changelog/2246.feature.md @@ -0,0 +1,4 @@ +Add storage backend fuzzing. + +Based on the work done for consensus fuzzing, support was added to run fuzzing +jobs on the storage api backend. diff --git a/go/Makefile b/go/Makefile index dde54c1e611..b6590407356 100644 --- a/go/Makefile +++ b/go/Makefile @@ -65,22 +65,30 @@ integrationrunner: @$(GO) test $(GOFLAGS) -c -covermode=atomic -coverpkg=./... -o oasis-node/$@/$@.test ./oasis-node/$@ # Fuzzing binaries. -build-fuzz: consensus/tendermint/fuzz/fuzz-fuzz.zip -consensus/tendermint/fuzz/fuzz-fuzz.zip: .FORCE - @echo "Building consensus fuzzer" +build-fuzz: consensus/tendermint/fuzz/fuzz-fuzz.zip storage/fuzz/fuzz-fuzz.zip +%/fuzz-fuzz.zip: .FORCE + @echo "Building $@" @cd "$$(dirname "$@")"; go-fuzz-build @cd "$$(dirname "$@")/gencorpus"; env -u GOPATH $(OASIS_GO) build -tags gofuzz # Run fuzzing. +define canned-fuzz-run +set -x; cd "$<"; \ +if ! [ -d corpus ]; then \ + mkdir corpus; \ + pushd corpus; \ + ../gencorpus/gencorpus; \ + popd; \ +fi; \ +go-fuzz -bin=./fuzz-fuzz.zip +endef fuzz-consensus: consensus/tendermint/fuzz/ - set -x; cd "$<"; \ - if ! [ -d corpus ]; then \ - mkdir corpus; \ - pushd corpus; \ - ../gencorpus/gencorpus; \ - popd; \ - fi; \ - go-fuzz -bin=./fuzz-fuzz.zip + $(canned-fuzz-run) +fuzz-storage: storage/fuzz/ oasis-node/oasis-node + @mkdir -p /tmp/oasis-node-fuzz-storage/identity + @chmod 0700 /tmp/oasis-node-fuzz-storage/identity + @oasis-node/oasis-node identity init --datadir /tmp/oasis-node-fuzz-storage/identity + $(canned-fuzz-run) # Clean. clean: diff --git a/go/common/fuzz/fuzz.go b/go/common/fuzz/fuzz.go index 52649fcdb0e..2d15afd7618 100644 --- a/go/common/fuzz/fuzz.go +++ b/go/common/fuzz/fuzz.go @@ -4,7 +4,9 @@ package fuzz import ( + "context" "encoding/binary" + "fmt" "math/rand" "reflect" @@ -103,3 +105,130 @@ func MakeSampleBlob(typ interface{}) []byte { return source.GetTraceback() } + +// InterfaceFuzzer is a helper class for fuzzing methods in structs or interfaces. +type InterfaceFuzzer struct { + instance interface{} + + typeObject reflect.Type + valObject reflect.Value + + methodList []int + + typeOverrides map[string]func()interface{} +} + +// OverrideType registers a custom callback for creating instances of a given type. +func (i *InterfaceFuzzer) OverrideType(typeName string, factory func()interface{}) { + i.typeOverrides[typeName] = factory +} + +// DispatchBlob constructs a method call with arguments from the given blob and dispatches it. +func (i *InterfaceFuzzer) DispatchBlob(blob []byte) ([]reflect.Value, bool) { + if len(blob) < 1 { + return nil, false + } + + meth := int(blob[0]) + if meth >= len(i.methodList) { + return nil, false + } + meth = i.methodList[meth] + if meth >= i.typeObject.NumMethod() { + return nil, false + } + methType := i.typeObject.Method(meth).Type + method := i.valObject.Method(meth) + + source := NewRandSource(blob[1:]) + fuzzer := gofuzz.New() + fuzzer = fuzzer.RandSource(source).NilChance(0) + + in := []reflect.Value{} + + for arg := 1; arg < methType.NumIn(); arg++ { + inType := methType.In(arg) + inTypeName := fmt.Sprintf("%s.%s", inType.PkgPath(), inType.Name()) + + var val reflect.Value + if factory, ok := i.typeOverrides[inTypeName]; ok { + inst := factory() + val = reflect.ValueOf(inst) + } else { + val = reflect.New(inType) + if val.Interface() != nil { + fuzzer.Fuzz(val.Interface()) + } + val = val.Elem() + } + in = append(in, val) + } + + return method.Call(in), true +} + +// MakeSampleBlobs returns an array of sample blobs for all methods in the interface. +func (i *InterfaceFuzzer) MakeSampleBlobs() [][]byte { + blobList := [][]byte{} + for seq, meth := range i.methodList { + source := NewTrackingRandSource() + fuzzer := gofuzz.New() + fuzzer = fuzzer.RandSource(source).NilChance(0) + + method := i.typeObject.Method(meth) + blob := []byte{byte(seq)} + for arg := 1; arg < method.Type.NumIn(); arg++ { + inType := method.Type.In(arg) + inTypeName := fmt.Sprintf("%s.%s", inType.PkgPath(), inType.Name()) + if _, ok := i.typeOverrides[inTypeName]; !ok { + newValue := reflect.New(inType) + if newValue.Interface() != nil { + fuzzer.Fuzz(newValue.Interface()) + } + } + } + + blob = append(blob, source.GetTraceback()...) + blobList = append(blobList, blob) + } + + return blobList +} + +// Method returns the method object associated with the fuzzer's index-th method for this instance. +func (i *InterfaceFuzzer) Method(method int) reflect.Method { + return i.typeObject.Method(i.methodList[method]) +} + +// IgnoreMethodNames makes the interface fuzzer skip the named methods. +func (i *InterfaceFuzzer) IgnoreMethodNames(names []string) { + for _, name := range names { + for listIndex, methIndex := range i.methodList { + if i.typeObject.Method(methIndex).Name == name { + i.methodList = append(i.methodList[:listIndex], i.methodList[listIndex+1:]...) + break + } + } + } +} + +// NewInterfaceFuzzer creates a new InterfaceFuzzer for the given instance. +func NewInterfaceFuzzer(instance interface{}) *InterfaceFuzzer { + val := reflect.ValueOf(instance) + ret := &InterfaceFuzzer{ + instance: instance, + typeObject: val.Type(), + valObject: val, + typeOverrides: map[string]func()interface{}{ + "context.Context": func()interface{}{ + return context.Background() + }, + }, + } + + for meth := 0; meth < val.NumMethod(); meth++ { + ret.methodList = append(ret.methodList, meth) + } + + return ret +} diff --git a/go/storage/fuzz/.gitignore b/go/storage/fuzz/.gitignore new file mode 100644 index 00000000000..c97791ee811 --- /dev/null +++ b/go/storage/fuzz/.gitignore @@ -0,0 +1,5 @@ +corpus/ +crashers/ +suppressions/ +*.zip +gencorpus/gencorpus diff --git a/go/storage/fuzz/fuzz.go b/go/storage/fuzz/fuzz.go new file mode 100644 index 00000000000..ec3c3380865 --- /dev/null +++ b/go/storage/fuzz/fuzz.go @@ -0,0 +1,62 @@ +// +build gofuzz + +package fuzz + +import ( + "context" + "io/ioutil" + + "github.com/oasislabs/oasis-core/go/common/crypto/signature" + "github.com/oasislabs/oasis-core/go/common/crypto/signature/signers/file" + commonFuzz "github.com/oasislabs/oasis-core/go/common/fuzz" + "github.com/oasislabs/oasis-core/go/common/identity" + "github.com/oasislabs/oasis-core/go/storage" + "github.com/oasislabs/oasis-core/go/storage/api" +) + +const ( + dataDir string = "/tmp/oasis-node-fuzz-storage" + identityDir string = dataDir + "/identity" +) + +var ( + storageBackend api.Backend + + fuzzer *commonFuzz.InterfaceFuzzer +) + +func init() { + signerFactory := file.NewFactory(identityDir, signature.SignerNode, signature.SignerP2P, signature.SignerConsensus) + identity, err := identity.Load(identityDir, signerFactory) + if err != nil { + panic(err) + } + + // Every Fuzz invocation should get its own database, + // otherwise the database handles would clash. + localDB, err := ioutil.TempDir(dataDir, "worker") + if err != nil { + panic(err) + } + + // Create the storage backend service. + storageBackend, err = storage.New(context.Background(), localDB, identity, nil, nil) + if err != nil { + panic(err) + } + + // Create and prepare the fuzzer. + fuzzer = commonFuzz.NewInterfaceFuzzer(storageBackend) + fuzzer.IgnoreMethodNames([]string{ + "Cleanup", + "Initialized", + }) +} + +func Fuzz(data []byte) int { + <-storageBackend.Initialized() + + fuzzer.DispatchBlob(data) + + return 0 +} diff --git a/go/storage/fuzz/gencorpus/main.go b/go/storage/fuzz/gencorpus/main.go new file mode 100644 index 00000000000..6eff196e563 --- /dev/null +++ b/go/storage/fuzz/gencorpus/main.go @@ -0,0 +1,39 @@ +// +build gofuzz + +// Gencorpus implements a simple utility to generate corpus files for the fuzzer. +// It has no command-line options and creates the files in the current working directory. +package main + +import ( + "context" + "fmt" + "io/ioutil" + + commonFuzz "github.com/oasislabs/oasis-core/go/common/fuzz" + "github.com/oasislabs/oasis-core/go/common/identity" + "github.com/oasislabs/oasis-core/go/storage" +) + +const ( + samplesPerMethod int = 20 +) + +func main() { + storage, err := storage.New(context.Background(), "/tmp/oasis-node-fuzz-storage", &identity.Identity{}, nil, nil) + if err != nil { + panic(err) + } + fuzzer := commonFuzz.NewInterfaceFuzzer(storage) + fuzzer.IgnoreMethodNames([]string{ + "Cleanup", + "Initialized", + }) + + for i := 0; i < samplesPerMethod; i++ { + blobs := fuzzer.MakeSampleBlobs() + for meth := 0; meth < len(blobs); meth++ { + fileName := fmt.Sprintf("%s_%02d.bin", fuzzer.Method(meth).Name, i) + _ = ioutil.WriteFile(fileName, blobs[meth], 0644) + } + } +}