diff --git a/.changelog/2245.feature.md b/.changelog/2245.feature.md new file mode 100644 index 00000000000..dffb035c60e --- /dev/null +++ b/.changelog/2245.feature.md @@ -0,0 +1,5 @@ +Add fuzzing for consensus methods. + +Initial support for fuzzing was added, along with an implementation of +it for some of the consensus methods. The implementation uses +oasis-core's demultiplexing and method dispatch mechanisms. 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 e49f6ef08e7..b6590407356 100644 --- a/go/Makefile +++ b/go/Makefile @@ -64,6 +64,32 @@ integrationrunner: @$(ECHO) "$(CYAN)*** Testing oasis-node with coverate...$(OFF)" @$(GO) test $(GOFLAGS) -c -covermode=atomic -coverpkg=./... -o oasis-node/$@/$@.test ./oasis-node/$@ +# Fuzzing binaries. +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/ + $(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: @$(ECHO) "$(CYAN)*** Cleaning up Go...$(OFF)" @@ -74,3 +100,5 @@ clean: generate $(go-binaries) build \ $(test-helpers) build-helpers $(test-vectors) gen-test-vectors \ fmt lint test integrationrunner clean all + +.FORCE: diff --git a/go/common/fuzz/fuzz.go b/go/common/fuzz/fuzz.go new file mode 100644 index 00000000000..2d15afd7618 --- /dev/null +++ b/go/common/fuzz/fuzz.go @@ -0,0 +1,234 @@ +// +build gofuzz + +// Package fuzz provides some common utilities useful for fuzzing other packages. +package fuzz + +import ( + "context" + "encoding/binary" + "fmt" + "math/rand" + "reflect" + + gofuzz "github.com/google/gofuzz" +) + +var ( + _ rand.Source64 = (*Source)(nil) +) + +// Source is a randomness source for the standard random generator. +type Source struct { + Backing []byte + Exhausted int + + pos int + track bool + + traceback []byte +} + +func (s *Source) Int63() int64 { + return int64(s.Uint64()&((1<<63)-1)) +} + +func (s *Source) Seed(_ int64) { + // Nothing to do here. +} + +func (s *Source) Uint64() uint64 { + if s.pos+8 > len(s.Backing) { + s.Exhausted += 8 + r := rand.Uint64() + if s.track { + chunk := make([]byte, 8) + binary.BigEndian.PutUint64(chunk[0:], r) + s.traceback = append(s.traceback, chunk...) + } + return r + } + + s.pos += 8 + return binary.BigEndian.Uint64(s.Backing[s.pos-8 : s.pos]) +} + +// GetTraceback returns the array of bytes returned from the random generator so far. +func (s *Source) GetTraceback() []byte { + return s.traceback +} + +// NewRandSource returns a new random source with the given backing array. +func NewRandSource(backing []byte) *Source { + return &Source{ + Backing: backing, + } +} + +// NewTrackingRandSource returns a new random source that keeps track of the bytes returned. +func NewTrackingRandSource() *Source { + return &Source{ + Backing: []byte{}, + track: true, + } +} + +// NewFilledInstance fills the given object with random values from the given blob. +func NewFilledInstance(data []byte, typ interface{}) (interface{}, bool) { + if typ == nil { + return nil, true + } + + source := NewRandSource(data) + fuzzer := gofuzz.New() + fuzzer = fuzzer.RandSource(source) + + obj := reflect.New(reflect.TypeOf(typ)).Interface() + + fuzzer.Fuzz(obj) + + return obj, source.Exhausted == 0 +} + +// MakeSampleBlob creates and returns a sample blob of bytes for filling the given object. +func MakeSampleBlob(typ interface{}) []byte { + if typ == nil { + return []byte{} + } + + source := NewTrackingRandSource() + fuzzer := gofuzz.New() + fuzzer = fuzzer.RandSource(source) + + obj := reflect.New(reflect.TypeOf(typ)).Interface() + + fuzzer.Fuzz(obj) + + 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/consensus/tendermint/abci/mux_mock.go b/go/consensus/tendermint/abci/mux_mock.go new file mode 100644 index 00000000000..542d0c1dc7d --- /dev/null +++ b/go/consensus/tendermint/abci/mux_mock.go @@ -0,0 +1,34 @@ +// +build gofuzz + +package abci + +import ( + "context" +) + +// MockABCIMux exports some of the muxer's internal methods for testing use. +type MockABCIMux struct { + *abciMux +} + +// MockRegisterApp is used to register apps with this muxer during testing. +func (mux *MockABCIMux) MockRegisterApp(app Application) error { + return mux.doRegister(app) +} + +// MockClose cleans up the muxer's state; it must be called once the muxer is no longer needed. +func (mux *MockABCIMux) MockClose() { + mux.doCleanup() +} + +// NewMockMux creates a new ABCI mux suitable for testing. +func NewMockMux(ctx context.Context, cfg *ApplicationConfig) (*MockABCIMux, error) { + mux, err := newABCIMux(ctx, cfg) + if err != nil { + return nil, err + } + mockMux := &MockABCIMux{ + abciMux: mux, + } + return mockMux, nil +} diff --git a/go/consensus/tendermint/fuzz/.gitignore b/go/consensus/tendermint/fuzz/.gitignore new file mode 100644 index 00000000000..c97791ee811 --- /dev/null +++ b/go/consensus/tendermint/fuzz/.gitignore @@ -0,0 +1,5 @@ +corpus/ +crashers/ +suppressions/ +*.zip +gencorpus/gencorpus diff --git a/go/consensus/tendermint/fuzz/fuzz.go b/go/consensus/tendermint/fuzz/fuzz.go new file mode 100644 index 00000000000..2a0e1de6da4 --- /dev/null +++ b/go/consensus/tendermint/fuzz/fuzz.go @@ -0,0 +1,101 @@ +// +build gofuzz + +package fuzz + +import ( + "context" + + "github.com/tendermint/tendermint/abci/types" + + "github.com/oasislabs/oasis-core/go/common/cbor" + "github.com/oasislabs/oasis-core/go/common/crypto/signature" + "github.com/oasislabs/oasis-core/go/common/crypto/signature/signers/memory" + "github.com/oasislabs/oasis-core/go/common/fuzz" + "github.com/oasislabs/oasis-core/go/consensus/api/transaction" + "github.com/oasislabs/oasis-core/go/consensus/tendermint/abci" + "github.com/oasislabs/oasis-core/go/consensus/tendermint/apps/epochtime_mock" + registryApp "github.com/oasislabs/oasis-core/go/consensus/tendermint/apps/registry" + stakingApp "github.com/oasislabs/oasis-core/go/consensus/tendermint/apps/staking" +) + +var ( + txSigner signature.Signer = memory.NewTestSigner("consensus-fuzz") + + // FuzzableApps is a list of ABCI apps the fuzzer can fuzz. + FuzzableApps []abci.Application = []abci.Application{ + epochtimemock.New(), + registryApp.New(), + stakingApp.New(), + } + + // FuzzableMethods is a list of all the apps' transaction methods that are fuzzable. + FuzzableMethods []transaction.MethodName +) + +func init() { + for _, app := range FuzzableApps { + FuzzableMethods = append(FuzzableMethods, app.Methods()...) + } +} + +func Fuzz(data []byte) int { + var err error + + ctx := context.Background() + + var pruneCfg abci.PruneConfig + + appConfig := &abci.ApplicationConfig{ + DataDir: "/tmp/oasis-node-fuzz-consensus", + Pruning: pruneCfg, + HaltEpochHeight: 1000000, + MinGasPrice: 1, + } + + // The muxer will start with the previous state, if it exists (the state database isn't cleared). + muxer, _ := abci.NewMockMux(ctx, appConfig) + defer muxer.MockClose() + + for _, app := range FuzzableApps { + muxer.MockRegisterApp(app) + } + + if len(data) < 2 { + return -1 + } + meth := int(data[0]) + if meth >= len(FuzzableMethods) { + return -1 + } + + methodName := FuzzableMethods[meth] + + blob, _ := fuzz.NewFilledInstance(data, methodName.BodyType()) + + tx := &transaction.Transaction{ + Method: methodName, + Body: cbor.Marshal(blob), + } + + signedTx, err := transaction.Sign(txSigner, tx) + if err != nil { + panic("error signing transaction") + } + + txBlob := cbor.Marshal(&signedTx) + + // Check the transaction. + checkReq := types.RequestCheckTx{ + Tx: txBlob, + Type: types.CheckTxType_New, + } + muxer.CheckTx(checkReq) + + // And try executing it too. + deliverReq := types.RequestDeliverTx{ + Tx: txBlob, + } + muxer.DeliverTx(deliverReq) + + return 0 +} diff --git a/go/consensus/tendermint/fuzz/gencorpus/main.go b/go/consensus/tendermint/fuzz/gencorpus/main.go new file mode 100644 index 00000000000..ed3d86b59be --- /dev/null +++ b/go/consensus/tendermint/fuzz/gencorpus/main.go @@ -0,0 +1,32 @@ +// +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 ( + "fmt" + "io/ioutil" + + commonFuzz "github.com/oasislabs/oasis-core/go/common/fuzz" + appFuzz "github.com/oasislabs/oasis-core/go/consensus/tendermint/fuzz" +) + +const ( + samplesPerMethod int = 20 +) + +func main() { + for meth := 0; meth < len(appFuzz.FuzzableMethods); meth++ { + for i := 0; i < samplesPerMethod; i++ { + methodName := appFuzz.FuzzableMethods[meth] + + fmt.Println("generating sample", i, "for method", methodName) + + actualSample := []byte{byte(meth)} + actualSample = append(actualSample, commonFuzz.MakeSampleBlob(methodName.BodyType())...) + fileName := fmt.Sprintf("%02d_%s_%02d.bin", meth, string(methodName), i) + _ = ioutil.WriteFile(fileName, actualSample, 0644) + } + } +} diff --git a/go/go.mod b/go/go.mod index bd40138ca53..0d92de2fe9f 100644 --- a/go/go.mod +++ b/go/go.mod @@ -21,6 +21,7 @@ require ( github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 // indirect github.com/cznic/strutil v0.0.0-20181122101858-275e90344537 // indirect github.com/dgraph-io/badger/v2 v2.0.0 + github.com/dvyukov/go-fuzz v0.0.0-20191022152526-8cb203812681 // indirect github.com/eapache/channels v1.1.0 github.com/eapache/queue v1.1.0 // indirect github.com/edsrzf/mmap-go v1.0.0 // indirect @@ -28,6 +29,7 @@ require ( github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31 // indirect github.com/go-kit/kit v0.9.0 github.com/golang/protobuf v1.3.2 + github.com/google/gofuzz v1.0.0 github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 github.com/hpcloud/tail v1.0.0 diff --git a/go/go.sum b/go/go.sum index 6a9477ffd10..4a2775ecd69 100644 --- a/go/go.sum +++ b/go/go.sum @@ -86,6 +86,8 @@ github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUn github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dvyukov/go-fuzz v0.0.0-20191022152526-8cb203812681 h1:3WV5aRRj1ELP3RcLlBp/v0WJTuy47OQMkL9GIQq8QEE= +github.com/dvyukov/go-fuzz v0.0.0-20191022152526-8cb203812681/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= github.com/eapache/channels v1.1.0 h1:F1taHcn7/F0i8DYqKXJnyhJcVpp2kgFcNePxXtnyu4k= github.com/eapache/channels v1.1.0/go.mod h1:jMm2qB5Ubtg9zLd+inMZd2/NUvXgzmWXsDaLyQIGfH0= github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= 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) + } + } +}