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  }