GoPLS Viewer

Home|gopls/cmd/signature-fuzzer/fuzz-runner/runner.go
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
10package main
11
12import (
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
28const pkName = "fzTest"
29
30// Basic options
31var verbflag = flag.Int("v"0"Verbose trace output level")
32var loopitflag = flag.Int("numit"10"Number of main loop iterations to run")
33var seedflag = flag.Int64("seed", -1"Random seed")
34var execflag = flag.Bool("execdriver"false"Exec fuzz-driver binary instead of invoking generator directly")
35var numpkgsflag = flag.Int("numpkgs"50"Number of test packages")
36var 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.
40var emitbadflag = flag.Int("emitbad", -1"[Testing only] force generator to emit 'bad' code.")
41var selbadpkgflag = flag.Int("badpkgidx"0"[Testing only] select index of bad package (used with -emitbad)")
42var selbadfcnflag = flag.Int("badfcnidx"0"[Testing only] select index of bad function (used with -emitbad)")
43var forcetmpcleanflag = flag.Bool("forcetmpclean"false"[Testing only] force cleanup of temp dir")
44var cleancacheflag = flag.Bool("cleancache"true"[Testing only] don't clean the go cache")
45var raceflag = flag.Bool("race"false"[Testing only] build generated code with -race")
46
47func verb(vlevel ints stringa ...interface{}) {
48    if *verbflag >= vlevel {
49        fmt.Printf(sa...)
50        fmt.Printf("\n")
51    }
52}
53
54func warn(s stringa ...interface{}) {
55    fmt.Fprintf(os.Stderrsa...)
56    fmt.Fprintf(os.Stderr"\n")
57}
58
59func fatal(s stringa ...interface{}) {
60    fmt.Fprintf(os.Stderrsa...)
61    fmt.Fprintf(os.Stderr"\n")
62    os.Exit(1)
63}
64
65type 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
75func 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.
93func docmd(cmd []stringdir stringint {
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    berr := 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// docodmout forks and execs command 'cmd' in dir 'dir', redirecting
111// stderr and stdout from the execution to file 'outfile'.
112func docmdout(cmd []stringdir stringoutfile stringint {
113    oferr := os.OpenFile(outfileos.O_RDWR|os.O_CREATE|os.O_TRUNC0644)
114    if err != nil {
115        fatal("opening outputfile %s: %v"outfileerr)
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.
143func (c *configgen(singlepk intsinglefn 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.gendirerr)
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{singlepk1}
183        }
184        if singlefn != -1 {
185            c.FcnMask = map[int]int{singlefn1}
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.
196func (c *configaction(cmd []stringoutfile stringemitout boolint {
197    st := docmdout(cmdc.gendiroutfile)
198    if emitout {
199        contenterr := 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
208func 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).
219func (c *configbuild(emitout boolint {
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(cmdc.buildOutFileemitout)
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.
237func (c *configrun(emitout boolint {
238    // Issue a run of the generated code.
239    c.runOutFile = filepath.Join(c.tmpdir"run.err.txt")
240    cmd := []string{filepath.Join(c.gendirbinaryName())}
241    verb(1"run command is: %v"cmd)
242    return c.action(cmdc.runOutFileemitout)
243}
244
245type minimizeMode int
246
247const (
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.
256func (c *configminimize(mode minimizeModeint {
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 := 0pidx < c.NumTestPackagespidx++ {
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 := 0pidx < c.NumTestPackagespidx++ {
298        if pidx != foundPkg {
299            chp := filepath.Join(c.gendirfmt.Sprintf("%s%s%d"c.Taggenerator.CheckerNamepidx))
300            if err := os.RemoveAll(chp); err != nil {
301                fatal("failed to clean pkg subdir %s: %v"chperr)
302            }
303            clp := filepath.Join(c.gendirfmt.Sprintf("%s%s%d"c.Taggenerator.CallerNamepidx))
304            if err := os.RemoveAll(clp); err != nil {
305                fatal("failed to clean pkg subdir %s: %v"clperr)
306            }
307        }
308    }
309
310    // Locate bad function. Again, brute force.
311    for fidx := 0fidx < c.NumTestFunctionsfidx++ {
312        c.gen(foundPkgfidx)
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.
346func (c *configcleanTemp() {
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.
362func (c *configperform() int {
363    defer c.cleanTemp()
364
365    // Main loop
366    for iter := 0iter < *loopitflagiter++ {
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"iterc.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
399func 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    tmpdirerr := 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        GenConfiggenerator.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        tmpdirtmpdir,
434        gendirgendir,
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}
444
MembersX
config.run.emitout
config.minimize.BlockStmt.BlockStmt.clp
config.minimize.fidx
verb.vlevel
docmdout
config.minimize.mode
docmd.err
docmdout.outfile
verb.a
fatal
docmd.st
config.gen.c
config.gen.singlepk
config.gen.BlockStmt.args
ioutil
verb.s
config.run
binaryName
config.build.emitout
config.gcflags
config.gen.err
config.action.cmd
config.action.BlockStmt.content
config.minimize.BlockStmt.BlockStmt.err
config.perform.iter
flag
warn
docmd
config.build.cmd
minimizeMode
main.st
strconv
config.gendir
config.perform.c
main.gendir
config.buildOutFile
docmdout.st
fatal.a
config.runOutFile
usage
docmdout.of
docmdout.err
config.build
log
generator
config.run.cmd
config.minimize
config.cleanTemp.c
main.c
docmdout.c
config.build.c
docmd.b
docmdout.dir
config.action.outfile
config.minimize.BlockStmt.BlockStmt.st
verb
docmd.dir
config.minimize.c
config.minimize.BlockStmt.st
config.cleanTemp
main.tmpdir
warn.s
minimizeBuildFailure
config.perform.BlockStmt.st
config.gen.BlockStmt.st
config.minimize.BlockStmt.BlockStmt.chp
config.gen.BlockStmt.errs
config.action.c
config.action
usage.msg
docmdout.cmd
config.action.emitout
config.perform
time
config.gen.singlefn
config.action.BlockStmt.err
config.run.c
config.minimize.pidx
main
config.nerrors
docmd.c
docmd.cmd
config.gen
pkName
config
config.tmpdir
config.action.st
main.err
main.BlockStmt.now
warn.a
fatal.s
Members
X