diff --git a/eth/protocols/snap/handler.go b/eth/protocols/snap/handler.go index 178230995..0a1ee2637 100644 --- a/eth/protocols/snap/handler.go +++ b/eth/protocols/snap/handler.go @@ -120,17 +120,17 @@ func MakeProtocols(backend Backend, dnsdisc enode.Iterator) []p2p.Protocol { // When this function terminates, the peer is disconnected. func Handle(backend Backend, peer *Peer) error { for { - if err := handleMessage(backend, peer); err != nil { + if err := HandleMessage(backend, peer); err != nil { peer.Log().Debug("Message handling failed in `snap`", "err", err) return err } } } -// handleMessage is invoked whenever an inbound message is received from a +// HandleMessage is invoked whenever an inbound message is received from a // remote peer on the `snap` protocol. The remote connection is torn down upon // returning any error. -func handleMessage(backend Backend, peer *Peer) error { +func HandleMessage(backend Backend, peer *Peer) error { // Read the next message from the remote peer, and ensure it's fully consumed msg, err := peer.rw.ReadMsg() if err != nil { diff --git a/eth/protocols/snap/peer.go b/eth/protocols/snap/peer.go index fc721c9f9..87a62d2f8 100644 --- a/eth/protocols/snap/peer.go +++ b/eth/protocols/snap/peer.go @@ -46,6 +46,16 @@ func NewPeer(version uint, p *p2p.Peer, rw p2p.MsgReadWriter) *Peer { } } +// NewFakePeer create a fake snap peer without a backing p2p peer, for testing purposes. +func NewFakePeer(version uint, id string, rw p2p.MsgReadWriter) *Peer { + return &Peer{ + id: id, + rw: rw, + version: version, + logger: log.New("peer", id[:8]), + } +} + // ID retrieves the peer's unique identifier. func (p *Peer) ID() string { return p.id diff --git a/oss-fuzz.sh b/oss-fuzz.sh index 9a24f6b17..745a5ba7c 100644 --- a/oss-fuzz.sh +++ b/oss-fuzz.sh @@ -120,5 +120,10 @@ compile_fuzzer tests/fuzzers/bls12381 FuzzCrossG1MultiExp fuzz_cross_g1_multiex compile_fuzzer tests/fuzzers/bls12381 FuzzCrossG2Add fuzz_cross_g2_add compile_fuzzer tests/fuzzers/bls12381 FuzzCrossPairing fuzz_cross_pairing +compile_fuzzer tests/fuzzers/snap FuzzARange fuzz_account_range +compile_fuzzer tests/fuzzers/snap FuzzSRange fuzz_storage_range +compile_fuzzer tests/fuzzers/snap FuzzByteCodes fuzz_byte_codes +compile_fuzzer tests/fuzzers/snap FuzzTrieNodes fuzz_trie_nodes + #TODO: move this to tests/fuzzers, if possible compile_fuzzer crypto/blake2b Fuzz fuzzBlake2b diff --git a/tests/fuzzers/snap/debug/main.go b/tests/fuzzers/snap/debug/main.go new file mode 100644 index 000000000..d0d1b4930 --- /dev/null +++ b/tests/fuzzers/snap/debug/main.go @@ -0,0 +1,39 @@ +// Copyright 2021 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package main + +import ( + "fmt" + "io/ioutil" + "os" + + "github.com/ethereum/go-ethereum/tests/fuzzers/snap" +) + +func main() { + if len(os.Args) != 2 { + fmt.Fprintf(os.Stderr, "Usage: debug \n") + os.Exit(1) + } + crasher := os.Args[1] + data, err := ioutil.ReadFile(crasher) + if err != nil { + fmt.Fprintf(os.Stderr, "error loading crasher %v: %v", crasher, err) + os.Exit(1) + } + snap.FuzzTrieNodes(data) +} diff --git a/tests/fuzzers/snap/fuzz_handler.go b/tests/fuzzers/snap/fuzz_handler.go new file mode 100644 index 000000000..1ae61df29 --- /dev/null +++ b/tests/fuzzers/snap/fuzz_handler.go @@ -0,0 +1,164 @@ +// Copyright 2021 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package snap + +import ( + "bytes" + "encoding/binary" + "fmt" + "math/big" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/eth/protocols/snap" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/p2p/enode" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rlp" + fuzz "github.com/google/gofuzz" +) + +var trieRoot common.Hash + +func getChain() *core.BlockChain { + db := rawdb.NewMemoryDatabase() + ga := make(core.GenesisAlloc, 1000) + var a = make([]byte, 20) + var mkStorage = func(k, v int) (common.Hash, common.Hash) { + var kB = make([]byte, 32) + var vB = make([]byte, 32) + binary.LittleEndian.PutUint64(kB, uint64(k)) + binary.LittleEndian.PutUint64(vB, uint64(v)) + return common.BytesToHash(kB), common.BytesToHash(vB) + } + storage := make(map[common.Hash]common.Hash) + for i := 0; i < 10; i++ { + k, v := mkStorage(i, i) + storage[k] = v + } + for i := 0; i < 1000; i++ { + binary.LittleEndian.PutUint64(a, uint64(i+0xff)) + acc := core.GenesisAccount{Balance: big.NewInt(int64(i))} + if i%2 == 1 { + acc.Storage = storage + } + ga[common.BytesToAddress(a)] = acc + } + gspec := core.Genesis{ + Config: params.TestChainConfig, + Alloc: ga, + } + genesis := gspec.MustCommit(db) + blocks, _ := core.GenerateChain(gspec.Config, genesis, ethash.NewFaker(), db, 2, + func(i int, gen *core.BlockGen) {}) + cacheConf := &core.CacheConfig{ + TrieCleanLimit: 0, + TrieDirtyLimit: 0, + TrieTimeLimit: 5 * time.Minute, + TrieCleanNoPrefetch: true, + TrieCleanRejournal: 0, + SnapshotLimit: 100, + SnapshotWait: true, + } + trieRoot = blocks[len(blocks)-1].Root() + bc, _ := core.NewBlockChain(db, cacheConf, gspec.Config, ethash.NewFaker(), vm.Config{}, nil, nil) + if _, err := bc.InsertChain(blocks); err != nil { + panic(err) + } + return bc +} + +type dummyBackend struct { + chain *core.BlockChain +} + +func (d *dummyBackend) Chain() *core.BlockChain { return d.chain } +func (d *dummyBackend) RunPeer(*snap.Peer, snap.Handler) error { return nil } +func (d *dummyBackend) PeerInfo(enode.ID) interface{} { return "Foo" } +func (d *dummyBackend) Handle(*snap.Peer, snap.Packet) error { return nil } + +type dummyRW struct { + code uint64 + data []byte + writeCount int +} + +func (d *dummyRW) ReadMsg() (p2p.Msg, error) { + return p2p.Msg{ + Code: d.code, + Payload: bytes.NewReader(d.data), + ReceivedAt: time.Now(), + Size: uint32(len(d.data)), + }, nil +} + +func (d *dummyRW) WriteMsg(msg p2p.Msg) error { + d.writeCount++ + return nil +} + +func doFuzz(input []byte, obj interface{}, code int) int { + if len(input) > 1024*4 { + return -1 + } + bc := getChain() + defer bc.Stop() + backend := &dummyBackend{bc} + fuzz.NewFromGoFuzz(input).Fuzz(obj) + var data []byte + switch p := obj.(type) { + case *snap.GetTrieNodesPacket: + p.Root = trieRoot + data, _ = rlp.EncodeToBytes(obj) + default: + data, _ = rlp.EncodeToBytes(obj) + } + cli := &dummyRW{ + code: uint64(code), + data: data, + } + peer := snap.NewFakePeer(65, "gazonk01", cli) + err := snap.HandleMessage(backend, peer) + switch { + case err == nil && cli.writeCount != 1: + panic(fmt.Sprintf("Expected 1 response, got %d", cli.writeCount)) + case err != nil && cli.writeCount != 0: + panic(fmt.Sprintf("Expected 0 response, got %d", cli.writeCount)) + } + return 1 +} + +// To run a fuzzer, do +// $ CGO_ENABLED=0 go-fuzz-build -func FuzzTrieNodes +// $ go-fuzz + +func FuzzARange(input []byte) int { + return doFuzz(input, &snap.GetAccountRangePacket{}, snap.GetAccountRangeMsg) +} +func FuzzSRange(input []byte) int { + return doFuzz(input, &snap.GetStorageRangesPacket{}, snap.GetStorageRangesMsg) +} +func FuzzByteCodes(input []byte) int { + return doFuzz(input, &snap.GetByteCodesPacket{}, snap.GetByteCodesMsg) +} +func FuzzTrieNodes(input []byte) int { + return doFuzz(input, &snap.GetTrieNodesPacket{}, snap.GetTrieNodesMsg) +}