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/go/Makefile b/go/Makefile index e49f6ef08e7..dde54c1e611 100644 --- a/go/Makefile +++ b/go/Makefile @@ -64,6 +64,24 @@ 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 +consensus/tendermint/fuzz/fuzz-fuzz.zip: .FORCE + @echo "Building consensus fuzzer" + @cd "$$(dirname "$@")"; go-fuzz-build + @cd "$$(dirname "$@")/gencorpus"; env -u GOPATH $(OASIS_GO) build -tags gofuzz + +# Run fuzzing. +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 + # Clean. clean: @$(ECHO) "$(CYAN)*** Cleaning up Go...$(OFF)" @@ -74,3 +92,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..52649fcdb0e --- /dev/null +++ b/go/common/fuzz/fuzz.go @@ -0,0 +1,105 @@ +// +build gofuzz + +// Package fuzz provides some common utilities useful for fuzzing other packages. +package fuzz + +import ( + "encoding/binary" + "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() +} 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=