syzkaller/pkg/instance/instance.go
Dmitry Vyukov a963689197 pkg/log: rename -v to -vv
To avoid conflicts with programs that import both syzkaller packages
and github.com/golang/glog which also defines -v flag.
2019-07-16 11:18:12 +02:00

445 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright 2018 syzkaller project authors. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
// Package instance provides helper functions for creation of temporal instances
// used for testing of images, patches and bisection.
package instance
import (
"bytes"
"encoding/json"
"fmt"
"net"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/google/syzkaller/pkg/build"
"github.com/google/syzkaller/pkg/csource"
"github.com/google/syzkaller/pkg/log"
"github.com/google/syzkaller/pkg/mgrconfig"
"github.com/google/syzkaller/pkg/osutil"
"github.com/google/syzkaller/pkg/report"
"github.com/google/syzkaller/pkg/vcs"
"github.com/google/syzkaller/prog"
"github.com/google/syzkaller/vm"
)
type Env struct {
cfg *mgrconfig.Config
}
func NewEnv(cfg *mgrconfig.Config) (*Env, error) {
if !vm.AllowsOvercommit(cfg.Type) {
return nil, fmt.Errorf("test instances are not supported for %v VMs", cfg.Type)
}
if cfg.Workdir == "" {
return nil, fmt.Errorf("workdir path is empty")
}
if cfg.KernelSrc == "" {
return nil, fmt.Errorf("kernel src path is empty")
}
if cfg.Syzkaller == "" {
return nil, fmt.Errorf("syzkaller path is empty")
}
if err := osutil.MkdirAll(cfg.Workdir); err != nil {
return nil, fmt.Errorf("failed to create tmp dir: %v", err)
}
env := &Env{
cfg: cfg,
}
return env, nil
}
func (env *Env) BuildSyzkaller(repo, commit string) error {
cfg := env.cfg
srcIndex := strings.LastIndex(cfg.Syzkaller, "/src/")
if srcIndex == -1 {
return fmt.Errorf("syzkaller path %q is not in GOPATH", cfg.Syzkaller)
}
if _, err := vcs.NewSyzkallerRepo(cfg.Syzkaller).CheckoutCommit(repo, commit); err != nil {
return fmt.Errorf("failed to checkout syzkaller repo: %v", err)
}
cmd := osutil.Command(MakeBin, "target")
cmd.Dir = cfg.Syzkaller
cmd.Env = append([]string{}, os.Environ()...)
cmd.Env = append(cmd.Env,
"GOPATH="+cfg.Syzkaller[:srcIndex],
"TARGETOS="+cfg.TargetOS,
"TARGETVMARCH="+cfg.TargetVMArch,
"TARGETARCH="+cfg.TargetArch,
// Since we can be building very old revisions for bisection here,
// make the build as permissive as possible.
// Newer compilers tend to produce more warnings also kernel headers may be broken, e.g.:
// ebtables.h:197:19: error: invalid conversion from void* to ebt_entry_target*
"CFLAGS=-fpermissive -w",
)
if _, err := osutil.Run(time.Hour, cmd); err != nil {
return fmt.Errorf("syzkaller build failed: %v", err)
}
return nil
}
func (env *Env) BuildKernel(compilerBin, userspaceDir, cmdlineFile, sysctlFile string,
kernelConfig []byte) (string, error) {
cfg := env.cfg
imageDir := filepath.Join(cfg.Workdir, "image")
if err := build.Image(cfg.TargetOS, cfg.TargetVMArch, cfg.Type,
cfg.KernelSrc, imageDir, compilerBin, userspaceDir,
cmdlineFile, sysctlFile, kernelConfig); err != nil {
return "", err
}
if err := SetConfigImage(cfg, imageDir, true); err != nil {
return "", err
}
kernelConfigFile := filepath.Join(imageDir, "kernel.config")
if !osutil.IsExist(kernelConfigFile) {
kernelConfigFile = ""
}
return kernelConfigFile, nil
}
func SetConfigImage(cfg *mgrconfig.Config, imageDir string, reliable bool) error {
cfg.KernelObj = filepath.Join(imageDir, "obj")
cfg.Image = filepath.Join(imageDir, "image")
if keyFile := filepath.Join(imageDir, "key"); osutil.IsExist(keyFile) {
cfg.SSHKey = keyFile
}
vmConfig := make(map[string]interface{})
if err := json.Unmarshal(cfg.VM, &vmConfig); err != nil {
return fmt.Errorf("failed to parse VM config: %v", err)
}
if cfg.Type == "qemu" || cfg.Type == "vmm" {
if kernel := filepath.Join(imageDir, "kernel"); osutil.IsExist(kernel) {
vmConfig["kernel"] = kernel
}
if initrd := filepath.Join(imageDir, "initrd"); osutil.IsExist(initrd) {
vmConfig["initrd"] = initrd
}
}
if cfg.Type == "gce" {
// Don't use preemptible VMs for image testing, patch testing and bisection.
vmConfig["preemptible"] = !reliable
}
vmCfg, err := json.Marshal(vmConfig)
if err != nil {
return fmt.Errorf("failed to serialize VM config: %v", err)
}
cfg.VM = vmCfg
return nil
}
func OverrideVMCount(cfg *mgrconfig.Config, n int) error {
vmConfig := make(map[string]interface{})
if err := json.Unmarshal(cfg.VM, &vmConfig); err != nil {
return fmt.Errorf("failed to parse VM config: %v", err)
}
if vmConfig["count"] == nil || !vm.AllowsOvercommit(cfg.Type) {
return nil
}
vmConfig["count"] = n
vmCfg, err := json.Marshal(vmConfig)
if err != nil {
return fmt.Errorf("failed to serialize VM config: %v", err)
}
cfg.VM = vmCfg
return nil
}
type TestError struct {
Boot bool // says if the error happened during booting or during instance testing
Title string
Output []byte
Report *report.Report
}
func (err *TestError) Error() string {
return err.Title
}
type CrashError struct {
Report *report.Report
}
func (err *CrashError) Error() string {
return err.Report.Title
}
// Test boots numVMs VMs, tests basic kernel operation, and optionally tests the provided reproducer.
// TestError is returned if there is a problem with kernel/image (crash, reboot loop, etc).
// CrashError is returned if the reproducer crashes kernel.
func (env *Env) Test(numVMs int, reproSyz, reproOpts, reproC []byte) ([]error, error) {
if err := mgrconfig.Complete(env.cfg); err != nil {
return nil, err
}
reporter, err := report.NewReporter(env.cfg)
if err != nil {
return nil, err
}
vmPool, err := vm.Create(env.cfg, false)
if err != nil {
return nil, fmt.Errorf("failed to create VM pool: %v", err)
}
if n := vmPool.Count(); numVMs > n {
numVMs = n
}
res := make(chan error, numVMs)
for i := 0; i < numVMs; i++ {
inst := &inst{
cfg: env.cfg,
reporter: reporter,
vmPool: vmPool,
vmIndex: i,
reproSyz: reproSyz,
reproOpts: reproOpts,
reproC: reproC,
}
go func() { res <- inst.test() }()
}
var errors []error
for i := 0; i < numVMs; i++ {
errors = append(errors, <-res)
}
return errors, nil
}
type inst struct {
cfg *mgrconfig.Config
reporter report.Reporter
vmPool *vm.Pool
vm *vm.Instance
vmIndex int
reproSyz []byte
reproOpts []byte
reproC []byte
}
func (inst *inst) test() error {
vmInst, err := inst.vmPool.Create(inst.vmIndex)
if err != nil {
testErr := &TestError{
Boot: true,
Title: err.Error(),
}
if bootErr, ok := err.(vm.BootErrorer); ok {
testErr.Title, testErr.Output = bootErr.BootError()
rep := inst.reporter.Parse(testErr.Output)
if rep != nil && rep.Type == report.UnexpectedReboot {
// Avoid detecting any boot crash as "unexpected kernel reboot".
output := testErr.Output[rep.EndPos:]
if pos := bytes.IndexByte(testErr.Output[rep.StartPos:], '\n'); pos != -1 {
output = testErr.Output[rep.StartPos+pos:]
}
rep = inst.reporter.Parse(output)
}
if rep == nil {
rep = &report.Report{
Title: testErr.Title,
Output: testErr.Output,
}
}
if err := inst.reporter.Symbolize(rep); err != nil {
// TODO(dvyukov): send such errors to dashboard.
log.Logf(0, "failed to symbolize report: %v", err)
}
testErr.Report = rep
testErr.Title = rep.Title
}
return testErr
}
defer vmInst.Close()
inst.vm = vmInst
if err := inst.testInstance(); err != nil {
return err
}
if len(inst.reproSyz) != 0 {
if err := inst.testRepro(); err != nil {
return err
}
}
return nil
}
// testInstance tests basic operation of the provided VM
// (that we can copy binaries, run binaries, they can connect to host, run syzkaller programs, etc).
// TestError is returned if there is a problem with the kernel (e.g. crash).
func (inst *inst) testInstance() error {
ln, err := net.Listen("tcp", ":")
if err != nil {
return fmt.Errorf("failed to open listening socket: %v", err)
}
defer ln.Close()
acceptErr := make(chan error, 1)
go func() {
conn, err := ln.Accept()
if err == nil {
conn.Close()
}
acceptErr <- err
}()
fwdAddr, err := inst.vm.Forward(ln.Addr().(*net.TCPAddr).Port)
if err != nil {
return fmt.Errorf("failed to setup port forwarding: %v", err)
}
fuzzerBin, err := inst.vm.Copy(inst.cfg.SyzFuzzerBin)
if err != nil {
return &TestError{Title: fmt.Sprintf("failed to copy test binary to VM: %v", err)}
}
executorBin, err := inst.vm.Copy(inst.cfg.SyzExecutorBin)
if err != nil {
return &TestError{Title: fmt.Sprintf("failed to copy test binary to VM: %v", err)}
}
cmd := OldFuzzerCmd(fuzzerBin, executorBin, "test", inst.cfg.TargetOS, inst.cfg.TargetArch, fwdAddr,
inst.cfg.Sandbox, 0, inst.cfg.Cover, true)
outc, errc, err := inst.vm.Run(10*time.Minute, nil, cmd)
if err != nil {
return fmt.Errorf("failed to run binary in VM: %v", err)
}
rep := inst.vm.MonitorExecution(outc, errc, inst.reporter, vm.ExitNormal)
if rep != nil {
if err := inst.reporter.Symbolize(rep); err != nil {
// TODO(dvyukov): send such errors to dashboard.
log.Logf(0, "failed to symbolize report: %v", err)
}
return &TestError{
Title: rep.Title,
Report: rep,
}
}
select {
case err := <-acceptErr:
return err
case <-time.After(10 * time.Second):
return fmt.Errorf("test machine failed to connect to host")
}
}
func (inst *inst) testRepro() error {
cfg := inst.cfg
execprogBin, err := inst.vm.Copy(cfg.SyzExecprogBin)
if err != nil {
return &TestError{Title: fmt.Sprintf("failed to copy test binary to VM: %v", err)}
}
executorBin, err := inst.vm.Copy(cfg.SyzExecutorBin)
if err != nil {
return &TestError{Title: fmt.Sprintf("failed to copy test binary to VM: %v", err)}
}
progFile := filepath.Join(cfg.Workdir, "repro.prog")
if err := osutil.WriteFile(progFile, inst.reproSyz); err != nil {
return fmt.Errorf("failed to write temp file: %v", err)
}
vmProgFile, err := inst.vm.Copy(progFile)
if err != nil {
return &TestError{Title: fmt.Sprintf("failed to copy test binary to VM: %v", err)}
}
opts, err := csource.DeserializeOptions(inst.reproOpts)
if err != nil {
return err
}
// Combine repro options and default options in a way that increases chances to reproduce the crash.
// First, we always enable threaded/collide as it should be [almost] strictly better.
// Executor does not support empty sandbox, so we use none instead.
// Finally, always use repeat and multiple procs.
if opts.Sandbox == "" {
opts.Sandbox = "none"
}
if !opts.Fault {
opts.FaultCall = -1
}
cmdSyz := ExecprogCmd(execprogBin, executorBin, cfg.TargetOS, cfg.TargetArch, opts.Sandbox,
true, true, true, cfg.Procs, opts.FaultCall, opts.FaultNth, vmProgFile)
if err := inst.testProgram(cmdSyz, 7*time.Minute); err != nil {
return err
}
if len(inst.reproC) == 0 {
return nil
}
target, err := prog.GetTarget(cfg.TargetOS, cfg.TargetArch)
if err != nil {
return err
}
bin, err := csource.BuildNoWarn(target, inst.reproC)
if err != nil {
return err
}
defer os.Remove(bin)
vmBin, err := inst.vm.Copy(bin)
if err != nil {
return &TestError{Title: fmt.Sprintf("failed to copy test binary to VM: %v", err)}
}
// We should test for longer (e.g. 5 mins), but the problem is that
// reproducer does not print anything, so after 3 mins we detect "no output".
return inst.testProgram(vmBin, time.Minute)
}
func (inst *inst) testProgram(command string, testTime time.Duration) error {
outc, errc, err := inst.vm.Run(testTime, nil, command)
if err != nil {
return fmt.Errorf("failed to run binary in VM: %v", err)
}
rep := inst.vm.MonitorExecution(outc, errc, inst.reporter,
vm.ExitTimeout|vm.ExitNormal|vm.ExitError)
if rep == nil {
return nil
}
if err := inst.reporter.Symbolize(rep); err != nil {
log.Logf(0, "failed to symbolize report: %v", err)
}
return &CrashError{Report: rep}
}
func FuzzerCmd(fuzzer, executor, name, OS, arch, fwdAddr, sandbox string, procs, verbosity int,
cover, debug, test, runtest bool) string {
osArg := ""
if OS == "akaros" {
// Only akaros needs OS, because the rest assume host OS.
// But speciying OS for all OSes breaks patch testing on syzbot
// because old execprog does not have os flag.
osArg = " -os=" + OS
}
runtestArg := ""
if runtest {
runtestArg = " -runtest"
}
verbosityArg := ""
if verbosity != 0 {
verbosityArg = fmt.Sprintf(" -vv=%v", verbosity)
}
return fmt.Sprintf("%v -executor=%v -name=%v -arch=%v%v -manager=%v -sandbox=%v"+
" -procs=%v -cover=%v -debug=%v -test=%v%v%v",
fuzzer, executor, name, arch, osArg, fwdAddr, sandbox,
procs, cover, debug, test, runtestArg, verbosityArg)
}
func OldFuzzerCmd(fuzzer, executor, name, OS, arch, fwdAddr, sandbox string, procs int, cover, test bool) string {
return FuzzerCmd(fuzzer, executor, name, OS, arch, fwdAddr, sandbox, procs, 0, cover, false, test, false)
}
func ExecprogCmd(execprog, executor, OS, arch, sandbox string, repeat, threaded, collide bool,
procs, faultCall, faultNth int, progFile string) string {
repeatCount := 1
if repeat {
repeatCount = 0
}
osArg := ""
if OS == "akaros" {
osArg = " -os=" + OS
}
return fmt.Sprintf("%v -executor=%v -arch=%v%v -sandbox=%v"+
" -procs=%v -repeat=%v -threaded=%v -collide=%v -cover=0"+
" -fault_call=%v -fault_nth=%v %v",
execprog, executor, arch, osArg, sandbox,
procs, repeatCount, threaded, collide,
faultCall, faultNth, progFile)
}
var MakeBin = func() string {
if runtime.GOOS == "freebsd" || runtime.GOOS == "openbsd" {
return "gmake"
}
return "make"
}()