Skip to content

Commit

Permalink
fuzzing harness (#153)
Browse files Browse the repository at this point in the history
Adds a go-fuzz entrypoint for fuzzing datastore transactions for crashes.
  • Loading branch information
willscott authored Apr 24, 2020
1 parent 799f546 commit 3c50706
Show file tree
Hide file tree
Showing 11 changed files with 848 additions and 17 deletions.
5 changes: 5 additions & 0 deletions fuzz/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
provider_*.go
*.zip
corpus
crashers
suppressions
28 changes: 28 additions & 0 deletions fuzz/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
IPFS Datastore Fuzzer
====

The fuzzer provides a [go fuzzer](https://github.com/dvyukov/go-fuzz) interface
to Datastore implementations. This can be used for fuzz testing of these
implementations.

Usage
----

First, configure the datastores to fuzz (from this directory):
```golang
// either run via `go run`
go run ./cmd/generate github.com/ipfs/go-ds-badger
// or `go generate`
DS_PROVIDERS="github.com/ipfs/go-ds-badger" go generate
```

Then, build the fuzzing artifact and fuzz:
```golang
go-fuzz-build
go-fuzz
```

If you don't have `go-fuzz` installed, it can be acquired as:
```
go get -u github.com/dvyukov/go-fuzz/go-fuzz github.com/dvyukov/go-fuzz/go-fuzz-build
```
104 changes: 104 additions & 0 deletions fuzz/cmd/compare/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package main

import (
"fmt"
"io/ioutil"
"os"

ds "github.com/ipfs/go-datastore"
fuzzer "github.com/ipfs/go-datastore/fuzz"
dsq "github.com/ipfs/go-datastore/query"

"github.com/spf13/pflag"
)

var input *string = pflag.StringP("input", "i", "", "file to read input from (stdin used if not specified)")
var db1 *string = pflag.StringP("db1", "d", "badger", "database to fuzz")
var db2 *string = pflag.StringP("db2", "e", "level", "database to fuzz")
var dbFile *string = pflag.StringP("file", "f", "tmp", "where the db instances should live on disk")
var threads *int = pflag.IntP("threads", "t", 1, "concurrent threads")

func main() {
pflag.Parse()

// do one, then the other, then compare state.

fuzzer.Threads = *threads

var dat []byte
var err error
if *input == "" {
dat, err = ioutil.ReadAll(os.Stdin)
} else {
dat, err = ioutil.ReadFile(*input)
}
if err != nil {
fmt.Fprintf(os.Stderr, "Could not read %s: %v\n", *input, err)
return
}

db1loc := *dbFile + "1"
inst1, err := fuzzer.Open(*db1, db1loc, false)
if err != nil {
fmt.Fprintf(os.Stderr, "Could not open db: %v\n", err)
return
}
defer inst1.Cancel()

db2loc := *dbFile + "2"
inst2, err := fuzzer.Open(*db2, db2loc, false)
if err != nil {
inst1.Cancel()
fmt.Fprintf(os.Stderr, "Could not open db: %v\n", err)
return
}
defer inst2.Cancel()

fmt.Printf("Running db1.........")
inst1.Fuzz(dat)
fmt.Printf("done\n")
fmt.Printf("Running db2.........")
inst2.Fuzz(dat)
fmt.Printf("done\n")

fmt.Printf("Checking equality...")
db1 := inst1.DB()
db2 := inst2.DB()
r1, err := db1.Query(dsq.Query{})
if err != nil {
panic(err)
}

for r := range r1.Next() {
if r.Error != nil {
break
}
if r.Entry.Key == "/" {
continue
}

if exist, _ := db2.Has(ds.NewKey(r.Entry.Key)); !exist {
fmt.Fprintf(os.Stderr, "db2 failed to get key %s held by db1\n", r.Entry.Key)
}
}

r2, err := db2.Query(dsq.Query{})
if err != nil {
panic(err)
}

for r := range r2.Next() {
if r.Error != nil {
break
}
if r.Entry.Key == "/" {
continue
}

if exist, _ := db1.Has(ds.NewKey(r.Entry.Key)); !exist {
fmt.Fprintf(os.Stderr, "db1 failed to get key %s held by db2\n", r.Entry.Key)
}
}

fmt.Printf("Done\n")
}
75 changes: 75 additions & 0 deletions fuzz/cmd/generate/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// This file is invoked by `go generate`
package main

import (
"fmt"
"os"
"os/exec"
"strings"
"text/template"
)

// This program generates bindings to fuzz a concrete datastore implementation.
// It can be invoked by running `go generate <implemenation>`

func main() {
providers := os.Args[1:]

if len(providers) == 0 {
providers = strings.Split(os.Getenv("DS_PROVIDERS"), ",")
}
if len(providers) == 0 {
fmt.Fprintf(os.Stderr, "No providers specified to generate. Nothing to do.")
return
}

for _, provider := range providers {
provider = strings.TrimSpace(provider)
if len(provider) == 0 {
continue
}
cmd := exec.Command("go", "get", provider)
err := cmd.Run()
if err != nil {
fmt.Fprintf(os.Stderr, "failed to add dependency for %s: %v\n", provider, err)
os.Exit(1)
}

nameComponents := strings.Split(provider, "/")
name := nameComponents[len(nameComponents)-1]
f, err := os.Create(fmt.Sprintf("provider_%s.go", name))
if err != nil {
fmt.Fprintf(os.Stderr, "failed to create provider file: %v\n", err)
os.Exit(1)
}
defer f.Close()
err = provideTemplate.Execute(f, struct {
Package string
PackageName string
}{
Package: provider,
PackageName: name,
})
if err != nil {
fmt.Fprintf(os.Stderr, "failed to write provider: %v\n", err)
os.Exit(1)
}
}
}

var provideTemplate = template.Must(template.New("").Parse(`// Code generated by go generate; DO NOT EDIT.
package fuzzer
import prov "{{ .Package }}"
import ds "github.com/ipfs/go-datastore"
func init() {
AddOpener("{{ .PackageName }}", func(loc string) ds.TxnDatastore {
d, err := prov.NewDatastore(loc, nil)
if err != nil {
panic("could not create db instance")
}
return d
})
}
`))
115 changes: 115 additions & 0 deletions fuzz/cmd/isprefix/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package main

// Checks if a db instance is equivalent to some prefix of an input script.

import (
"fmt"
"io/ioutil"
"os"

ds "github.com/ipfs/go-datastore"
fuzzer "github.com/ipfs/go-datastore/fuzz"
dsq "github.com/ipfs/go-datastore/query"

"github.com/spf13/pflag"
)

var input *string = pflag.StringP("input", "i", "", "file to read input from (stdin used if not specified)")
var db *string = pflag.StringP("db", "d", "go-ds-badger", "database driver")
var dbPrev *string = pflag.StringP("exist", "e", "tmp1", "database instance already made")
var dbFile *string = pflag.StringP("file", "f", "tmp2", "where the replay should live")
var threads *int = pflag.IntP("threads", "t", 1, "concurrent threads")

type validatingReader struct {
b []byte
i int
validator func(bool) bool
validI int
}

func (v *validatingReader) Read(buf []byte) (n int, err error) {
if v.i == len(v.b) {
return 0, nil
}
if v.validator(false) {
v.validI = v.i
}
buf[0] = v.b[v.i]
v.i++
return 1, nil
}

func main() {
pflag.Parse()

fuzzer.Threads = *threads

var dat []byte
var err error
if *input == "" {
dat, err = ioutil.ReadAll(os.Stdin)
} else {
dat, err = ioutil.ReadFile(*input)
}
if err != nil {
fmt.Fprintf(os.Stderr, "could not read %s: %v\n", *input, err)
return
}

previousDB, err := fuzzer.Open(*db, *dbPrev, false)
if err != nil {
fmt.Fprintf(os.Stderr, "could not open: %v\n", err)
return
}
defer previousDB.Cancel()

replayDB, err := fuzzer.Open(*db, *dbFile, true)
if err != nil {
fmt.Fprintf(os.Stderr, "could not open: %v\n", err)
return
}
defer replayDB.Cancel()

reader := validatingReader{dat, 0, func(verbose bool) bool {
res, _ := replayDB.DB().Query(dsq.Query{})
for e := range res.Next() {
if e.Entry.Key == "/" {
continue
}
if h, _ := previousDB.DB().Has(ds.NewKey(e.Entry.Key)); !h {
if verbose {
fmt.Printf("failed - script run db has %s not in existing.\n", e.Entry.Key)
}
return false // not yet complete
}
}
// next; make sure the other way is equal.
res, _ = previousDB.DB().Query(dsq.Query{})
for e := range res.Next() {
if e.Entry.Key == "/" {
continue
}
if h, _ := replayDB.DB().Has(ds.NewKey(e.Entry.Key)); !h {
if verbose {
fmt.Printf("failed - existing db has %s not in replay.\n", e.Entry.Key)
}
return false
}
}
// db images are the same.
return true
}, -1}

replayDB.FuzzStream(&reader)
if reader.validator(true) {
reader.validI = reader.i
}

if reader.validI > -1 {
fmt.Printf("Matched at stream position %d.\n", reader.validI)
os.Exit(0)
} else {
fmt.Printf("Failed to match\n")
os.Exit(1)
}
}
41 changes: 41 additions & 0 deletions fuzz/cmd/run/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package main

import (
"bufio"
"fmt"
"io/ioutil"
"os"

fuzzer "github.com/ipfs/go-datastore/fuzz"

"github.com/spf13/pflag"
)

var input *string = pflag.StringP("input", "i", "", "file to read input from (stdin used if not specified)")
var db *string = pflag.StringP("database", "d", "go-ds-badger", "database to fuzz")
var dbFile *string = pflag.StringP("file", "f", "tmp", "where the db instace should live on disk")
var threads *int = pflag.IntP("threads", "t", 1, "concurrent threads")

func main() {
pflag.Parse()

fuzzer.Threads = *threads

if *input != "" {
dat, err := ioutil.ReadFile(*input)
if err != nil {
fmt.Fprintf(os.Stderr, "could not read %s: %v\n", *input, err)
os.Exit(1)
}
ret := fuzzer.FuzzDB(*db, *dbFile, false, dat)
os.Exit(ret)
} else {
reader := bufio.NewReader(os.Stdin)
err := fuzzer.FuzzStream(*db, *dbFile, false, reader)
if err != nil {
fmt.Fprintf(os.Stderr, "Error fuzzing: %v\n", err)
os.Exit(1)
}
return
}
}
Loading

0 comments on commit 3c50706

Please sign in to comment.