github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/cmd/fuzz/main.go (about)

     1  // Copyright 2019 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  // fuzz builds and executes fuzz tests.
    12  //
    13  // Fuzz tests can be added to CockroachDB by adding a function of the form:
    14  //   func FuzzXXX(data []byte) int
    15  // To help the fuzzer increase coverage, this function should return 1 on
    16  // interesting input (for example, a parse succeeded) and 0 otherwise. Panics
    17  // will be detected and reported.
    18  //
    19  // To exclude this file except during fuzzing, tag it with:
    20  //   // +build gofuzz
    21  package main
    22  
    23  import (
    24  	"bufio"
    25  	"context"
    26  	"flag"
    27  	"fmt"
    28  	"io/ioutil"
    29  	"os"
    30  	"os/exec"
    31  	"path/filepath"
    32  	"regexp"
    33  	"strconv"
    34  	"time"
    35  
    36  	"golang.org/x/tools/go/packages"
    37  )
    38  
    39  var (
    40  	flags   = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
    41  	tests   = flags.String("tests", "", "regex filter for tests to run")
    42  	timeout = flags.Duration("timeout", 1*time.Minute, "time to run each Fuzz func")
    43  	verbose = flags.Bool("v", false, "verbose output")
    44  )
    45  
    46  func usage() {
    47  	fmt.Fprintf(flags.Output(), "Usage of %s:\n", os.Args[0])
    48  	flags.PrintDefaults()
    49  	os.Exit(1)
    50  }
    51  
    52  func main() {
    53  	// go-fuzz-build doesn't seem to support the vendor directory. It
    54  	// appears to require the go-fuzz-dep be in the canonical
    55  	// location. Hence we can't vendor go-fuzz and go-fuzz-build, we
    56  	// require the user install them to their global GOPATH.
    57  	for _, file := range []string{"go-fuzz", "go-fuzz-build"} {
    58  		if _, err := exec.LookPath(file); err != nil {
    59  			fmt.Println(file, "must be in your PATH")
    60  			fmt.Println("Run `go get -u github.com/dvyukov/go-fuzz/...` to install.")
    61  			os.Exit(1)
    62  		}
    63  	}
    64  	if err := flags.Parse(os.Args[1:]); err != nil {
    65  		usage()
    66  	}
    67  	patterns := flags.Args()
    68  	if len(patterns) == 0 {
    69  		fmt.Print("missing packages\n\n")
    70  		usage()
    71  	}
    72  	crashers, err := fuzz(patterns, *tests, *timeout)
    73  	if err != nil {
    74  		fmt.Println(err)
    75  		os.Exit(1)
    76  	}
    77  	if crashers > 0 {
    78  		fmt.Println(crashers, "crashers")
    79  		os.Exit(2)
    80  	}
    81  }
    82  
    83  func fatal(arg interface{}) {
    84  	panic(arg)
    85  }
    86  
    87  func log(format string, args ...interface{}) {
    88  	if !*verbose {
    89  		return
    90  	}
    91  	fmt.Fprintf(os.Stderr, format, args...)
    92  }
    93  
    94  func fuzz(patterns []string, tests string, timeout time.Duration) (int, error) {
    95  	ctx := context.Background()
    96  	pkgs, err := packages.Load(&packages.Config{
    97  		Mode:       packages.NeedFiles,
    98  		BuildFlags: []string{"-tags", "gofuzz"},
    99  	}, patterns...)
   100  	if err != nil {
   101  		return 0, err
   102  	}
   103  	var testsRE *regexp.Regexp
   104  	if tests != "" {
   105  		testsRE, err = regexp.Compile(tests)
   106  		if err != nil {
   107  			return 0, err
   108  		}
   109  	}
   110  	crashers := 0
   111  	for _, pkg := range pkgs {
   112  		if len(pkg.Errors) > 0 {
   113  			return 0, pkg.Errors[0]
   114  		}
   115  		log("%s: searching for Fuzz funcs\n", pkg)
   116  		fns, err := findFuncs(pkg)
   117  		if err != nil {
   118  			return 0, err
   119  		}
   120  		if len(fns) == 0 {
   121  			continue
   122  		}
   123  		dir := filepath.Dir(pkg.GoFiles[0])
   124  		{
   125  			log("%s: executing go-fuzz-build...", pkg)
   126  			cmd := exec.Command("go-fuzz-build",
   127  				// These packages break go-fuzz for some reason, so skip them.
   128  				"-preserve", "github.com/cockroachdb/cockroach/pkg/sql/stats,github.com/cockroachdb/cockroach/pkg/server/serverpb",
   129  			)
   130  			cmd.Dir = dir
   131  			out, err := cmd.CombinedOutput()
   132  			log(" done\n")
   133  			if err != nil {
   134  				log("%s\n", out)
   135  				return 0, err
   136  			}
   137  		}
   138  		for _, fn := range fns {
   139  			if testsRE != nil && !testsRE.MatchString(fn) {
   140  				continue
   141  			}
   142  			crashers += execGoFuzz(ctx, pkg, dir, fn, timeout)
   143  		}
   144  	}
   145  	return crashers, nil
   146  }
   147  
   148  var goFuzzRE = regexp.MustCompile(`crashers: (\d+)`)
   149  
   150  // execGoFuzz executes go-fuzz and returns the number of crashers found.
   151  func execGoFuzz(
   152  	ctx context.Context, pkg *packages.Package, dir, fn string, timeout time.Duration,
   153  ) int {
   154  	log("\n%s: fuzzing %s for %v\n", pkg, fn, timeout)
   155  	ctx, cancel := context.WithTimeout(ctx, timeout)
   156  	defer cancel()
   157  	workdir := fmt.Sprintf("work-%s", fn)
   158  	cmd := exec.CommandContext(ctx, "go-fuzz", "-func", fn, "-workdir", workdir)
   159  	cmd.Dir = dir
   160  	stderr, err := cmd.StderrPipe()
   161  	if err != nil {
   162  		fatal(err)
   163  	}
   164  	if err := cmd.Start(); err != nil {
   165  		fatal(err)
   166  	}
   167  	crashers := 0
   168  	scanner := bufio.NewScanner(stderr)
   169  	for scanner.Scan() {
   170  		line := scanner.Text()
   171  		log("%s\n", line)
   172  		matches := goFuzzRE.FindStringSubmatch(line)
   173  		if len(matches) == 0 {
   174  			continue
   175  		}
   176  		i, err := strconv.Atoi(matches[1])
   177  		if err != nil {
   178  			fatal(err)
   179  		}
   180  		if i > crashers {
   181  			if crashers == 0 {
   182  				fmt.Printf("workdir: %s\n", filepath.Join(dir, workdir))
   183  			}
   184  			crashers = i
   185  			fmt.Printf("crashers: %d\n", crashers)
   186  		}
   187  	}
   188  	if err := scanner.Err(); err != nil {
   189  		fatal(err)
   190  	}
   191  	if err := cmd.Wait(); err != nil {
   192  		fatal(err)
   193  	}
   194  	return crashers
   195  }
   196  
   197  var fuzzFuncRE = regexp.MustCompile(`(?m)^func (Fuzz\w*)\(\w+ \[\]byte\) int {$`)
   198  
   199  // findFuncs returns a list of fuzzable function names in the given package.
   200  func findFuncs(pkg *packages.Package) ([]string, error) {
   201  	var ret []string
   202  	for _, file := range pkg.GoFiles {
   203  		content, err := ioutil.ReadFile(file)
   204  		if err != nil {
   205  			return nil, err
   206  		}
   207  		matches := fuzzFuncRE.FindAllSubmatch(content, -1)
   208  		for _, match := range matches {
   209  			ret = append(ret, string(match[1]))
   210  		}
   211  	}
   212  	return ret, nil
   213  }