-
Notifications
You must be signed in to change notification settings - Fork 65
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
Changes from all commits
5c2309b
5da85dd
3578d51
53074b5
2fafe54
5697c8d
7fa0f19
c333b9d
d2ec4b2
d3093d8
88d69c2
0442b81
ed90f4a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
provider_*.go | ||
*.zip | ||
corpus | ||
crashers | ||
suppressions |
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 | ||
``` |
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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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") | ||
} |
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 | ||
}) | ||
} | ||
`)) |
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) | ||
} | ||
} |
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 | ||
} | ||
} |
There was a problem hiding this comment.
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: