[tools] Improvements to 'fuzz'
Add support for running multiple fuzzer targets (only 'wgsl' right now).
Copy filtered files out of tint/test to the corpus directory, so that we don't fuzz uninteresting files.
Pass --dxc flag to fuzzers
Add a flag for ignoring non-security crashes.
Change-Id: I1a49c61c642118880da71ea2eaa42b1734ec44df
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/190922
Reviewed-by: Ryan Harrison <rharrison@chromium.org>
Commit-Queue: Ryan Harrison <rharrison@chromium.org>
Auto-Submit: Ben Clayton <bclayton@google.com>
diff --git a/tools/src/cmd/fuzz/main.go b/tools/src/cmd/fuzz/main.go
index 7bddaee..2fd4736 100644
--- a/tools/src/cmd/fuzz/main.go
+++ b/tools/src/cmd/fuzz/main.go
@@ -38,6 +38,7 @@
"os"
"os/exec"
"path/filepath"
+ "regexp"
"runtime"
"strings"
"sync/atomic"
@@ -50,10 +51,6 @@
"dawn.googlesource.com/dawn/tools/src/utils"
)
-const (
- wgslDictionaryRelPath = "src/tint/cmd/fuzz/wgsl/dictionary.txt"
-)
-
func main() {
if err := run(); err != nil {
fmt.Println(err)
@@ -61,34 +58,64 @@
}
}
-func showUsage() {
- fmt.Println(`
+type fuzzerInfo struct {
+ name string // Short name of the fuzzer
+ path string // Path to the fuzzer executable
+ ext string // File extensions used by the fuzzer
+ dict string // Optional path to a dictionary file for the fuzzer
+}
+
+func run() error {
+ t := tool{}
+
+ allFuzzers := []fuzzerInfo{
+ {
+ name: "wgsl",
+ path: "tint_wgsl_fuzzer",
+ ext: ".wgsl",
+ dict: "src/tint/cmd/fuzz/wgsl/dictionary.txt",
+ },
+ }
+ fuzzerByName := map[string]fuzzerInfo{}
+ for _, fuzzer := range allFuzzers {
+ fuzzerByName[fuzzer.name] = fuzzer
+ }
+ allFuzzerNames := transform.SliceNoErr(allFuzzers, func(f fuzzerInfo) string { return f.name })
+
+ check := false
+ build := ""
+ flag.BoolVar(&t.verbose, "verbose", false, "print additional output")
+ flag.BoolVar(&check, "check", false, "check that all the end-to-end test do not fail")
+ flag.BoolVar(&t.securityOnly, "security-only", false, "ignore issues that are not considered security impacting")
+ flag.StringVar(&t.filter, "filter", "", "filter the fuzzers run to those with this substring")
+ flag.StringVar(&t.corpus, "corpus", defaultCorpusDir(), "the corpus directory")
+ flag.StringVar(&build, "build", defaultBuildDir(), "the build directory")
+ flag.StringVar(&t.out, "out", "<tmp>", "the directory to hold generated test files")
+ flag.IntVar(&t.numProcesses, "j", runtime.NumCPU(), "number of concurrent fuzzers to run")
+ flag.Usage = func() {
+ fmt.Printf(`
fuzz is a helper for running the tint fuzzer executables
fuzz can check that the corpus does not trigger any issues with the fuzzers, and
simplify running of the fuzzers locally.
usage:
- fuzz [flags...]`)
- flag.PrintDefaults()
- fmt.Println(``)
- os.Exit(1)
-}
+ fuzz [fuzzers] [flags...]
-func run() error {
- t := tool{}
-
- check := false
- build := ""
- flag.BoolVar(&t.verbose, "verbose", false, "print additional output")
- flag.BoolVar(&check, "check", false, "check that all the end-to-end test do not fail")
- flag.StringVar(&t.filter, "filter", "", "filter the fuzzers run to those with this substring")
- flag.StringVar(&t.corpus, "corpus", defaultCorpusDir(), "the corpus directory")
- flag.StringVar(&build, "build", defaultBuildDir(), "the build directory")
- flag.StringVar(&t.out, "out", "<tmp>", "the directory to hold generated test files")
- flag.IntVar(&t.numProcesses, "j", runtime.NumCPU(), "number of concurrent fuzzers to run")
+fuzzers are the fuzzer types to run, defaults to all.
+ Possible values: ` + strings.Join(allFuzzerNames, ", ") + `
+`)
+ flag.PrintDefaults()
+ fmt.Println(``)
+ os.Exit(1)
+ }
flag.Parse()
+ selectedFuzzers := flag.Args()
+ if len(selectedFuzzers) == 0 {
+ selectedFuzzers = allFuzzerNames
+ }
+
if t.numProcesses < 1 {
t.numProcesses = 1
}
@@ -110,17 +137,31 @@
return fmt.Errorf("output directory '%v' does not exist", t.out)
}
- // Verify all of the fuzzer executables are present
- for _, fuzzer := range []struct {
- name string
- path *string
- }{
- {"tint_wgsl_fuzzer", &t.wgslFuzzer},
- } {
- *fuzzer.path = filepath.Join(build, fuzzer.name)
- if !fileutils.IsExe(*fuzzer.path) {
- return fmt.Errorf("fuzzer not found at '%v'", *fuzzer.path)
+ // Register all the fuzzers
+ for _, name := range selectedFuzzers {
+ fuzzer, ok := fuzzerByName[name]
+ if !ok {
+ return fmt.Errorf("unknown fuzzer '%v'. Possible values: %v", name, strings.Join(allFuzzerNames, ", "))
}
+
+ fuzzer.path = filepath.Join(build, fuzzer.path)
+ if !fileutils.IsExe(fuzzer.path) {
+ return fmt.Errorf("fuzzer not found at '%v'", fuzzer.path)
+ }
+
+ if fuzzer.dict != "" {
+ dictPath, err := filepath.Abs(filepath.Join(fileutils.DawnRoot(), fuzzer.dict))
+ if err != nil || !fileutils.IsFile(dictPath) {
+ return fmt.Errorf("failed to obtain the dictionary.txt path: %w", err)
+ }
+ fuzzer.dict = dictPath
+ }
+
+ t.fuzzers = append(t.fuzzers, fuzzer)
+ }
+
+ if dxc := filepath.Join(build, dxcFileName()); fileutils.IsFile(dxc) {
+ t.dxc = dxc
}
// If --check was passed, then just ensure that all the files in the corpus
@@ -135,27 +176,39 @@
type tool struct {
verbose bool
- filter string // filter fuzzers to those with this substring
- corpus string // directory of test files
- out string // where to emit new test files
- wgslFuzzer string // path to tint_wgsl_fuzzer
- numProcesses int // number of concurrent processes to spawn
+ filter string // filter fuzzers to those with this substring
+ corpus string // directory of test files
+ out string // where to emit new test files
+ dxc string // path to the DXC DLL / so
+ fuzzers []fuzzerInfo // the fuzzers to run
+ numProcesses int // number of concurrent processes to spawn
+ securityOnly bool // Ignore non-security crashes
}
// check() runs the fuzzers against all the .wgsl files under to the corpus directory,
// ensuring that the fuzzers do not error for the given file.
func (t tool) check() error {
- wgslFiles, err := glob.Glob(filepath.Join(t.corpus, "**.wgsl"))
- if err != nil {
- return err
+ type job struct {
+ file string
+ exe string
}
- // Remove '*.expected.wgsl'
- wgslFiles = transform.Filter(wgslFiles, func(s string) bool { return !strings.Contains(s, ".expected.") })
+ jobs := []job{}
- log.Printf("checking %v files...\n", len(wgslFiles))
+ for _, fuzzer := range t.fuzzers {
+ files, err := t.fuzzerCorpusFiles(fuzzer)
+ if err != nil {
+ return err
+ }
- remaining := transform.SliceToChan(wgslFiles)
+ log.Printf("%v: checking %v files...\n", fuzzer.name, len(files))
+
+ for _, file := range files {
+ jobs = append(jobs, job{file: file, exe: fuzzer.path})
+ }
+ }
+
+ remaining := transform.SliceToChan(jobs)
var pb *progressbar.ProgressBar
if term.CanUseAnsiEscapeSequences() {
@@ -165,26 +218,32 @@
var numDone uint32
routine := func() error {
- for file := range remaining {
+ for job := range remaining {
atomic.AddUint32(&numDone, 1)
if pb != nil {
pb.Update(progressbar.Status{
- Total: len(wgslFiles),
+ Total: len(jobs),
Segments: []progressbar.Segment{
{Count: int(atomic.LoadUint32(&numDone))},
},
})
}
- if out, err := exec.Command(t.wgslFuzzer, file).CombinedOutput(); err != nil {
- _, fuzzer := filepath.Split(t.wgslFuzzer)
- return fmt.Errorf("%v run with %v failed with %v\n\n%v", fuzzer, file, err, string(out))
+ args := []string{}
+ if t.dxc != "" {
+ args = append(args, "--dxc="+t.dxc)
+ }
+ args = append(args, job.file)
+
+ if out, err := exec.Command(job.exe, args...).CombinedOutput(); err != nil {
+ _, fuzzer := filepath.Split(job.exe)
+ return fmt.Errorf("%v run with %v failed with %v\n\n%v", fuzzer, job, err, string(out))
}
}
return nil
}
- if err = utils.RunConcurrent(t.numProcesses, routine); err != nil {
+ if err := utils.RunConcurrent(t.numProcesses, routine); err != nil {
return err
}
@@ -197,55 +256,105 @@
// New cases are written to t.out.
// Blocks until a fuzzer errors, or the process is interrupted.
func (t tool) run() error {
+ // Regular expression used to identify the crash file written by libfuzzer
+ var reCrashFile = regexp.MustCompile("crash-[a-z0-9]{40}")
+
ctx := utils.CancelOnInterruptContext(context.Background())
ctx, cancel := context.WithCancel(ctx)
defer cancel()
- dictPath, err := filepath.Abs(filepath.Join(fileutils.DawnRoot(), wgslDictionaryRelPath))
- if err != nil || !fileutils.IsFile(dictPath) {
- return fmt.Errorf("failed to obtain the dictionary.txt path: %w", err)
+ routinesPerFuzzer := t.numProcesses / len(t.fuzzers)
+ if routinesPerFuzzer == 0 {
+ routinesPerFuzzer = 1
}
- args := []string{t.out, t.corpus,
- "-dict=" + dictPath,
- }
- if t.verbose {
- args = append(args, "--verbose")
- }
- if t.filter != "" {
- args = append(args, "--filter="+t.filter)
- }
+ errs := make(chan error, 8)
- fmt.Println("running", t.numProcesses, "fuzzer instances")
- errs := make(chan error, t.numProcesses)
- for i := 0; i < t.numProcesses; i++ {
- go func() {
- cmd := exec.CommandContext(ctx, t.wgslFuzzer, args...)
- out := bytes.Buffer{}
- cmd.Stdout = &out
- cmd.Stderr = &out
- if t.verbose {
- cmd.Stdout = io.MultiWriter(&out, os.Stdout)
- cmd.Stderr = io.MultiWriter(&out, os.Stderr)
+ for _, fuzzer := range t.fuzzers {
+ fuzzer := fuzzer
+
+ corpusFiles, err := t.fuzzerCorpusFiles(fuzzer)
+ if err != nil {
+ return err
+ }
+
+ log.Println("copying", len(corpusFiles), fuzzer.ext, "files to", t.out+"...")
+ for _, path := range corpusFiles {
+ _, file := filepath.Split(path)
+ if err := fileutils.CopyFile(filepath.Join(t.out, file), path); err != nil {
+ return err
}
- if err := cmd.Run(); err != nil {
- if ctxErr := ctx.Err(); ctxErr != nil {
- errs <- ctxErr
- } else {
- _, fuzzer := filepath.Split(t.wgslFuzzer)
- errs <- fmt.Errorf("%v failed with %v\n\n%v", fuzzer, err, out.String())
+ }
+
+ args := []string{t.out}
+ if fuzzer.dict != "" {
+ args = append(args, "-dict="+fuzzer.dict)
+ }
+ if t.verbose {
+ args = append(args, "--verbose")
+ }
+ if t.filter != "" {
+ args = append(args, "--filter="+t.filter)
+ }
+ if t.dxc != "" {
+ args = append(args, "--dxc="+t.dxc)
+ }
+
+ log.Println("running", routinesPerFuzzer, fuzzer.name, "fuzzer instances...")
+ for i := 0; i < routinesPerFuzzer; i++ {
+ go func() {
+ for {
+ cmd := exec.CommandContext(ctx, fuzzer.path, args...)
+ out := bytes.Buffer{}
+ cmd.Stdout = &out
+ cmd.Stderr = &out
+ if t.verbose {
+ cmd.Stdout = io.MultiWriter(&out, os.Stdout)
+ cmd.Stderr = io.MultiWriter(&out, os.Stderr)
+ }
+ if err := cmd.Run(); err != nil {
+ if ctxErr := ctx.Err(); ctxErr != nil {
+ errs <- ctxErr
+ } else {
+ if t.securityOnly && isFailureNonSecurity(out.String()) {
+ log.Println("non-security crash found. restarting...")
+ if file := reCrashFile.FindString(out.String()); file != "" {
+ os.Remove(file)
+ }
+ continue
+ }
+ _, fuzzer := filepath.Split(fuzzer.ext)
+ errs <- fmt.Errorf("%v failed with %v\n\n%v", fuzzer, err, out.String())
+ }
+ } else {
+ errs <- fmt.Errorf("fuzzer unexpectedly terminated without error:\n%v", out.String())
+ }
+ break
}
- } else {
- errs <- fmt.Errorf("fuzzer unexpectedly terminated without error:\n%v", out.String())
- }
- }()
+ }()
+ }
}
+
for err := range errs {
return err
}
return nil
}
+func (t tool) fuzzerCorpusFiles(f fuzzerInfo) ([]string, error) {
+ files, err := glob.Glob(filepath.Join(t.corpus, "**"+f.ext))
+ if err != nil {
+ return nil, err
+ }
+
+ // Remove '*.expected.wgsl'
+ if f.name == "wgsl" {
+ files = transform.Filter(files, func(s string) bool { return !strings.Contains(s, ".expected.wgsl") })
+ }
+
+ return files, nil
+}
+
func defaultCorpusDir() string {
return filepath.Join(fileutils.DawnRoot(), "test/tint")
}
@@ -253,3 +362,26 @@
func defaultBuildDir() string {
return filepath.Join(fileutils.DawnRoot(), "out/active")
}
+
+func dxcFileName() string {
+ switch runtime.GOOS {
+ case "windows":
+ return "dxcompiler.dll"
+ case "darwin":
+ return "libdxcompiler.dylib"
+ default:
+ return "libdxcompiler.so"
+ }
+}
+
+func isFailureNonSecurity(out string) bool {
+ for _, str := range []string{
+ "AddressSanitizer: SEGV on unknown address 0x000000000000",
+ "ICE while running fuzzer",
+ } {
+ if strings.Contains(out, str) {
+ return true
+ }
+ }
+ return false
+}