Skip to content

Commit db5f2dd

Browse files
authored
Add a fuzz tester. (#31)
1 parent 9902db1 commit db5f2dd

File tree

13 files changed

+592
-0
lines changed

13 files changed

+592
-0
lines changed

amt.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,3 +353,15 @@ func (r *Root) Flush(ctx context.Context) (cid.Cid, error) {
353353
func (r *Root) Len() uint64 {
354354
return r.count
355355
}
356+
357+
func (r *Root) Clone() *Root {
358+
return &Root{
359+
bitWidth: r.bitWidth,
360+
height: r.height,
361+
count: r.count,
362+
363+
node: r.node.clone(),
364+
365+
store: r.store,
366+
}
367+
}

fuzz/.gitignore

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

fuzz/Makefile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
fuzz: fuzzer-fuzz.zip
2+
go run github.com/dvyukov/go-fuzz/go-fuzz
3+
.PHONY: fuzz
4+
5+
fuzzer-fuzz.zip:
6+
go run github.com/dvyukov/go-fuzz/go-fuzz-build
7+
8+
clean:
9+
rm -rf fuzzer-fuzz.zip crashers corpus suppressions

fuzz/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Fuzzer to validate the AMT
2+
3+
To fuzz, run `make`.

fuzz/checked_amt.go

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
package fuzzer
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"math/rand"
7+
8+
cbor "github.com/ipfs/go-ipld-cbor"
9+
cbg "github.com/whyrusleeping/cbor-gen"
10+
11+
"github.com/filecoin-project/go-amt-ipld/v4"
12+
)
13+
14+
type checkedAMT struct {
15+
amt *amt.Root
16+
step uint64
17+
bs cbor.IpldStore
18+
19+
array map[uint64]cbg.CborInt
20+
keyCache []uint64
21+
seen map[uint64]struct{}
22+
}
23+
24+
func newCheckedAMT() (*checkedAMT, error) {
25+
bs := cbor.NewCborStore(newMockBlocks())
26+
root, err := amt.NewAMT(bs)
27+
if err != nil {
28+
return nil, err
29+
}
30+
return &checkedAMT{
31+
amt: root,
32+
bs: bs,
33+
array: make(map[uint64]cbg.CborInt),
34+
seen: make(map[uint64]struct{}),
35+
}, nil
36+
}
37+
38+
func (c *checkedAMT) randKey(key uint64) uint64 {
39+
if len(c.keyCache) == 0 {
40+
return key
41+
}
42+
return c.keyCache[key%uint64(len(c.keyCache))]
43+
}
44+
45+
func (c *checkedAMT) cache(key uint64) {
46+
if _, ok := c.seen[key]; !ok {
47+
c.seen[key] = struct{}{}
48+
c.keyCache = append(c.keyCache, key)
49+
}
50+
}
51+
52+
func (c *checkedAMT) setSeen(key uint64, value cbg.CborInt) {
53+
c.set(c.randKey(key), value)
54+
}
55+
56+
func (c *checkedAMT) getSeen(key uint64) {
57+
c.get(c.randKey(key))
58+
}
59+
60+
func (c *checkedAMT) deleteSeen(key uint64) {
61+
c.delete(c.randKey(key))
62+
}
63+
64+
func (c *checkedAMT) set(key uint64, value cbg.CborInt) {
65+
c.trace("set %d to %d", key, value)
66+
c.array[key] = value
67+
c.checkErr(c.amt.Set(context.Background(), key, &value))
68+
c.cache(key)
69+
}
70+
71+
func (c *checkedAMT) get(key uint64) {
72+
c.trace("get %d", key)
73+
expected, hasValue := c.array[key]
74+
var actual cbg.CborInt
75+
found, err := c.amt.Get(context.Background(), key, &actual)
76+
c.checkErr(err)
77+
if hasValue != found {
78+
if found {
79+
c.fail("did not expect to find %d", key)
80+
} else {
81+
c.fail("expected to find %d", key)
82+
}
83+
}
84+
if found {
85+
c.checkEq(expected, actual)
86+
}
87+
c.cache(key)
88+
}
89+
90+
func (c *checkedAMT) delete(key uint64) {
91+
c.trace("delete %d", key)
92+
_, hasValue := c.array[key]
93+
delete(c.array, key)
94+
found, err := c.amt.Delete(context.Background(), key)
95+
c.checkErr(err)
96+
if hasValue != found {
97+
if found {
98+
c.fail("did not expect to find %d", key)
99+
} else {
100+
c.fail("expected to find %d", key)
101+
}
102+
}
103+
c.cache(key)
104+
}
105+
106+
func (c *checkedAMT) flush() {
107+
c.trace("flush")
108+
c1, err := c.amt.Flush(context.Background())
109+
c.checkErr(err)
110+
c2, err := c.amt.Flush(context.Background())
111+
c.checkErr(err)
112+
if c1 != c2 {
113+
c.fail("cids don't match %s != %s", c1, c2)
114+
}
115+
// Don't check the amt itself here, we'll check that at the end.
116+
}
117+
118+
func (c *checkedAMT) reload() {
119+
c.trace("reload")
120+
cid, err := c.amt.Flush(context.Background())
121+
c.checkErr(err)
122+
c.amt, err = amt.LoadAMT(context.Background(), c.bs, cid)
123+
c.checkErr(err)
124+
// Don't check the amt itself here, we'll check that at the end.
125+
}
126+
127+
func (c *checkedAMT) trace(msg string, args ...interface{}) {
128+
c.step++
129+
if Debug {
130+
fmt.Printf("step %d: "+msg+"\n", append([]interface{}{c.step}, args...)...)
131+
}
132+
}
133+
134+
func (c *checkedAMT) check() {
135+
// Check in-memory state
136+
c.checkByIter(c.amt.Clone())
137+
c.checkByGet(c.amt.Clone())
138+
139+
root, err := c.amt.Clone().Flush(context.Background())
140+
c.checkErr(err)
141+
142+
// Now try loading
143+
{
144+
// Check by iterating
145+
array, err := amt.LoadAMT(context.Background(), c.bs, root)
146+
c.checkErr(err)
147+
c.checkByIter(array)
148+
}
149+
150+
{
151+
// Check by random get
152+
array, err := amt.LoadAMT(context.Background(), c.bs, root)
153+
c.checkErr(err)
154+
c.checkByGet(array)
155+
}
156+
157+
{
158+
// Check by reproducing.
159+
array, err := amt.NewAMT(c.bs)
160+
c.checkErr(err)
161+
for i, j := range c.array {
162+
c.checkErr(array.Set(context.Background(), i, &j))
163+
}
164+
newCid, err := array.Flush(context.Background())
165+
c.checkErr(err)
166+
if newCid != root {
167+
c.fail("expected to reconstruct identical AMT")
168+
}
169+
}
170+
171+
}
172+
173+
func (c *checkedAMT) checkErr(e error) {
174+
if e != nil {
175+
c.fail(e.Error())
176+
}
177+
}
178+
179+
func (c *checkedAMT) checkEq(a, b cbg.CborInt) {
180+
if a != b {
181+
c.fail("expected %d == %d", a, b)
182+
}
183+
}
184+
185+
func (c *checkedAMT) checkByGet(array *amt.Root) {
186+
expectedKeys := make([]uint64, 0, len(c.array))
187+
for k := range c.array {
188+
expectedKeys = append(expectedKeys, k)
189+
}
190+
rand.Shuffle(len(expectedKeys), func(i, j int) {
191+
expectedKeys[i], expectedKeys[j] = expectedKeys[j], expectedKeys[i]
192+
})
193+
for _, k := range expectedKeys {
194+
var actual cbg.CborInt
195+
found, err := array.Get(context.Background(), k, &actual)
196+
c.checkErr(err)
197+
if !found {
198+
c.fail("expected to find key %s", k)
199+
}
200+
c.checkEq(c.array[k], actual)
201+
}
202+
}
203+
204+
func (c *checkedAMT) checkByIter(array *amt.Root) {
205+
toFind := make(map[uint64]cbg.CborInt, len(c.array))
206+
for k, v := range c.array {
207+
toFind[k] = v
208+
}
209+
c.checkEq(cbg.CborInt(len(c.array)), cbg.CborInt(array.Len()))
210+
c.checkErr(array.ForEach(context.Background(), func(k uint64, v *cbg.Deferred) error {
211+
expected, found := toFind[k]
212+
if !found {
213+
c.fail("unexpected key %d", k)
214+
}
215+
delete(toFind, k)
216+
var actual cbg.CborInt
217+
c.checkErr(cbor.DecodeInto(v.Raw, &actual))
218+
c.checkEq(expected, actual)
219+
return nil
220+
}))
221+
if len(toFind) > 0 {
222+
missingKeys := make([]uint64, 0, len(toFind))
223+
for i := range toFind {
224+
missingKeys = append(missingKeys, i)
225+
}
226+
c.fail("failed to find expected entries in AMT: %v", missingKeys)
227+
}
228+
}
229+
230+
func (c *checkedAMT) fail(msg string, args ...interface{}) {
231+
panic(fmt.Sprintf("step %d: "+msg, append([]interface{}{c.step}, args...)...))
232+
}

fuzz/fuzzer.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package fuzzer
2+
3+
import (
4+
"encoding/binary"
5+
"fmt"
6+
7+
"github.com/filecoin-project/go-amt-ipld/v4"
8+
cbg "github.com/whyrusleeping/cbor-gen"
9+
)
10+
11+
var Debug = false
12+
13+
type opCode byte
14+
15+
const (
16+
opSet opCode = iota
17+
opSetSeen
18+
opGet
19+
opGetSeen
20+
opDelete
21+
opDeleteSeen
22+
opFlush
23+
opReload
24+
opMax
25+
)
26+
27+
type op struct {
28+
code opCode
29+
key uint64
30+
value cbg.CborInt
31+
}
32+
33+
func Parse(data []byte) (ops []op) {
34+
scratch := make([]byte, 17)
35+
36+
for len(data) > 0 {
37+
n := copy(scratch, data)
38+
data = data[n:]
39+
40+
code := opCode(scratch[0] % byte(opMax))
41+
k := binary.LittleEndian.Uint64(scratch[1:]) % amt.MaxIndex
42+
v := binary.LittleEndian.Uint64(scratch[9:])
43+
ops = append(ops, op{code, k, cbg.CborInt(v)})
44+
}
45+
return ops
46+
}
47+
48+
func Fuzz(data []byte) int {
49+
if len(data) < 1 {
50+
return -1
51+
}
52+
53+
arr, err := newCheckedAMT()
54+
if err != nil {
55+
panic("failed to construct AMT")
56+
}
57+
for _, op := range Parse(data) {
58+
switch op.code {
59+
case opSet:
60+
arr.set(op.key, op.value)
61+
case opSetSeen:
62+
arr.setSeen(op.key, op.value)
63+
case opGet:
64+
arr.get(op.key)
65+
case opGetSeen:
66+
arr.getSeen(op.key)
67+
case opDelete:
68+
arr.delete(op.key)
69+
case opDeleteSeen:
70+
arr.deleteSeen(op.key)
71+
case opFlush:
72+
arr.flush()
73+
case opReload:
74+
arr.reload()
75+
default:
76+
panic("impossible")
77+
}
78+
}
79+
if Debug {
80+
fmt.Printf("checking\n")
81+
}
82+
arr.check()
83+
return 0
84+
}

fuzz/go.mod

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
module github.com/filecoin-project/go-amt-ipld/fuzz
2+
3+
go 1.15
4+
5+
replace github.com/filecoin-project/go-amt-ipld/v4 => ../
6+
7+
require (
8+
github.com/dvyukov/go-fuzz v0.0.0-20220726122315-1d375ef9f9f6
9+
github.com/elazarl/go-bindata-assetfs v1.0.1
10+
github.com/filecoin-project/go-amt-ipld/v4 v4.1.0
11+
github.com/ipfs/go-block-format v0.1.2
12+
github.com/ipfs/go-cid v0.4.0
13+
github.com/ipfs/go-ipld-cbor v0.0.6
14+
github.com/stephens2424/writerset v1.0.2
15+
github.com/whyrusleeping/cbor-gen v0.0.0-20230126041949-52956bd4c9aa
16+
)

0 commit comments

Comments
 (0)