From 83ee4d0509ff0fad88c9226d1bc2e5ceba2a2289 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sat, 13 May 2023 12:42:45 +0100 Subject: [PATCH] internal/literals: add fuzzer To inevstigate #721, I wrote this fuzzer to see if any particular combination of string literals and literal obfuscators would result in a broken program. I didn't find anything, but I reckon this fuzzer can still be useful. --- internal/literals/fuzz_test.go | 115 +++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 internal/literals/fuzz_test.go diff --git a/internal/literals/fuzz_test.go b/internal/literals/fuzz_test.go new file mode 100644 index 0000000..2029e61 --- /dev/null +++ b/internal/literals/fuzz_test.go @@ -0,0 +1,115 @@ +package literals_test + +import ( + "fmt" + "go/ast" + "go/parser" + "go/printer" + "go/token" + "go/types" + mathrand "math/rand" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "sync/atomic" + "testing" + + "mvdan.cc/garble/internal/literals" +) + +// The fuzzing string is passed in as a string and []byte literal. +var fuzzTemplate = ` +package main + +var str string = %#[1]v +var strFold string = "x" + %#[1]v + "y" +var byt []byte = %#[2]v +var bytPtr *[]byte = &%#[2]v + +func main() { + println(str) + println(strFold) + println("--") + println(string(byt)) + println(string(*bytPtr)) +} +`[1:] + +func FuzzObfuscate(f *testing.F) { + initialRandSeed := int64(123) + f.Add("", initialRandSeed) + f.Add("short", initialRandSeed) + f.Add("long_enough_string", initialRandSeed) + f.Add("binary_\x00\x01\x02", initialRandSeed) + f.Add("whitespace \n\t\t", initialRandSeed) + f.Add(strings.Repeat("x", (2<<10)+1), initialRandSeed) // past maxSize + + tdir := f.TempDir() + var tdirCounter atomic.Int64 + f.Fuzz(func(t *testing.T, in string, randSeed int64) { + // The code below is an extreme simplification of what "garble build" does, + // but it does significantly less, allowing the fuzz function to be faster. + // For example, we only obfuscate the literals, not any identifiers. + // Note that the fuzzer is still quite slow, as it still builds a binary. + + // Create the source, parse it, and typecheck it. + srcText := fmt.Sprintf(fuzzTemplate, in, []byte(in)) + t.Log(srcText) // shown on failures + fset := token.NewFileSet() + srcSyntax, err := parser.ParseFile(fset, "", srcText, parser.SkipObjectResolution) + if err != nil { + t.Fatal(err) + } + info := types.Info{ + Types: make(map[ast.Expr]types.TypeAndValue), + Defs: make(map[*ast.Ident]types.Object), + Uses: make(map[*ast.Ident]types.Object), + } + var conf types.Config + if _, err := conf.Check("p", fset, []*ast.File{srcSyntax}, &info); err != nil { + t.Fatal(err) + } + + // Obfuscate the literals and print the source back. + rand := mathrand.New(mathrand.NewSource(randSeed)) + srcSyntax = literals.Obfuscate(rand, srcSyntax, &info, nil) + count := tdirCounter.Add(1) + f, err := os.Create(filepath.Join(tdir, fmt.Sprintf("src_%d.go", count))) + if err != nil { + t.Fatal(err) + } + srcPath := f.Name() + t.Cleanup(func() { + f.Close() + os.Remove(srcPath) + }) + if err := printer.Fprint(f, fset, srcSyntax); err != nil { + t.Fatal(err) + } + + // Build the main package. Use some flags to avoid work. + binPath := strings.TrimSuffix(srcPath, ".go") + if runtime.GOOS == "windows" { + binPath += ".exe" + } + if out, err := exec.Command( + "go", "build", "-trimpath", "-ldflags=-w -s", "-p", "1", + "-o", binPath, srcPath, + ).CombinedOutput(); err != nil { + t.Fatalf("%v: %s", err, out) + } + t.Cleanup(func() { os.Remove(binPath) }) + + // Run the binary, expecting the output to match. + out, err := exec.Command(binPath).CombinedOutput() + if err != nil { + t.Fatalf("%v: %s", err, out) + } + want := fmt.Sprintf("%[1]s\nx%[1]sy\n--\n%[1]s\n%[1]s\n", in) + if got := string(out); got != want { + t.Fatalf("got: %q\nwant: %q", got, want) + } + }) +}