test/fuzz: convert to Go 1.18 native fuzzing (#8359)

This commit is contained in:
elias-orijtech
2022-04-19 20:49:51 +02:00
committed by GitHub
parent ad249ca178
commit e4991fd862
33 changed files with 142 additions and 322 deletions

View File

@@ -1,29 +0,0 @@
#!/usr/bin/make -f
.PHONY: fuzz-mempool
fuzz-mempool:
cd mempool && \
rm -f *-fuzz.zip && \
go-fuzz-build && \
go-fuzz
.PHONY: fuzz-p2p-sc
fuzz-p2p-sc:
cd p2p/secretconnection && \
rm -f *-fuzz.zip && \
go run ./init-corpus/main.go && \
go-fuzz-build && \
go-fuzz
.PHONY: fuzz-rpc-server
fuzz-rpc-server:
cd rpc/jsonrpc/server && \
rm -f *-fuzz.zip && \
go-fuzz-build && \
go-fuzz
clean:
find . -name corpus -type d -exec rm -rf {} +;
find . -name crashers -type d -exec rm -rf {} +;
find . -name suppressions -type d -exec rm -rf {} +;
find . -name *\.zip -type f -delete

View File

@@ -5,68 +5,18 @@ Fuzzing for various packages in Tendermint using [go-fuzz](https://github.com/dv
Inputs:
- mempool `CheckTx` (using kvstore in-process ABCI app)
- p2p `Addrbook#AddAddress`
- p2p `pex.Reactor#Receive`
- p2p `SecretConnection#Read` and `SecretConnection#Write`
- rpc jsonrpc server
## Directory structure
```
| test
| |- corpus/
| |- crashers/
| |- init-corpus/
| |- suppressions/
| |- testdata/
| |- <testname>.go
```
`/corpus` directory contains corpus data. The idea is to help the fuzzier to
understand what bytes sequences are semantically valid (e.g. if we're testing
PNG decoder, then we would put black-white PNG into corpus directory; with
blockchain reactor - we would put blockchain messages into corpus).
`/init-corpus` (if present) contains a script for generating corpus data.
`/testdata` directory may contain an additional data (like `addrbook.json`).
Upon running the fuzzier, `/crashers` and `/suppressions` dirs will be created,
along with <testname>.zip archive. `/crashers` will show any inputs, which have
lead to panics (plus a trace). `/suppressions` will show any suppressed inputs.
## Running
```sh
make fuzz-mempool
make fuzz-p2p-addrbook
make fuzz-p2p-pex
make fuzz-p2p-sc
make fuzz-rpc-server
```
Each command will create corpus data (if needed), generate a fuzz archive and
call `go-fuzz` executable.
Then watch out for the respective outputs in the fuzzer output to announce new
crashers which can be found in the directory `crashers`.
For example if we find
The fuzz tests are in native Go fuzzing format. Use the `go`
tool to run them:
```sh
ls crashers/
61bde465f47c93254d64d643c3b2480e0a54666e
61bde465f47c93254d64d643c3b2480e0a54666e.output
61bde465f47c93254d64d643c3b2480e0a54666e.quoted
da39a3ee5e6b4b0d3255bfef95601890afd80709
da39a3ee5e6b4b0d3255bfef95601890afd80709.output
da39a3ee5e6b4b0d3255bfef95601890afd80709.quoted
go test -fuzz Mempool ./tests
go test -fuzz P2PSecretConnection ./tests
go test -fuzz RPCJSONRPCServer ./tests
```
the crashing bytes generated by the fuzzer will be in
`61bde465f47c93254d64d643c3b2480e0a54666e` the respective crash report in
`61bde465f47c93254d64d643c3b2480e0a54666e.output`
and the bug report can be created by retrieving the bytes in
`61bde465f47c93254d64d643c3b2480e0a54666e` and feeding those back into the
`Fuzz` function.
See [the Go Fuzzing introduction](https://go.dev/doc/fuzz/) for more information.

View File

@@ -1,34 +0,0 @@
package mempool_test
import (
"io"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
mempool "github.com/tendermint/tendermint/test/fuzz/mempool"
)
const testdataCasesDir = "testdata/cases"
func TestMempoolTestdataCases(t *testing.T) {
entries, err := os.ReadDir(testdataCasesDir)
require.NoError(t, err)
for _, e := range entries {
entry := e
t.Run(entry.Name(), func(t *testing.T) {
defer func() {
r := recover()
require.Nilf(t, r, "testdata/cases test panic")
}()
f, err := os.Open(filepath.Join(testdataCasesDir, entry.Name()))
require.NoError(t, err)
input, err := io.ReadAll(f)
require.NoError(t, err)
mempool.Fuzz(input)
})
}
}

View File

@@ -1,10 +1,22 @@
#!/bin/bash -eu
#!/bin/bash
set -euo pipefail
export FUZZ_ROOT="github.com/tendermint/tendermint"
(cd test/fuzz/p2p/secretconnection; go run ./init-corpus/main.go)
compile_go_fuzzer "$FUZZ_ROOT"/test/fuzz/p2p/secretconnection Fuzz fuzz_p2p_secretconnection fuzz
build_go_fuzzer() {
local function="$1"
local fuzzer="$2"
compile_go_fuzzer "$FUZZ_ROOT"/test/fuzz/mempool Fuzz fuzz_mempool fuzz
gotip run github.com/orijtech/otils/corpus2ossfuzz@latest -o "$OUT"/"$fuzzer"_seed_corpus.zip -corpus test/fuzz/tests/testdata/fuzz/"$function"
compile_native_go_fuzzer "$FUZZ_ROOT"/test/fuzz/tests "$function" "$fuzzer"
}
compile_go_fuzzer "$FUZZ_ROOT"/test/fuzz/rpc/jsonrpc/server Fuzz fuzz_rpc_jsonrpc_server fuzz
gotip get github.com/AdamKorcz/go-118-fuzz-build/utils
gotip get github.com/prometheus/common/expfmt@v0.32.1
build_go_fuzzer FuzzP2PSecretConnection fuzz_p2p_secretconnection
build_go_fuzzer FuzzMempool fuzz_mempool
build_go_fuzzer FuzzRPCJSONRPCServer fuzz_rpc_jsonrpc_server

View File

@@ -1,34 +0,0 @@
package secretconnection_test
import (
"io"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/test/fuzz/p2p/secretconnection"
)
const testdataCasesDir = "testdata/cases"
func TestSecretConnectionTestdataCases(t *testing.T) {
entries, err := os.ReadDir(testdataCasesDir)
require.NoError(t, err)
for _, e := range entries {
entry := e
t.Run(entry.Name(), func(t *testing.T) {
defer func() {
r := recover()
require.Nilf(t, r, "testdata/cases test panic")
}()
f, err := os.Open(filepath.Join(testdataCasesDir, entry.Name()))
require.NoError(t, err)
input, err := io.ReadAll(f)
require.NoError(t, err)
secretconnection.Fuzz(input)
})
}
}

View File

@@ -1,47 +0,0 @@
// nolint: gosec
package main
import (
"flag"
"fmt"
"log"
"os"
"path/filepath"
)
func main() {
baseDir := flag.String("base", ".", `where the "corpus" directory will live`)
flag.Parse()
initCorpus(*baseDir)
}
func initCorpus(baseDir string) {
log.SetFlags(0)
corpusDir := filepath.Join(baseDir, "corpus")
if err := os.MkdirAll(corpusDir, 0755); err != nil {
log.Fatal(err)
}
data := []string{
"dadc04c2-cfb1-4aa9-a92a-c0bf780ec8b6",
"",
" ",
" a ",
`{"a": 12, "tsp": 999, k: "blue"}`,
`9999.999`,
`""`,
`Tendermint fuzzing`,
}
for i, datum := range data {
filename := filepath.Join(corpusDir, fmt.Sprintf("%d", i))
if err := os.WriteFile(filename, []byte(datum), 0644); err != nil {
log.Fatalf("can't write %v to %q: %v", datum, filename, err)
}
log.Printf("wrote %q", filename)
}
}

View File

@@ -1,34 +0,0 @@
package server_test
import (
"io"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/test/fuzz/rpc/jsonrpc/server"
)
const testdataCasesDir = "testdata/cases"
func TestServerTestdataCases(t *testing.T) {
entries, err := os.ReadDir(testdataCasesDir)
require.NoError(t, err)
for _, e := range entries {
entry := e
t.Run(entry.Name(), func(t *testing.T) {
defer func() {
r := recover()
require.Nilf(t, r, "testdata/cases test panic")
}()
f, err := os.Open(filepath.Join(testdataCasesDir, entry.Name()))
require.NoError(t, err)
input, err := io.ReadAll(f)
require.NoError(t, err)
server.Fuzz(input)
})
}
}

View File

@@ -1,65 +0,0 @@
package server
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"github.com/tendermint/tendermint/libs/log"
rs "github.com/tendermint/tendermint/rpc/jsonrpc/server"
"github.com/tendermint/tendermint/rpc/jsonrpc/types"
)
var rpcFuncMap = map[string]*rs.RPCFunc{
"c": rs.NewRPCFunc(func(ctx context.Context, s string, i int) (string, error) {
return "foo", nil
}, "s", "i"),
}
var mux *http.ServeMux
func init() {
mux = http.NewServeMux()
rs.RegisterRPCFuncs(mux, rpcFuncMap, log.NewNopLogger())
}
func Fuzz(data []byte) int {
if len(data) == 0 {
return -1
}
req, _ := http.NewRequest("POST", "http://localhost/", bytes.NewReader(data))
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
res := rec.Result()
blob, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
if err := res.Body.Close(); err != nil {
panic(err)
}
if len(blob) == 0 {
return 1
}
if outputJSONIsSlice(blob) {
recv := []types.RPCResponse{}
if err := json.Unmarshal(blob, &recv); err != nil {
panic(err)
}
return 1
}
recv := &types.RPCResponse{}
if err := json.Unmarshal(blob, recv); err != nil {
panic(err)
}
return 1
}
func outputJSONIsSlice(input []byte) bool {
slice := []interface{}{}
return json.Unmarshal(input, &slice) == nil
}

View File

@@ -1,7 +1,10 @@
package mempool
//go:build gofuzz || go1.18
package tests
import (
"context"
"testing"
abciclient "github.com/tendermint/tendermint/abci/client"
"github.com/tendermint/tendermint/abci/example/kvstore"
@@ -33,11 +36,11 @@ func init() {
}
}
func Fuzz(data []byte) int {
err := getMp().CheckTx(context.Background(), data, nil, mempool.TxInfo{})
if err != nil {
return 0
}
return 1
func FuzzMempool(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
err := getMp().CheckTx(context.Background(), data, nil, mempool.TxInfo{})
if err != nil {
panic(err)
}
})
}

View File

@@ -1,19 +1,28 @@
package secretconnection
//go:build gofuzz || go1.18
package tests
import (
"bytes"
"fmt"
"io"
"log"
"testing"
"github.com/tendermint/tendermint/crypto/ed25519"
"github.com/tendermint/tendermint/internal/libs/async"
sc "github.com/tendermint/tendermint/internal/p2p/conn"
)
func Fuzz(data []byte) int {
func FuzzP2PSecretConnection(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
fuzz(data)
})
}
func fuzz(data []byte) {
if len(data) == 0 {
return -1
return
}
fooConn, barConn := makeSecretConnPair()
@@ -50,8 +59,6 @@ func Fuzz(data []byte) int {
if !bytes.Equal(data, dataRead) {
panic("bytes written != read")
}
return 1
}
type kvstoreConn struct {

View File

@@ -0,0 +1,68 @@
//go:build gofuzz || go1.18
package tests
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/tendermint/tendermint/libs/log"
rpcserver "github.com/tendermint/tendermint/rpc/jsonrpc/server"
"github.com/tendermint/tendermint/rpc/jsonrpc/types"
)
func FuzzRPCJSONRPCServer(f *testing.F) {
var rpcFuncMap = map[string]*rpcserver.RPCFunc{
"c": rpcserver.NewRPCFunc(func(ctx context.Context, s string, i int) (string, error) {
return "foo", nil
}, "s", "i"),
}
mux := http.NewServeMux()
rpcserver.RegisterRPCFuncs(mux, rpcFuncMap, log.NewNopLogger())
f.Fuzz(func(t *testing.T, data []byte) {
if len(data) == 0 {
return
}
req, err := http.NewRequest("POST", "http://localhost/", bytes.NewReader(data))
if err != nil {
panic(err)
}
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
res := rec.Result()
blob, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
if err := res.Body.Close(); err != nil {
panic(err)
}
if len(blob) == 0 {
return
}
if outputJSONIsSlice(blob) {
var recv []types.RPCResponse
if err := json.Unmarshal(blob, &recv); err != nil {
panic(err)
}
return
}
var recv types.RPCResponse
if err := json.Unmarshal(blob, &recv); err != nil {
panic(err)
}
})
}
func outputJSONIsSlice(input []byte) bool {
var slice []json.RawMessage
return json.Unmarshal(input, &slice) == nil
}

View File

@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("S1")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("0")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
[]byte(" ")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("{\"a\": 12, \"tsp\": 999, k: \"blue\"}")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("\"\"")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("9999.999")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
[]byte(" a ")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("Tendermint fuzzing")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("[{\"iD\":7},{\"iD\":7}]")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("[0,0]")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("[0]")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("")