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
#3548)

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 authored Jan 31, 2025
1 parent 5c8dcfd commit 7992a29
Showing 1 changed file with 199 additions and 30 deletions.
229 changes: 199 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"
@@ -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.Sprint(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.
@@ -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)})
}
}
}
@@ -107,19 +121,174 @@ func genRandomProgram(size int) *program {
func TestMutableTreeFuzz(t *testing.T) {
t.Parallel()

runThenGenerateMutableTreeFuzzSeeds(t, false)
}

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

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
}

tree := NewMutableTree(memdb.NewMemDB(), 0)
err := program.Execute(tree)
if err != nil {
t.Fatal(err)
}
})
}

0 comments on commit 7992a29

Please sign in to comment.