github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/cmd/fuzz/main.go (about) 1 // Copyright 2019 The Cockroach Authors. 2 // 3 // Use of this software is governed by the Business Source License 4 // included in the file licenses/BSL.txt. 5 // 6 // As of the Change Date specified in that file, in accordance with 7 // the Business Source License, use of this software will be governed 8 // by the Apache License, Version 2.0, included in the file 9 // licenses/APL.txt. 10 11 // fuzz builds and executes fuzz tests. 12 // 13 // Fuzz tests can be added to CockroachDB by adding a function of the form: 14 // func FuzzXXX(data []byte) int 15 // To help the fuzzer increase coverage, this function should return 1 on 16 // interesting input (for example, a parse succeeded) and 0 otherwise. Panics 17 // will be detected and reported. 18 // 19 // To exclude this file except during fuzzing, tag it with: 20 // // +build gofuzz 21 package main 22 23 import ( 24 "bufio" 25 "context" 26 "flag" 27 "fmt" 28 "io/ioutil" 29 "os" 30 "os/exec" 31 "path/filepath" 32 "regexp" 33 "strconv" 34 "time" 35 36 "golang.org/x/tools/go/packages" 37 ) 38 39 var ( 40 flags = flag.NewFlagSet(os.Args[0], flag.ExitOnError) 41 tests = flags.String("tests", "", "regex filter for tests to run") 42 timeout = flags.Duration("timeout", 1*time.Minute, "time to run each Fuzz func") 43 verbose = flags.Bool("v", false, "verbose output") 44 ) 45 46 func usage() { 47 fmt.Fprintf(flags.Output(), "Usage of %s:\n", os.Args[0]) 48 flags.PrintDefaults() 49 os.Exit(1) 50 } 51 52 func main() { 53 // go-fuzz-build doesn't seem to support the vendor directory. It 54 // appears to require the go-fuzz-dep be in the canonical 55 // location. Hence we can't vendor go-fuzz and go-fuzz-build, we 56 // require the user install them to their global GOPATH. 57 for _, file := range []string{"go-fuzz", "go-fuzz-build"} { 58 if _, err := exec.LookPath(file); err != nil { 59 fmt.Println(file, "must be in your PATH") 60 fmt.Println("Run `go get -u github.com/dvyukov/go-fuzz/...` to install.") 61 os.Exit(1) 62 } 63 } 64 if err := flags.Parse(os.Args[1:]); err != nil { 65 usage() 66 } 67 patterns := flags.Args() 68 if len(patterns) == 0 { 69 fmt.Print("missing packages\n\n") 70 usage() 71 } 72 crashers, err := fuzz(patterns, *tests, *timeout) 73 if err != nil { 74 fmt.Println(err) 75 os.Exit(1) 76 } 77 if crashers > 0 { 78 fmt.Println(crashers, "crashers") 79 os.Exit(2) 80 } 81 } 82 83 func fatal(arg interface{}) { 84 panic(arg) 85 } 86 87 func log(format string, args ...interface{}) { 88 if !*verbose { 89 return 90 } 91 fmt.Fprintf(os.Stderr, format, args...) 92 } 93 94 func fuzz(patterns []string, tests string, timeout time.Duration) (int, error) { 95 ctx := context.Background() 96 pkgs, err := packages.Load(&packages.Config{ 97 Mode: packages.NeedFiles, 98 BuildFlags: []string{"-tags", "gofuzz"}, 99 }, patterns...) 100 if err != nil { 101 return 0, err 102 } 103 var testsRE *regexp.Regexp 104 if tests != "" { 105 testsRE, err = regexp.Compile(tests) 106 if err != nil { 107 return 0, err 108 } 109 } 110 crashers := 0 111 for _, pkg := range pkgs { 112 if len(pkg.Errors) > 0 { 113 return 0, pkg.Errors[0] 114 } 115 log("%s: searching for Fuzz funcs\n", pkg) 116 fns, err := findFuncs(pkg) 117 if err != nil { 118 return 0, err 119 } 120 if len(fns) == 0 { 121 continue 122 } 123 dir := filepath.Dir(pkg.GoFiles[0]) 124 { 125 log("%s: executing go-fuzz-build...", pkg) 126 cmd := exec.Command("go-fuzz-build", 127 // These packages break go-fuzz for some reason, so skip them. 128 "-preserve", "github.com/cockroachdb/cockroach/pkg/sql/stats,github.com/cockroachdb/cockroach/pkg/server/serverpb", 129 ) 130 cmd.Dir = dir 131 out, err := cmd.CombinedOutput() 132 log(" done\n") 133 if err != nil { 134 log("%s\n", out) 135 return 0, err 136 } 137 } 138 for _, fn := range fns { 139 if testsRE != nil && !testsRE.MatchString(fn) { 140 continue 141 } 142 crashers += execGoFuzz(ctx, pkg, dir, fn, timeout) 143 } 144 } 145 return crashers, nil 146 } 147 148 var goFuzzRE = regexp.MustCompile(`crashers: (\d+)`) 149 150 // execGoFuzz executes go-fuzz and returns the number of crashers found. 151 func execGoFuzz( 152 ctx context.Context, pkg *packages.Package, dir, fn string, timeout time.Duration, 153 ) int { 154 log("\n%s: fuzzing %s for %v\n", pkg, fn, timeout) 155 ctx, cancel := context.WithTimeout(ctx, timeout) 156 defer cancel() 157 workdir := fmt.Sprintf("work-%s", fn) 158 cmd := exec.CommandContext(ctx, "go-fuzz", "-func", fn, "-workdir", workdir) 159 cmd.Dir = dir 160 stderr, err := cmd.StderrPipe() 161 if err != nil { 162 fatal(err) 163 } 164 if err := cmd.Start(); err != nil { 165 fatal(err) 166 } 167 crashers := 0 168 scanner := bufio.NewScanner(stderr) 169 for scanner.Scan() { 170 line := scanner.Text() 171 log("%s\n", line) 172 matches := goFuzzRE.FindStringSubmatch(line) 173 if len(matches) == 0 { 174 continue 175 } 176 i, err := strconv.Atoi(matches[1]) 177 if err != nil { 178 fatal(err) 179 } 180 if i > crashers { 181 if crashers == 0 { 182 fmt.Printf("workdir: %s\n", filepath.Join(dir, workdir)) 183 } 184 crashers = i 185 fmt.Printf("crashers: %d\n", crashers) 186 } 187 } 188 if err := scanner.Err(); err != nil { 189 fatal(err) 190 } 191 if err := cmd.Wait(); err != nil { 192 fatal(err) 193 } 194 return crashers 195 } 196 197 var fuzzFuncRE = regexp.MustCompile(`(?m)^func (Fuzz\w*)\(\w+ \[\]byte\) int {$`) 198 199 // findFuncs returns a list of fuzzable function names in the given package. 200 func findFuncs(pkg *packages.Package) ([]string, error) { 201 var ret []string 202 for _, file := range pkg.GoFiles { 203 content, err := ioutil.ReadFile(file) 204 if err != nil { 205 return nil, err 206 } 207 matches := fuzzFuncRE.FindAllSubmatch(content, -1) 208 for _, match := range matches { 209 ret = append(ret, string(match[1])) 210 } 211 } 212 return ret, nil 213 }