From a21750ebebaa67d5d9f80385229e2f8b45f071c7 Mon Sep 17 00:00:00 2001 From: Emmanuel T Odeke Date: Sat, 18 Jan 2025 22:12:09 -0800 Subject: [PATCH] feat(tm2/pkg/iavl): add FuzzIterateRange and modernize FuzzMutableTree 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 --- tm2/pkg/iavl/tree_fuzz_test.go | 229 ++++++++++++++++++++++++++++----- 1 file changed, 199 insertions(+), 30 deletions(-) diff --git a/tm2/pkg/iavl/tree_fuzz_test.go b/tm2/pkg/iavl/tree_fuzz_test.go index 08645414fbf9..ba709cc9da2f 100644 --- a/tm2/pkg/iavl/tree_fuzz_test.go +++ b/tm2/pkg/iavl/tree_fuzz_test.go @@ -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,28 +20,36 @@ 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) } @@ -43,39 +57,39 @@ func (p *program) Execute(tree *MutableTree) (err error) { } 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) + } + }) }