blob: e62f9190033960883bd311dae59d439331ebe333 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package lib
import (
"bufio"
"fmt"
"io"
"regexp"
"strconv"
"strings"
"github.com/golang/glog"
)
// A Fuzzer represents a fuzzer present on an instance.
type Fuzzer struct {
build Build
// Name is `package/binary`
Name string
pkg string
cmx string
url string
args []string
options map[string]string
}
var expectedFuzzerReturnCodes = [...]int{0, 1, 77}
// NewFuzzer constructs a fuzzer object with the given pkg/fuzzer name
func NewFuzzer(build Build, pkg, fuzzer string) *Fuzzer {
return &Fuzzer{
build: build,
Name: fmt.Sprintf("%s/%s", pkg, fuzzer),
pkg: pkg,
cmx: fmt.Sprintf("%s.cmx", fuzzer),
url: fmt.Sprintf("fuchsia-pkg://fuchsia.com/%s#meta/%s.cmx", pkg, fuzzer),
}
}
// AbsPath returns the absolute target path for a given relative path in a
// fuzzer package. The path may differ depending on whether it is identified as
// a resource, data, or neither.
func (f *Fuzzer) AbsPath(relpath string) string {
if strings.HasPrefix(relpath, "/") {
return relpath
} else if strings.HasPrefix(relpath, "pkg/") {
return fmt.Sprintf("/pkgfs/packages/%s/0/%s", f.pkg, relpath[4:])
} else if strings.HasPrefix(relpath, "data/") {
return fmt.Sprintf("/data/r/sys/fuchsia.com:%s:0#meta:%s/%s", f.pkg, f.cmx, relpath[5:])
} else {
return fmt.Sprintf("/%s", relpath)
}
}
// Parse command line arguments for the fuzzer. For '-key=val' style options,
// the last 'val' for a given 'key' is used.
func (f *Fuzzer) Parse(args []string) {
f.options = make(map[string]string)
re := regexp.MustCompile(`^-([^-=\s]*)=([^-=\s]*)$`)
for _, arg := range args {
submatch := re.FindStringSubmatch(arg)
if submatch == nil {
f.args = append(f.args, arg)
} else {
f.options[submatch[1]] = submatch[2]
}
}
}
func scanFuzzerOutput(conn Connector, out io.WriteCloser, in io.Reader) (chan error,
chan []string) {
scanErr := make(chan error, 1)
artifactsCh := make(chan []string, 1)
go func() {
defer out.Close() // Propagate the EOF, so the symbolizer terminates properly
artifacts := []string{}
// mutRegex detects output from
// MutationDispatcher::PrintMutationSequence
// (compiler-rt/lib/fuzzer/FuzzerMutate.cpp), which itself is called
// from Fuzzer::DumpCurrentUnit (compiler-rt/lib/fuzzer/FuzzerLoop.cpp)
// as part of exit/crash callbacks
mutRegex := regexp.MustCompile(`^MS: [0-9]*`)
pidRegex := regexp.MustCompile(`^==([0-9]+)==`)
artifactRegex := regexp.MustCompile(`Test unit written to (\S*)`)
sawMut := false
sawPid := false
scanner := bufio.NewScanner(in)
pid := 0
for scanner.Scan() {
line := scanner.Text()
if m := pidRegex.FindStringSubmatch(line); m != nil {
if sawPid {
glog.Warningf("Saw multiple PIDs")
}
sawPid = true
pid, _ = strconv.Atoi(m[1]) // guaranteed parseable due to regex
glog.Infof("Found fuzzer PID: %d", pid)
}
if m := artifactRegex.FindStringSubmatch(line); m != nil {
glog.Infof("Found artifact: %s", m[1])
artifacts = append(artifacts, m[1])
}
if mutRegex.MatchString(line) {
if sawMut {
glog.Warningf("Saw multiple mutation sequences")
}
sawMut = true
glog.Infof("Found mutation sequence: %s", line)
if pid == 0 {
glog.Warningf("WARNING: failed to fetch syslog: missing pid")
// Include this warning inline so it is visible in fuzzer logs
fmt.Fprintf(out, "WARNING: failed to fetch syslog: missing pid\n")
} else {
log, err := conn.GetSysLog(pid)
if err != nil {
glog.Warningf("WARNING: failed to fetch syslog: %s", err)
// Include this warning inline so it is visible in fuzzer logs
fmt.Fprintf(out, "WARNING: failed to fetch syslog: %s\n", err)
} else {
io.WriteString(out, log+"\n")
}
}
}
io.WriteString(out, line+"\n")
}
scanErr <- scanner.Err()
artifactsCh <- artifacts
}()
return scanErr, artifactsCh
}
// Run the fuzzer, sending symbolized output to out and copying any referenced artifacts (e.g.
// crashes) to the artifactDir.
func (f *Fuzzer) Run(conn Connector, out io.Writer, artifactDir string) error {
if f.options == nil {
return fmt.Errorf("Run called on Fuzzer before Parse")
}
cmdline := []string{f.url}
for k, v := range f.options {
cmdline = append(cmdline, fmt.Sprintf("-%s=%s", k, v))
}
for _, arg := range f.args {
cmdline = append(cmdline, arg)
}
cmd := conn.Command("run", cmdline...)
fuzzerOutput, err := cmd.StdoutPipe()
if err != nil {
return err
}
if err := cmd.Start(); err != nil {
return err
}
fromScanner, toSymbolizer := io.Pipe()
// Start goroutine to scan for artifact names and PID
scanErr, artifactsCh := scanFuzzerOutput(conn, toSymbolizer, fuzzerOutput)
// Start symbolizer goroutine, connected to the output
symErr := make(chan error, 1)
go func(in io.Reader) {
symErr <- f.build.Symbolize(in, out)
}(fromScanner)
err = cmd.Wait()
glog.Infof("Fuzzer run has completed")
// Check the returncode (though this might be modified
// by libfuzzer args and thus be unreliable)
if cmderr, ok := err.(*InstanceCmdError); ok {
found := false
for _, code := range expectedFuzzerReturnCodes {
if cmderr.ReturnCode == code {
found = true
break
}
}
if !found {
return fmt.Errorf("unexpected return code: %s", err)
}
} else if err != nil {
return fmt.Errorf("failed during wait: %s", err)
}
// Check for any errors in the goroutines
if err := <-symErr; err != nil {
return fmt.Errorf("failed during symbolization: %s", err)
}
if err := <-scanErr; err != nil {
return fmt.Errorf("failed during scanning: %s", err)
}
// TODO(fxb/47370): implement this, possibly elsewhere
for _, artifact := range <-artifactsCh {
glog.Infof("should fetch artifact %s", f.AbsPath(artifact))
}
return nil
}