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  }