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