github.com/cockroachdb/tools@v0.0.0-20230222021103-a6d27438930d/cmd/signature-fuzzer/fuzz-runner/runner.go (about) 1 // Copyright 2021 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Program for performing test runs using "fuzz-driver". 6 // Main loop iteratively runs "fuzz-driver" to create a corpus, 7 // then builds and runs the code. If a failure in the run is 8 // detected, then a testcase minimization phase kicks in. 9 10 package main 11 12 import ( 13 "flag" 14 "fmt" 15 "io/ioutil" 16 "log" 17 "os" 18 "os/exec" 19 "path/filepath" 20 "runtime" 21 "strconv" 22 "strings" 23 "time" 24 25 generator "golang.org/x/tools/cmd/signature-fuzzer/internal/fuzz-generator" 26 ) 27 28 const pkName = "fzTest" 29 30 // Basic options 31 var verbflag = flag.Int("v", 0, "Verbose trace output level") 32 var loopitflag = flag.Int("numit", 10, "Number of main loop iterations to run") 33 var seedflag = flag.Int64("seed", -1, "Random seed") 34 var execflag = flag.Bool("execdriver", false, "Exec fuzz-driver binary instead of invoking generator directly") 35 var numpkgsflag = flag.Int("numpkgs", 50, "Number of test packages") 36 var numfcnsflag = flag.Int("numfcns", 20, "Number of test functions per package.") 37 38 // Debugging/testing options. These tell the generator to emit "bad" code so as to 39 // test the logic for detecting errors and/or minimization. 40 var emitbadflag = flag.Int("emitbad", -1, "[Testing only] force generator to emit 'bad' code.") 41 var selbadpkgflag = flag.Int("badpkgidx", 0, "[Testing only] select index of bad package (used with -emitbad)") 42 var selbadfcnflag = flag.Int("badfcnidx", 0, "[Testing only] select index of bad function (used with -emitbad)") 43 var forcetmpcleanflag = flag.Bool("forcetmpclean", false, "[Testing only] force cleanup of temp dir") 44 var cleancacheflag = flag.Bool("cleancache", true, "[Testing only] don't clean the go cache") 45 var raceflag = flag.Bool("race", false, "[Testing only] build generated code with -race") 46 47 func verb(vlevel int, s string, a ...interface{}) { 48 if *verbflag >= vlevel { 49 fmt.Printf(s, a...) 50 fmt.Printf("\n") 51 } 52 } 53 54 func warn(s string, a ...interface{}) { 55 fmt.Fprintf(os.Stderr, s, a...) 56 fmt.Fprintf(os.Stderr, "\n") 57 } 58 59 func fatal(s string, a ...interface{}) { 60 fmt.Fprintf(os.Stderr, s, a...) 61 fmt.Fprintf(os.Stderr, "\n") 62 os.Exit(1) 63 } 64 65 type config struct { 66 generator.GenConfig 67 tmpdir string 68 gendir string 69 buildOutFile string 70 runOutFile string 71 gcflags string 72 nerrors int 73 } 74 75 func usage(msg string) { 76 if len(msg) > 0 { 77 fmt.Fprintf(os.Stderr, "error: %s\n", msg) 78 } 79 fmt.Fprintf(os.Stderr, "usage: fuzz-runner [flags]\n\n") 80 flag.PrintDefaults() 81 fmt.Fprintf(os.Stderr, "Example:\n\n") 82 fmt.Fprintf(os.Stderr, " fuzz-runner -numit=500 -numpkgs=11 -numfcns=13 -seed=10101\n\n") 83 fmt.Fprintf(os.Stderr, " \tRuns 500 rounds of test case generation\n") 84 fmt.Fprintf(os.Stderr, " \tusing random see 10101, in each round emitting\n") 85 fmt.Fprintf(os.Stderr, " \t11 packages each with 13 function pairs.\n") 86 87 os.Exit(2) 88 } 89 90 // docmd executes the specified command in the dir given and pipes the 91 // output to stderr. return status is 0 if command passed, 1 92 // otherwise. 93 func docmd(cmd []string, dir string) int { 94 verb(2, "docmd: %s", strings.Join(cmd, " ")) 95 c := exec.Command(cmd[0], cmd[1:]...) 96 if dir != "" { 97 c.Dir = dir 98 } 99 b, err := c.CombinedOutput() 100 st := 0 101 if err != nil { 102 warn("error executing cmd %s: %v", 103 strings.Join(cmd, " "), err) 104 st = 1 105 } 106 os.Stderr.Write(b) 107 return st 108 } 109 110 // docmdout forks and execs command 'cmd' in dir 'dir', redirecting 111 // stderr and stdout from the execution to file 'outfile'. 112 func docmdout(cmd []string, dir string, outfile string) int { 113 of, err := os.OpenFile(outfile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) 114 if err != nil { 115 fatal("opening outputfile %s: %v", outfile, err) 116 } 117 c := exec.Command(cmd[0], cmd[1:]...) 118 defer of.Close() 119 if dir != "" { 120 verb(2, "setting cmd.Dir to %s", dir) 121 c.Dir = dir 122 } 123 verb(2, "docmdout: %s > %s", strings.Join(cmd, " "), outfile) 124 c.Stdout = of 125 c.Stderr = of 126 err = c.Run() 127 st := 0 128 if err != nil { 129 warn("error executing cmd %s: %v", 130 strings.Join(cmd, " "), err) 131 st = 1 132 } 133 return st 134 } 135 136 // gen is the main hook for kicking off code generation. For 137 // non-minimization runs, 'singlepk' and 'singlefn' will both be -1 138 // (indicating that we want all functions and packages to be 139 // generated). If 'singlepk' is set to a non-negative value, then 140 // code generation will be restricted to the single package with that 141 // index (as a try at minimization), similarly with 'singlefn' 142 // restricting the codegen to a single specified function. 143 func (c *config) gen(singlepk int, singlefn int) { 144 145 // clean the output dir 146 verb(2, "cleaning outdir %s", c.gendir) 147 if err := os.RemoveAll(c.gendir); err != nil { 148 fatal("error cleaning gen dir %s: %v", c.gendir, err) 149 } 150 151 // emit code into the output dir. Here we either invoke the 152 // generator directly, or invoke fuzz-driver if -execflag is 153 // set. If the code generation process itself fails, this is 154 // typically a bug in the fuzzer itself, so it gets reported 155 // as a fatal error. 156 if *execflag { 157 args := []string{"fuzz-driver", 158 "-numpkgs", strconv.Itoa(c.NumTestPackages), 159 "-numfcns", strconv.Itoa(c.NumTestFunctions), 160 "-seed", strconv.Itoa(int(c.Seed)), 161 "-outdir", c.OutDir, 162 "-pkgpath", pkName, 163 "-maxfail", strconv.Itoa(c.MaxFail)} 164 if singlepk != -1 { 165 args = append(args, "-pkgmask", strconv.Itoa(singlepk)) 166 } 167 if singlefn != -1 { 168 args = append(args, "-fcnmask", strconv.Itoa(singlefn)) 169 } 170 if *emitbadflag != 0 { 171 args = append(args, "-emitbad", strconv.Itoa(*emitbadflag), 172 "-badpkgidx", strconv.Itoa(*selbadpkgflag), 173 "-badfcnidx", strconv.Itoa(*selbadfcnflag)) 174 } 175 verb(1, "invoking fuzz-driver with args: %v", args) 176 st := docmd(args, "") 177 if st != 0 { 178 fatal("fatal error: generation failed, cmd was: %v", args) 179 } 180 } else { 181 if singlepk != -1 { 182 c.PkgMask = map[int]int{singlepk: 1} 183 } 184 if singlefn != -1 { 185 c.FcnMask = map[int]int{singlefn: 1} 186 } 187 verb(1, "invoking generator.Generate with config: %v", c.GenConfig) 188 errs := generator.Generate(c.GenConfig) 189 if errs != 0 { 190 log.Fatal("errors during generation") 191 } 192 } 193 } 194 195 // action performs a selected action/command in the generated code dir. 196 func (c *config) action(cmd []string, outfile string, emitout bool) int { 197 st := docmdout(cmd, c.gendir, outfile) 198 if emitout { 199 content, err := ioutil.ReadFile(outfile) 200 if err != nil { 201 log.Fatal(err) 202 } 203 fmt.Fprintf(os.Stderr, "%s", content) 204 } 205 return st 206 } 207 208 func binaryName() string { 209 if runtime.GOOS == "windows" { 210 return pkName + ".exe" 211 } else { 212 return "./" + pkName 213 } 214 } 215 216 // build builds a generated corpus of Go code. If 'emitout' is set, then dump out the 217 // results of the build after it completes (during minimization emitout is set to false, 218 // since there is no need to see repeated errors). 219 func (c *config) build(emitout bool) int { 220 // Issue a build of the generated code. 221 c.buildOutFile = filepath.Join(c.tmpdir, "build.err.txt") 222 cmd := []string{"go", "build", "-o", binaryName()} 223 if c.gcflags != "" { 224 cmd = append(cmd, "-gcflags=all="+c.gcflags) 225 } 226 if *raceflag { 227 cmd = append(cmd, "-race") 228 } 229 cmd = append(cmd, ".") 230 verb(1, "build command is: %v", cmd) 231 return c.action(cmd, c.buildOutFile, emitout) 232 } 233 234 // run invokes a binary built from a generated corpus of Go code. If 235 // 'emitout' is set, then dump out the results of the run after it 236 // completes. 237 func (c *config) run(emitout bool) int { 238 // Issue a run of the generated code. 239 c.runOutFile = filepath.Join(c.tmpdir, "run.err.txt") 240 cmd := []string{filepath.Join(c.gendir, binaryName())} 241 verb(1, "run command is: %v", cmd) 242 return c.action(cmd, c.runOutFile, emitout) 243 } 244 245 type minimizeMode int 246 247 const ( 248 minimizeBuildFailure = iota 249 minimizeRuntimeFailure 250 ) 251 252 // minimize tries to minimize a failing scenario down to a single 253 // package and/or function if possible. This is done using an 254 // iterative search. Here 'minimizeMode' tells us whether we're 255 // looking for a compile-time error or a runtime error. 256 func (c *config) minimize(mode minimizeMode) int { 257 258 verb(0, "... starting minimization for failed directory %s", c.gendir) 259 260 foundPkg := -1 261 foundFcn := -1 262 263 // Locate bad package. Uses brute-force linear search, could do better... 264 for pidx := 0; pidx < c.NumTestPackages; pidx++ { 265 verb(1, "minimization: trying package %d", pidx) 266 c.gen(pidx, -1) 267 st := c.build(false) 268 if mode == minimizeBuildFailure { 269 if st != 0 { 270 // Found. 271 foundPkg = pidx 272 c.nerrors++ 273 break 274 } 275 } else { 276 if st != 0 { 277 warn("run minimization: unexpected build failed while searching for bad pkg") 278 return 1 279 } 280 st := c.run(false) 281 if st != 0 { 282 // Found. 283 c.nerrors++ 284 verb(1, "run minimization found bad package: %d", pidx) 285 foundPkg = pidx 286 break 287 } 288 } 289 } 290 if foundPkg == -1 { 291 verb(0, "** minimization failed, could not locate bad package") 292 return 1 293 } 294 warn("package minimization succeeded: found bad pkg %d", foundPkg) 295 296 // clean unused packages 297 for pidx := 0; pidx < c.NumTestPackages; pidx++ { 298 if pidx != foundPkg { 299 chp := filepath.Join(c.gendir, fmt.Sprintf("%s%s%d", c.Tag, generator.CheckerName, pidx)) 300 if err := os.RemoveAll(chp); err != nil { 301 fatal("failed to clean pkg subdir %s: %v", chp, err) 302 } 303 clp := filepath.Join(c.gendir, fmt.Sprintf("%s%s%d", c.Tag, generator.CallerName, pidx)) 304 if err := os.RemoveAll(clp); err != nil { 305 fatal("failed to clean pkg subdir %s: %v", clp, err) 306 } 307 } 308 } 309 310 // Locate bad function. Again, brute force. 311 for fidx := 0; fidx < c.NumTestFunctions; fidx++ { 312 c.gen(foundPkg, fidx) 313 st := c.build(false) 314 if mode == minimizeBuildFailure { 315 if st != 0 { 316 // Found. 317 verb(1, "build minimization found bad function: %d", fidx) 318 foundFcn = fidx 319 break 320 } 321 } else { 322 if st != 0 { 323 warn("run minimization: unexpected build failed while searching for bad fcn") 324 return 1 325 } 326 st := c.run(false) 327 if st != 0 { 328 // Found. 329 verb(1, "run minimization found bad function: %d", fidx) 330 foundFcn = fidx 331 break 332 } 333 } 334 // not the function we want ... continue the hunt 335 } 336 if foundFcn == -1 { 337 verb(0, "** function minimization failed, could not locate bad function") 338 return 1 339 } 340 warn("function minimization succeeded: found bad fcn %d", foundFcn) 341 342 return 0 343 } 344 345 // cleanTemp removes the temp dir we've been working with. 346 func (c *config) cleanTemp() { 347 if !*forcetmpcleanflag { 348 if c.nerrors != 0 { 349 verb(1, "preserving temp dir %s", c.tmpdir) 350 return 351 } 352 } 353 verb(1, "cleaning temp dir %s", c.tmpdir) 354 os.RemoveAll(c.tmpdir) 355 } 356 357 // perform is the top level driver routine for the program, containing the 358 // main loop. Each iteration of the loop performs a generate/build/run 359 // sequence, and then updates the seed afterwards if no failure is found. 360 // If a failure is detected, we try to minimize it and then return without 361 // attempting any additional tests. 362 func (c *config) perform() int { 363 defer c.cleanTemp() 364 365 // Main loop 366 for iter := 0; iter < *loopitflag; iter++ { 367 if iter != 0 && iter%50 == 0 { 368 // Note: cleaning the Go cache periodically is 369 // pretty much a requirement if you want to do 370 // things like overnight runs of the fuzzer, 371 // but it is also a very unfriendly thing do 372 // to if we're executing as part of a unit 373 // test run (in which case there may be other 374 // tests running in parallel with this 375 // one). Check the "cleancache" flag before 376 // doing this. 377 if *cleancacheflag { 378 docmd([]string{"go", "clean", "-cache"}, "") 379 } 380 } 381 verb(0, "... begin iteration %d with current seed %d", iter, c.Seed) 382 c.gen(-1, -1) 383 st := c.build(true) 384 if st != 0 { 385 c.minimize(minimizeBuildFailure) 386 return 1 387 } 388 st = c.run(true) 389 if st != 0 { 390 c.minimize(minimizeRuntimeFailure) 391 return 1 392 } 393 // update seed so that we get different code on the next iter. 394 c.Seed += 101 395 } 396 return 0 397 } 398 399 func main() { 400 log.SetFlags(0) 401 log.SetPrefix("fuzz-runner: ") 402 flag.Parse() 403 if flag.NArg() != 0 { 404 usage("unknown extra arguments") 405 } 406 verb(1, "in main, verblevel=%d", *verbflag) 407 408 tmpdir, err := ioutil.TempDir("", "fuzzrun") 409 if err != nil { 410 fatal("creation of tempdir failed: %v", err) 411 } 412 gendir := filepath.Join(tmpdir, "fuzzTest") 413 414 // select starting seed 415 if *seedflag == -1 { 416 now := time.Now() 417 *seedflag = now.UnixNano() % 123456789 418 } 419 420 // set up params for this run 421 c := &config{ 422 GenConfig: generator.GenConfig{ 423 NumTestPackages: *numpkgsflag, // 100 424 NumTestFunctions: *numfcnsflag, // 20 425 Seed: *seedflag, 426 OutDir: gendir, 427 Pragma: "-maxfail=9999", 428 PkgPath: pkName, 429 EmitBad: *emitbadflag, 430 BadPackageIdx: *selbadpkgflag, 431 BadFuncIdx: *selbadfcnflag, 432 }, 433 tmpdir: tmpdir, 434 gendir: gendir, 435 } 436 437 // kick off the main loop. 438 st := c.perform() 439 440 // done 441 verb(1, "leaving main, num errors=%d", c.nerrors) 442 os.Exit(st) 443 }