Skip to content

Commit

Permalink
feat(tm2/pkg/iavl): add FuzzIterateRange and modernize FuzzMutableTree
Browse files Browse the repository at this point in the history
This change hooks MutableTree fuzzing to Go's native fuzzing that's
more intelligent and coverage guided to mutate inputs instead of
naive random program generation.
While here also added FuzzIterateRange.

Updates #3087
  • Loading branch information
odeke-em committed Jan 19, 2025
1 parent d2813f8 commit 5d8bb6e
Showing 1 changed file with 214 additions and 30 deletions.
244 changes: 214 additions & 30 deletions tm2/pkg/iavl/tree_fuzz_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
package iavl

import (
"encoding/json"
"fmt"
"io"
"io/fs"
"math/rand"
"os"
"path/filepath"
"strings"
"testing"

"github.com/gnolang/gno/tm2/pkg/db/memdb"
Expand All @@ -14,68 +20,76 @@ import (

// A program is a list of instructions.
type program struct {
instructions []instruction
Instructions []instruction `json:"instructions"`
}

func (p *program) Execute(tree *MutableTree) (err error) {
var errLine int

defer func() {
if r := recover(); r != nil {
var str string

for i, instr := range p.instructions {
prefix := " "
if i == errLine {
prefix = ">> "
}
str += prefix + instr.String() + "\n"
r := recover()
if r == nil {
return
}

// These are simply input errors and shouldn't be reported as actual logical issues.
if containsAny(fmt.Sprintf("%s", r), "Unrecognized op:", "Attempt to store nil value at key") {
return
}

var str string

for i, instr := range p.Instructions {
prefix := " "
if i == errLine {
prefix = ">> "
}
err = fmt.Errorf("Program panicked with: %s\n%s", r, str)
str += prefix + instr.String() + "\n"
}
err = fmt.Errorf("Program panicked with: %s\n%s", r, str)
}()

for i, instr := range p.instructions {
for i, instr := range p.Instructions {
errLine = i
instr.Execute(tree)
}
return
}

func (p *program) addInstruction(i instruction) {
p.instructions = append(p.instructions, i)
p.Instructions = append(p.Instructions, i)
}

func (p *program) size() int {
return len(p.instructions)
return len(p.Instructions)
}

type instruction struct {
op string
k, v []byte
version int64
Op string
K, V []byte
Version int64
}

func (i instruction) Execute(tree *MutableTree) {
switch i.op {
switch i.Op {
case "SET":
tree.Set(i.k, i.v)
tree.Set(i.K, i.V)
case "REMOVE":
tree.Remove(i.k)
tree.Remove(i.K)
case "SAVE":
tree.SaveVersion()
case "DELETE":
tree.DeleteVersion(i.version)
tree.DeleteVersion(i.Version)
default:
panic("Unrecognized op: " + i.op)
panic("Unrecognized op: " + i.Op)
}
}

func (i instruction) String() string {
if i.version > 0 {
return fmt.Sprintf("%-8s %-8s %-8s %-8d", i.op, i.k, i.v, i.version)
if i.Version > 0 {
return fmt.Sprintf("%-8s %-8s %-8s %-8d", i.Op, i.K, i.V, i.Version)
}
return fmt.Sprintf("%-8s %-8s %-8s", i.op, i.k, i.v)
return fmt.Sprintf("%-8s %-8s %-8s", i.Op, i.K, i.V)
}

// Generate a random program of the given size.
Expand All @@ -88,15 +102,15 @@ func genRandomProgram(size int) *program {

switch rand.Int() % 7 {
case 0, 1, 2:
p.addInstruction(instruction{op: "SET", k: k, v: v})
p.addInstruction(instruction{Op: "SET", K: k, V: v})
case 3, 4:
p.addInstruction(instruction{op: "REMOVE", k: k})
p.addInstruction(instruction{Op: "REMOVE", K: k})
case 5:
p.addInstruction(instruction{op: "SAVE", version: int64(nextVersion)})
p.addInstruction(instruction{Op: "SAVE", Version: int64(nextVersion)})
nextVersion++
case 6:
if rv := rand.Int() % nextVersion; rv < nextVersion && rv > 0 {
p.addInstruction(instruction{op: "DELETE", version: int64(rv)})
p.addInstruction(instruction{Op: "DELETE", Version: int64(rv)})
}
}
}
Expand All @@ -107,19 +121,189 @@ func genRandomProgram(size int) *program {
func TestMutableTreeFuzz(t *testing.T) {
t.Parallel()

runBasicMutableTreeFuzzing(t)
}

var pathForMutableTreeProgramSeeds = filepath.Join("testdata", "corpra", "mutable_tree_programs")

func runBasicMutableTreeFuzzing(t *testing.T) {

Check failure on line 129 in tm2/pkg/iavl/tree_fuzz_test.go

View workflow job for this annotation

GitHub Actions / Run TM2 suite / Go Lint / lint

test helper function should start from t.Helper() (thelper)
runThenGenerateMutableTreeFuzzSeeds(t, false)
}

func runThenGenerateMutableTreeFuzzSeeds(tb testing.TB, writeSeedsToFileSystem bool) {
tb.Helper()

if testing.Short() {
tb.Skip("Running in -short mode")
}

maxIterations := testFuzzIterations
progsPerIteration := 100000
iterations := 0

if writeSeedsToFileSystem {
if err := os.MkdirAll(pathForMutableTreeProgramSeeds, 0o755); err != nil {
tb.Fatal(err)
}
}

for size := 5; iterations < maxIterations; size++ {
for i := 0; i < progsPerIteration/size; i++ {
tree := NewMutableTree(memdb.NewMemDB(), 0)
program := genRandomProgram(size)
err := program.Execute(tree)
if err != nil {
t.Fatalf("Error after %d iterations (size %d): %s\n%s", iterations, size, err.Error(), tree.String())
tb.Fatalf("Error after %d iterations (size %d): %s\n%s", iterations, size, err.Error(), tree.String())
}
iterations++

if !writeSeedsToFileSystem {
continue
}

// Otherwise write them to the testdata/corpra directory.
programJSON, err := json.Marshal(program)
if err != nil {
tb.Fatal(err)
}
path := filepath.Join(pathForMutableTreeProgramSeeds, fmt.Sprintf("%d", i+1))
if err := os.WriteFile(path, programJSON, 0o755); err != nil {
tb.Fatal(err)
}
}
}
}

type treeRange struct {
Start []byte
End []byte
Forward bool
}

var basicRecords = []struct {
key, value string
}{
{"abc", "123"},
{"low", "high"},
{"fan", "456"},
{"foo", "a"},
{"foobaz", "c"},
{"good", "bye"},
{"foobang", "d"},
{"foobar", "b"},
{"food", "e"},
{"foml", "f"},
}

// Allows hooking into Go's fuzzers and then for continuous fuzzing
// enriched with coverage guided mutations, instead of naive mutations.
func FuzzIterateRange(f *testing.F) {
if testing.Short() {
f.Skip("Skipping in -short mode")
}

// 1. Add the seeds.
seeds := []*treeRange{
{[]byte("foo"), []byte("goo"), true},
{[]byte("aaa"), []byte("abb"), true},
{nil, []byte("flap"), true},
{[]byte("foob"), nil, true},
{[]byte("very"), nil, true},
{[]byte("very"), nil, false},
{[]byte("fooba"), []byte("food"), true},
{[]byte("fooba"), []byte("food"), false},
{[]byte("g"), nil, false},
}
for _, seed := range seeds {
blob, err := json.Marshal(seed)
if err != nil {
f.Fatal(err)
}
f.Add(blob)
}

db := memdb.NewMemDB()
tree := NewMutableTree(db, 0)
for _, br := range basicRecords {
tree.Set([]byte(br.key), []byte(br.value))
}

var trav traverser

// 2. Run the fuzzer.
f.Fuzz(func(t *testing.T, rangeJSON []byte) {
tr := new(treeRange)
if err := json.Unmarshal(rangeJSON, tr); err != nil {
return
}

tree.IterateRange(tr.Start, tr.End, tr.Forward, trav.view)
})
}

func containsAny(s string, anyOf ...string) bool {
for _, q := range anyOf {
if strings.Contains(s, q) {
return true
}
}
return false
}

func FuzzMutableTreeInstructions(f *testing.F) {
if testing.Short() {
f.Skip("Skipping in -short mode")
}

// 0. Generate then add the seeds.
runThenGenerateMutableTreeFuzzSeeds(f, true)

// 1. Add the seeds.
dir := os.DirFS("testdata")
err := fs.WalkDir(dir, ".", func(path string, de fs.DirEntry, err error) error {
if de.IsDir() {
return err
}

ff, err := dir.Open(path)
if err != nil {
return err
}
defer ff.Close()

blob, err := io.ReadAll(ff)
if err != nil {
return err
}
f.Add(blob)
return nil
})
if err != nil {
f.Fatal(err)
}

// 2. Run the fuzzer.
f.Fuzz(func(t *testing.T, programJSON []byte) {
program := new(program)
if err := json.Unmarshal(programJSON, program); err != nil {
return
}

defer func() {
r := recover()
if r == nil {
return
}

s := fmt.Sprintf("%s", r)
if !containsAny(s, "Unrecognized op:", "Attempt to store nil value at key") {
panic(r)
}
}()
tree := NewMutableTree(memdb.NewMemDB(), 0)
err := program.Execute(tree)
if err != nil {
t.Fatal(err)
}
})
}

0 comments on commit 5d8bb6e

Please sign in to comment.