"os"
"os/exec"
"runtime"
+ "sync"
"time"
)
// TODO(jayconrod): if we get an error here, something failed between
// main and the call to testing.F.Fuzz. The error here won't
// be useful. Collect stderr, clean it up, and return that.
+ // TODO(jayconrod): we can get EPIPE if w.stop is called concurrently
+ // and it kills the worker process. Suppress this message in
+ // that case.
// TODO(jayconrod): what happens if testing.F.Fuzz is never called?
// TODO(jayconrod): time out if the test process hangs.
fmt.Fprintf(os.Stderr, "communicating with worker: %v\n", err)
if err != nil {
return err
}
- srv := &workerServer{workerComm: comm, fn: fn}
+ srv := &workerServer{workerComm: comm, fuzzFn: fn}
return srv.serve()
}
Err string
}
-// workerComm holds objects needed for the worker client and server
-// to communicate.
+// workerComm holds pipes and shared memory used for communication
+// between the coordinator process (client) and a worker process (server).
type workerComm struct {
fuzzIn, fuzzOut *os.File
mem *sharedMem
}
-// workerServer is a minimalist RPC server, run in fuzz worker processes.
+// workerServer is a minimalist RPC server, run by fuzz worker processes.
+// It allows the coordinator process (using workerClient) to call methods in a
+// worker process. This system allows the coordinator to run multiple worker
+// processes in parallel and to collect inputs that caused crashes from shared
+// memory after a worker process terminates unexpectedly.
type workerServer struct {
workerComm
- fn func([]byte) error
+
+ // fuzzFn runs the worker's fuzz function on the given input and returns
+ // an error if it finds a crasher (the process may also exit or crash).
+ fuzzFn func([]byte) error
}
-// serve deserializes and executes RPCs on a given pair of pipes.
+// serve reads serialized RPC messages on fuzzIn. When serve receives a message,
+// it calls the corresponding method, then sends the serialized result back
+// on fuzzOut.
+//
+// serve handles RPC calls synchronously; it will not attempt to read a message
+// until the previous call has finished.
//
-// serve returns errors communicating over the pipes. It does not return
-// errors from methods; those are passed through response values.
+// serve returns errors that occurred when communicating over pipes. serve
+// does not return errors from method calls; those are passed through serialized
+// responses.
func (ws *workerServer) serve() error {
enc := json.NewEncoder(ws.fuzzOut)
dec := json.NewDecoder(ws.fuzzIn)
return fuzzResponse{}
default:
b := mutate(value)
- if err := ws.fn(b); err != nil {
+ if err := ws.fuzzFn(b); err != nil {
return fuzzResponse{Crasher: b, Err: err.Error()}
}
// TODO(jayconrod,katiehockman): return early if coverage is expanded
}
}
-// workerClient is a minimalist RPC client, run in the fuzz coordinator.
+// workerClient is a minimalist RPC client. The coordinator process uses a
+// workerClient to call methods in each worker process (handled by
+// workerServer).
type workerClient struct {
workerComm
+
+ mu sync.Mutex
enc *json.Encoder
dec *json.Decoder
}
// closing fuzz_in. Close drains fuzz_out (avoiding a SIGPIPE in the worker),
// and closes it after the worker process closes the other end.
func (wc *workerClient) Close() error {
+ wc.mu.Lock()
+ defer wc.mu.Unlock()
+
// Close fuzzIn. This signals to the server that there are no more calls,
// and it should exit.
if err := wc.fuzzIn.Close(); err != nil {
// fuzz tells the worker to call the fuzz method. See workerServer.fuzz.
func (wc *workerClient) fuzz(value []byte, args fuzzArgs) (fuzzResponse, error) {
+ wc.mu.Lock()
+ defer wc.mu.Unlock()
+
wc.mem.setValue(value)
c := call{Fuzz: &args}
if err := wc.enc.Encode(c); err != nil {