Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

initial fuzzing harness #153

Merged
merged 13 commits into from
Apr 24, 2020
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
```

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a section at the very end:

If you have not installed the fuzzing tools yet, execute:

go get -u github.com/dvyukov/go-fuzz/go-fuzz github.com/dvyukov/go-fuzz/go-fuzz-build


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"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd slightly prefer to use github.com/urfave/cli to be consistent with the other projects.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that i'm not pulling in a full app/cli framework for these entry points, i'm going to defer on that for now.

)

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