// Copyright 2020 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build darwin || freebsd || linux package fuzz import ( "fmt" "os" "os/exec" "syscall" ) type sharedMemSys struct{} func sharedMemMapFile(f *os.File, size int, removeOnClose bool) (*sharedMem, error) { prot := syscall.PROT_READ | syscall.PROT_WRITE flags := syscall.MAP_FILE | syscall.MAP_SHARED region, err := syscall.Mmap(int(f.Fd()), 0, size, prot, flags) if err != nil { return nil, err } return &sharedMem{f: f, region: region, removeOnClose: removeOnClose}, nil } // Close unmaps the shared memory and closes the temporary file. If this // sharedMem was created with sharedMemTempFile, Close also removes the file. func (m *sharedMem) Close() error { // Attempt all operations, even if we get an error for an earlier operation. // os.File.Close may fail due to I/O errors, but we still want to delete // the temporary file. var errs []error errs = append(errs, syscall.Munmap(m.region), m.f.Close()) if m.removeOnClose { errs = append(errs, os.Remove(m.f.Name())) } for _, err := range errs { if err != nil { return err } } return nil } // setWorkerComm configures communication channels on the cmd that will // run a worker process. func setWorkerComm(cmd *exec.Cmd, comm workerComm) { mem := <-comm.memMu memFile := mem.f comm.memMu <- mem cmd.ExtraFiles = []*os.File{comm.fuzzIn, comm.fuzzOut, memFile} } // getWorkerComm returns communication channels in the worker process. func getWorkerComm() (comm workerComm, err error) { fuzzIn := os.NewFile(3, "fuzz_in") fuzzOut := os.NewFile(4, "fuzz_out") memFile := os.NewFile(5, "fuzz_mem") fi, err := memFile.Stat() if err != nil { return workerComm{}, err } size := int(fi.Size()) if int64(size) != fi.Size() { return workerComm{}, fmt.Errorf("fuzz temp file exceeds maximum size") } removeOnClose := false mem, err := sharedMemMapFile(memFile, size, removeOnClose) if err != nil { return workerComm{}, err } memMu := make(chan *sharedMem, 1) memMu <- mem return workerComm{fuzzIn: fuzzIn, fuzzOut: fuzzOut, memMu: memMu}, nil } // isInterruptError returns whether an error was returned by a process that // was terminated by an interrupt signal (SIGINT). func isInterruptError(err error) bool { exitErr, ok := err.(*exec.ExitError) if !ok || exitErr.ExitCode() >= 0 { return false } status := exitErr.Sys().(syscall.WaitStatus) return status.Signal() == syscall.SIGINT } // terminationSignal checks if err is an exec.ExitError with a signal status. // If it is, terminationSignal returns the signal and true. // If not, -1 and false. func terminationSignal(err error) (os.Signal, bool) { exitErr, ok := err.(*exec.ExitError) if !ok || exitErr.ExitCode() >= 0 { return syscall.Signal(-1), false } status := exitErr.Sys().(syscall.WaitStatus) return status.Signal(), status.Signaled() } // isCrashSignal returns whether a signal was likely to have been caused by an // error in the program that received it, triggered by a fuzz input. For // example, SIGSEGV would be received after a nil pointer dereference. // Other signals like SIGKILL or SIGHUP are more likely to have been sent by // another process, and we shouldn't record a crasher if the worker process // receives one of these. // // Note that Go installs its own signal handlers on startup, so some of these // signals may only be received if signal handlers are changed. For example, // SIGSEGV is normally transformed into a panic that causes the process to exit // with status 2 if not recovered, which we handle as a crash. func isCrashSignal(signal os.Signal) bool { switch signal { case syscall.SIGILL, // illegal instruction syscall.SIGTRAP, // breakpoint syscall.SIGABRT, // abort() called syscall.SIGBUS, // invalid memory access (e.g., misaligned address) syscall.SIGFPE, // math error, e.g., integer divide by zero syscall.SIGSEGV, // invalid memory access (e.g., write to read-only) syscall.SIGPIPE: // sent data to closed pipe or socket return true default: return false } }