Skip to content

Commit 8fabdce

Browse files
Jay ConrodFiloSottile
authored andcommitted
[dev.fuzz] internal/fuzz: coordinate fuzzing across workers
Package fuzz provides common fuzzing functionality for tests built with "go test" and for programs that use fuzzing functionality in the testing package. Change-Id: I3901c6a993a9adb8a93733ae1838b86dd78c7036 Reviewed-on: https://go-review.googlesource.com/c/go/+/259259 Run-TryBot: Jay Conrod <jayconrod@google.com> TryBot-Result: Go Bot <gobot@golang.org> Reviewed-by: Katie Hockman <katie@golang.org> Trust: Katie Hockman <katie@golang.org> Trust: Jay Conrod <jayconrod@google.com>
1 parent 0a6f004 commit 8fabdce

File tree

10 files changed

+704
-46
lines changed

10 files changed

+704
-46
lines changed

src/cmd/go/internal/test/flagdefs_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ func TestPassFlagToTestIncludesAllTestFlags(t *testing.T) {
1717
}
1818
name := strings.TrimPrefix(f.Name, "test.")
1919
switch name {
20-
case "testlogfile", "paniconexit0":
20+
case "testlogfile", "paniconexit0", "fuzzworker":
2121
// These are internal flags.
2222
default:
2323
if !passFlagToTest[name] {

src/cmd/go/internal/test/test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,6 @@ See the documentation of the testing package for more information.
467467
`,
468468
}
469469

470-
// TODO(katiehockman): complete the testing here
471470
var (
472471
testBench string // -bench flag
473472
testC bool // -c flag

src/go/build/deps_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,10 @@ var depsRules = `
467467
FMT, flag, runtime/debug, runtime/trace
468468
< testing;
469469
470-
internal/testlog, runtime/pprof, regexp
470+
FMT, encoding/json
471+
< internal/fuzz;
472+
473+
internal/fuzz, internal/testlog, runtime/pprof, regexp
471474
< testing/internal/testdeps;
472475
473476
OS, flag, testing, internal/cfg

src/internal/fuzz/fuzz.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// Copyright 2020 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+
// Package fuzz provides common fuzzing functionality for tests built with
6+
// "go test" and for programs that use fuzzing functionality in the testing
7+
// package.
8+
package fuzz
9+
10+
import (
11+
"os"
12+
"runtime"
13+
"sync"
14+
"time"
15+
)
16+
17+
// CoordinateFuzzing creates several worker processes and communicates with
18+
// them to test random inputs that could trigger crashes and expose bugs.
19+
// The worker processes run the same binary in the same directory with the
20+
// same environment variables as the coordinator process. Workers also run
21+
// with the same arguments as the coordinator, except with the -test.fuzzworker
22+
// flag prepended to the argument list.
23+
//
24+
// parallel is the number of worker processes to run in parallel. If parallel
25+
// is 0, CoordinateFuzzing will run GOMAXPROCS workers.
26+
//
27+
// seed is a list of seed values added by the fuzz target with testing.F.Add.
28+
// Seed values from testdata and GOFUZZCACHE should not be included in this
29+
// list; this function loads them separately.
30+
func CoordinateFuzzing(parallel int, seed [][]byte) error {
31+
if parallel == 0 {
32+
parallel = runtime.GOMAXPROCS(0)
33+
}
34+
// TODO(jayconrod): support fuzzing indefinitely or with a given duration.
35+
// The value below is just a placeholder until we figure out how to handle
36+
// interrupts.
37+
duration := 5 * time.Second
38+
39+
// TODO(jayconrod): do we want to support fuzzing different binaries?
40+
dir := "" // same as self
41+
binPath := os.Args[0]
42+
args := append([]string{"-test.fuzzworker"}, os.Args[1:]...)
43+
env := os.Environ() // same as self
44+
45+
c := &coordinator{
46+
doneC: make(chan struct{}),
47+
inputC: make(chan corpusEntry),
48+
}
49+
50+
newWorker := func() *worker {
51+
return &worker{
52+
dir: dir,
53+
binPath: binPath,
54+
args: args,
55+
env: env,
56+
coordinator: c,
57+
}
58+
}
59+
60+
corpus := corpus{entries: make([]corpusEntry, len(seed))}
61+
for i, v := range seed {
62+
corpus.entries[i].b = v
63+
}
64+
if len(corpus.entries) == 0 {
65+
// TODO(jayconrod,katiehockman): pick a good starting corpus when one is
66+
// missing or very small.
67+
corpus.entries = append(corpus.entries, corpusEntry{b: []byte{0}})
68+
}
69+
70+
// TODO(jayconrod,katiehockman): read corpus from testdata.
71+
// TODO(jayconrod,katiehockman): read corpus from GOFUZZCACHE.
72+
73+
// Start workers.
74+
workers := make([]*worker, parallel)
75+
runErrs := make([]error, parallel)
76+
var wg sync.WaitGroup
77+
wg.Add(parallel)
78+
for i := 0; i < parallel; i++ {
79+
go func(i int) {
80+
defer wg.Done()
81+
workers[i] = newWorker()
82+
runErrs[i] = workers[i].runFuzzing()
83+
}(i)
84+
}
85+
86+
// Main event loop.
87+
stopC := time.After(duration)
88+
i := 0
89+
for {
90+
select {
91+
// TODO(jayconrod): handle interruptions like SIGINT.
92+
// TODO(jayconrod,katiehockman): receive crashers and new corpus values
93+
// from workers.
94+
95+
case <-stopC:
96+
// Time's up.
97+
close(c.doneC)
98+
99+
case <-c.doneC:
100+
// Wait for workers to stop and return.
101+
wg.Wait()
102+
for _, err := range runErrs {
103+
if err != nil {
104+
return err
105+
}
106+
}
107+
return nil
108+
109+
case c.inputC <- corpus.entries[i]:
110+
// Sent the next input to any worker.
111+
// TODO(jayconrod,katiehockman): need a scheduling algorithm that chooses
112+
// which corpus value to send next (or generates something new).
113+
i = (i + 1) % len(corpus.entries)
114+
}
115+
}
116+
117+
// TODO(jayconrod,katiehockman): write crashers to testdata and other inputs
118+
// to GOFUZZCACHE. If the testdata directory is outside the current module,
119+
// always write to GOFUZZCACHE, since the testdata is likely read-only.
120+
}
121+
122+
type corpus struct {
123+
entries []corpusEntry
124+
}
125+
126+
// TODO(jayconrod,katiehockman): decide whether and how to unify this type
127+
// with the equivalent in testing.
128+
type corpusEntry struct {
129+
b []byte
130+
}
131+
132+
// coordinator holds channels that workers can use to communicate with
133+
// the coordinator.
134+
type coordinator struct {
135+
// doneC is closed to indicate fuzzing is done and workers should stop.
136+
// doneC may be closed due to a time limit expiring or a fatal error in
137+
// a worker.
138+
doneC chan struct{}
139+
140+
// inputC is sent values to fuzz by the coordinator. Any worker may receive
141+
// values from this channel.
142+
inputC chan corpusEntry
143+
}

src/internal/fuzz/sys_posix.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright 2020 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+
// +build !windows
6+
7+
package fuzz
8+
9+
import (
10+
"os"
11+
"os/exec"
12+
)
13+
14+
// setWorkerComm configures communciation channels on the cmd that will
15+
// run a worker process.
16+
func setWorkerComm(cmd *exec.Cmd, fuzzIn, fuzzOut *os.File) {
17+
cmd.ExtraFiles = []*os.File{fuzzIn, fuzzOut}
18+
}
19+
20+
// getWorkerComm returns communication channels in the worker process.
21+
func getWorkerComm() (fuzzIn, fuzzOut *os.File, err error) {
22+
fuzzIn = os.NewFile(3, "fuzz_in")
23+
fuzzOut = os.NewFile(4, "fuzz_out")
24+
return fuzzIn, fuzzOut, nil
25+
}

src/internal/fuzz/sys_windows.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright 2020 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+
// +build windows
6+
7+
package fuzz
8+
9+
import (
10+
"fmt"
11+
"os"
12+
"os/exec"
13+
"strconv"
14+
"strings"
15+
"syscall"
16+
)
17+
18+
// setWorkerComm configures communciation channels on the cmd that will
19+
// run a worker process.
20+
func setWorkerComm(cmd *exec.Cmd, fuzzIn, fuzzOut *os.File) {
21+
syscall.SetHandleInformation(syscall.Handle(fuzzIn.Fd()), syscall.HANDLE_FLAG_INHERIT, 1)
22+
syscall.SetHandleInformation(syscall.Handle(fuzzOut.Fd()), syscall.HANDLE_FLAG_INHERIT, 1)
23+
cmd.Env = append(cmd.Env, fmt.Sprintf("GO_TEST_FUZZ_WORKER_HANDLES=%x,%x", fuzzIn.Fd(), fuzzOut.Fd()))
24+
}
25+
26+
// getWorkerComm returns communication channels in the worker process.
27+
func getWorkerComm() (fuzzIn *os.File, fuzzOut *os.File, err error) {
28+
v := os.Getenv("GO_TEST_FUZZ_WORKER_HANDLES")
29+
if v == "" {
30+
return nil, nil, fmt.Errorf("GO_TEST_FUZZ_WORKER_HANDLES not set")
31+
}
32+
parts := strings.Split(v, ",")
33+
if len(parts) != 2 {
34+
return nil, nil, fmt.Errorf("GO_TEST_FUZZ_WORKER_HANDLES has invalid value")
35+
}
36+
base := 16
37+
bitSize := 64
38+
in, err := strconv.ParseInt(parts[0], base, bitSize)
39+
if err != nil {
40+
return nil, nil, fmt.Errorf("GO_TEST_FUZZ_WORKER_HANDLES has invalid value: %v", err)
41+
}
42+
out, err := strconv.ParseInt(parts[1], base, bitSize)
43+
if err != nil {
44+
return nil, nil, fmt.Errorf("GO_TEST_FUZZ_WORKER_HANDLES has invalid value: %v", err)
45+
}
46+
fuzzIn = os.NewFile(uintptr(in), "fuzz_in")
47+
fuzzOut = os.NewFile(uintptr(out), "fuzz_out")
48+
return fuzzIn, fuzzOut, nil
49+
}

0 commit comments

Comments
 (0)