Skip to content

Commit 3c50706

Browse files
authored
fuzzing harness (#153)
Adds a go-fuzz entrypoint for fuzzing datastore transactions for crashes.
1 parent 799f546 commit 3c50706

File tree

11 files changed

+848
-17
lines changed

11 files changed

+848
-17
lines changed

fuzz/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
provider_*.go
2+
*.zip
3+
corpus
4+
crashers
5+
suppressions

fuzz/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
IPFS Datastore Fuzzer
2+
====
3+
4+
The fuzzer provides a [go fuzzer](https://github.com/dvyukov/go-fuzz) interface
5+
to Datastore implementations. This can be used for fuzz testing of these
6+
implementations.
7+
8+
Usage
9+
----
10+
11+
First, configure the datastores to fuzz (from this directory):
12+
```golang
13+
// either run via `go run`
14+
go run ./cmd/generate github.com/ipfs/go-ds-badger
15+
// or `go generate`
16+
DS_PROVIDERS="github.com/ipfs/go-ds-badger" go generate
17+
```
18+
19+
Then, build the fuzzing artifact and fuzz:
20+
```golang
21+
go-fuzz-build
22+
go-fuzz
23+
```
24+
25+
If you don't have `go-fuzz` installed, it can be acquired as:
26+
```
27+
go get -u github.com/dvyukov/go-fuzz/go-fuzz github.com/dvyukov/go-fuzz/go-fuzz-build
28+
```

fuzz/cmd/compare/main.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"io/ioutil"
6+
"os"
7+
8+
ds "github.com/ipfs/go-datastore"
9+
fuzzer "github.com/ipfs/go-datastore/fuzz"
10+
dsq "github.com/ipfs/go-datastore/query"
11+
12+
"github.com/spf13/pflag"
13+
)
14+
15+
var input *string = pflag.StringP("input", "i", "", "file to read input from (stdin used if not specified)")
16+
var db1 *string = pflag.StringP("db1", "d", "badger", "database to fuzz")
17+
var db2 *string = pflag.StringP("db2", "e", "level", "database to fuzz")
18+
var dbFile *string = pflag.StringP("file", "f", "tmp", "where the db instances should live on disk")
19+
var threads *int = pflag.IntP("threads", "t", 1, "concurrent threads")
20+
21+
func main() {
22+
pflag.Parse()
23+
24+
// do one, then the other, then compare state.
25+
26+
fuzzer.Threads = *threads
27+
28+
var dat []byte
29+
var err error
30+
if *input == "" {
31+
dat, err = ioutil.ReadAll(os.Stdin)
32+
} else {
33+
dat, err = ioutil.ReadFile(*input)
34+
}
35+
if err != nil {
36+
fmt.Fprintf(os.Stderr, "Could not read %s: %v\n", *input, err)
37+
return
38+
}
39+
40+
db1loc := *dbFile + "1"
41+
inst1, err := fuzzer.Open(*db1, db1loc, false)
42+
if err != nil {
43+
fmt.Fprintf(os.Stderr, "Could not open db: %v\n", err)
44+
return
45+
}
46+
defer inst1.Cancel()
47+
48+
db2loc := *dbFile + "2"
49+
inst2, err := fuzzer.Open(*db2, db2loc, false)
50+
if err != nil {
51+
inst1.Cancel()
52+
fmt.Fprintf(os.Stderr, "Could not open db: %v\n", err)
53+
return
54+
}
55+
defer inst2.Cancel()
56+
57+
fmt.Printf("Running db1.........")
58+
inst1.Fuzz(dat)
59+
fmt.Printf("done\n")
60+
fmt.Printf("Running db2.........")
61+
inst2.Fuzz(dat)
62+
fmt.Printf("done\n")
63+
64+
fmt.Printf("Checking equality...")
65+
db1 := inst1.DB()
66+
db2 := inst2.DB()
67+
r1, err := db1.Query(dsq.Query{})
68+
if err != nil {
69+
panic(err)
70+
}
71+
72+
for r := range r1.Next() {
73+
if r.Error != nil {
74+
break
75+
}
76+
if r.Entry.Key == "/" {
77+
continue
78+
}
79+
80+
if exist, _ := db2.Has(ds.NewKey(r.Entry.Key)); !exist {
81+
fmt.Fprintf(os.Stderr, "db2 failed to get key %s held by db1\n", r.Entry.Key)
82+
}
83+
}
84+
85+
r2, err := db2.Query(dsq.Query{})
86+
if err != nil {
87+
panic(err)
88+
}
89+
90+
for r := range r2.Next() {
91+
if r.Error != nil {
92+
break
93+
}
94+
if r.Entry.Key == "/" {
95+
continue
96+
}
97+
98+
if exist, _ := db1.Has(ds.NewKey(r.Entry.Key)); !exist {
99+
fmt.Fprintf(os.Stderr, "db1 failed to get key %s held by db2\n", r.Entry.Key)
100+
}
101+
}
102+
103+
fmt.Printf("Done\n")
104+
}

fuzz/cmd/generate/main.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// This file is invoked by `go generate`
2+
package main
3+
4+
import (
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
"strings"
9+
"text/template"
10+
)
11+
12+
// This program generates bindings to fuzz a concrete datastore implementation.
13+
// It can be invoked by running `go generate <implemenation>`
14+
15+
func main() {
16+
providers := os.Args[1:]
17+
18+
if len(providers) == 0 {
19+
providers = strings.Split(os.Getenv("DS_PROVIDERS"), ",")
20+
}
21+
if len(providers) == 0 {
22+
fmt.Fprintf(os.Stderr, "No providers specified to generate. Nothing to do.")
23+
return
24+
}
25+
26+
for _, provider := range providers {
27+
provider = strings.TrimSpace(provider)
28+
if len(provider) == 0 {
29+
continue
30+
}
31+
cmd := exec.Command("go", "get", provider)
32+
err := cmd.Run()
33+
if err != nil {
34+
fmt.Fprintf(os.Stderr, "failed to add dependency for %s: %v\n", provider, err)
35+
os.Exit(1)
36+
}
37+
38+
nameComponents := strings.Split(provider, "/")
39+
name := nameComponents[len(nameComponents)-1]
40+
f, err := os.Create(fmt.Sprintf("provider_%s.go", name))
41+
if err != nil {
42+
fmt.Fprintf(os.Stderr, "failed to create provider file: %v\n", err)
43+
os.Exit(1)
44+
}
45+
defer f.Close()
46+
err = provideTemplate.Execute(f, struct {
47+
Package string
48+
PackageName string
49+
}{
50+
Package: provider,
51+
PackageName: name,
52+
})
53+
if err != nil {
54+
fmt.Fprintf(os.Stderr, "failed to write provider: %v\n", err)
55+
os.Exit(1)
56+
}
57+
}
58+
}
59+
60+
var provideTemplate = template.Must(template.New("").Parse(`// Code generated by go generate; DO NOT EDIT.
61+
62+
package fuzzer
63+
import prov "{{ .Package }}"
64+
import ds "github.com/ipfs/go-datastore"
65+
66+
func init() {
67+
AddOpener("{{ .PackageName }}", func(loc string) ds.TxnDatastore {
68+
d, err := prov.NewDatastore(loc, nil)
69+
if err != nil {
70+
panic("could not create db instance")
71+
}
72+
return d
73+
})
74+
}
75+
`))

fuzz/cmd/isprefix/main.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package main
2+
3+
// Checks if a db instance is equivalent to some prefix of an input script.
4+
5+
import (
6+
"fmt"
7+
"io/ioutil"
8+
"os"
9+
10+
ds "github.com/ipfs/go-datastore"
11+
fuzzer "github.com/ipfs/go-datastore/fuzz"
12+
dsq "github.com/ipfs/go-datastore/query"
13+
14+
"github.com/spf13/pflag"
15+
)
16+
17+
var input *string = pflag.StringP("input", "i", "", "file to read input from (stdin used if not specified)")
18+
var db *string = pflag.StringP("db", "d", "go-ds-badger", "database driver")
19+
var dbPrev *string = pflag.StringP("exist", "e", "tmp1", "database instance already made")
20+
var dbFile *string = pflag.StringP("file", "f", "tmp2", "where the replay should live")
21+
var threads *int = pflag.IntP("threads", "t", 1, "concurrent threads")
22+
23+
type validatingReader struct {
24+
b []byte
25+
i int
26+
validator func(bool) bool
27+
validI int
28+
}
29+
30+
func (v *validatingReader) Read(buf []byte) (n int, err error) {
31+
if v.i == len(v.b) {
32+
return 0, nil
33+
}
34+
if v.validator(false) {
35+
v.validI = v.i
36+
}
37+
buf[0] = v.b[v.i]
38+
v.i++
39+
return 1, nil
40+
}
41+
42+
func main() {
43+
pflag.Parse()
44+
45+
fuzzer.Threads = *threads
46+
47+
var dat []byte
48+
var err error
49+
if *input == "" {
50+
dat, err = ioutil.ReadAll(os.Stdin)
51+
} else {
52+
dat, err = ioutil.ReadFile(*input)
53+
}
54+
if err != nil {
55+
fmt.Fprintf(os.Stderr, "could not read %s: %v\n", *input, err)
56+
return
57+
}
58+
59+
previousDB, err := fuzzer.Open(*db, *dbPrev, false)
60+
if err != nil {
61+
fmt.Fprintf(os.Stderr, "could not open: %v\n", err)
62+
return
63+
}
64+
defer previousDB.Cancel()
65+
66+
replayDB, err := fuzzer.Open(*db, *dbFile, true)
67+
if err != nil {
68+
fmt.Fprintf(os.Stderr, "could not open: %v\n", err)
69+
return
70+
}
71+
defer replayDB.Cancel()
72+
73+
reader := validatingReader{dat, 0, func(verbose bool) bool {
74+
res, _ := replayDB.DB().Query(dsq.Query{})
75+
for e := range res.Next() {
76+
if e.Entry.Key == "/" {
77+
continue
78+
}
79+
if h, _ := previousDB.DB().Has(ds.NewKey(e.Entry.Key)); !h {
80+
if verbose {
81+
fmt.Printf("failed - script run db has %s not in existing.\n", e.Entry.Key)
82+
}
83+
return false // not yet complete
84+
}
85+
}
86+
// next; make sure the other way is equal.
87+
res, _ = previousDB.DB().Query(dsq.Query{})
88+
for e := range res.Next() {
89+
if e.Entry.Key == "/" {
90+
continue
91+
}
92+
if h, _ := replayDB.DB().Has(ds.NewKey(e.Entry.Key)); !h {
93+
if verbose {
94+
fmt.Printf("failed - existing db has %s not in replay.\n", e.Entry.Key)
95+
}
96+
return false
97+
}
98+
}
99+
// db images are the same.
100+
return true
101+
}, -1}
102+
103+
replayDB.FuzzStream(&reader)
104+
if reader.validator(true) {
105+
reader.validI = reader.i
106+
}
107+
108+
if reader.validI > -1 {
109+
fmt.Printf("Matched at stream position %d.\n", reader.validI)
110+
os.Exit(0)
111+
} else {
112+
fmt.Printf("Failed to match\n")
113+
os.Exit(1)
114+
}
115+
}

fuzz/cmd/run/main.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"io/ioutil"
7+
"os"
8+
9+
fuzzer "github.com/ipfs/go-datastore/fuzz"
10+
11+
"github.com/spf13/pflag"
12+
)
13+
14+
var input *string = pflag.StringP("input", "i", "", "file to read input from (stdin used if not specified)")
15+
var db *string = pflag.StringP("database", "d", "go-ds-badger", "database to fuzz")
16+
var dbFile *string = pflag.StringP("file", "f", "tmp", "where the db instace should live on disk")
17+
var threads *int = pflag.IntP("threads", "t", 1, "concurrent threads")
18+
19+
func main() {
20+
pflag.Parse()
21+
22+
fuzzer.Threads = *threads
23+
24+
if *input != "" {
25+
dat, err := ioutil.ReadFile(*input)
26+
if err != nil {
27+
fmt.Fprintf(os.Stderr, "could not read %s: %v\n", *input, err)
28+
os.Exit(1)
29+
}
30+
ret := fuzzer.FuzzDB(*db, *dbFile, false, dat)
31+
os.Exit(ret)
32+
} else {
33+
reader := bufio.NewReader(os.Stdin)
34+
err := fuzzer.FuzzStream(*db, *dbFile, false, reader)
35+
if err != nil {
36+
fmt.Fprintf(os.Stderr, "Error fuzzing: %v\n", err)
37+
os.Exit(1)
38+
}
39+
return
40+
}
41+
}

0 commit comments

Comments
 (0)