// 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" "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) { switch cfg.Type { case "gce", "qemu", "gvisor": default: return nil, fmt.Errorf("test instances can only work with qemu/gce") } 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("make", "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, ) 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) 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 } return SetConfigImage(cfg, imageDir) } func SetConfigImage(cfg *mgrconfig.Config, imageDir string) 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 } if cfg.Type == "qemu" { kernel := filepath.Join(imageDir, "kernel") if !osutil.IsExist(kernel) { kernel = "" } initrd := filepath.Join(imageDir, "initrd") if !osutil.IsExist(initrd) { initrd = "" } if kernel != "" || initrd != "" { qemu := make(map[string]interface{}) if err := json.Unmarshal(cfg.VM, &qemu); err != nil { return fmt.Errorf("failed to parse qemu config: %v", err) } if kernel != "" { qemu["kernel"] = kernel } if initrd != "" { qemu["initrd"] = initrd } vmCfg, err := json.Marshal(qemu) if err != nil { return fmt.Errorf("failed to serialize qemu 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() // This linux-ism avoids detecting any crash during boot as "unexpected kernel reboot". output := testErr.Output if pos := bytes.Index(output, []byte("Booting the kernel.")); pos != -1 { output = output[pos+1:] } testErr.Report = inst.reporter.Parse(output) if testErr.Report != nil { testErr.Title = testErr.Report.Title } else { testErr.Report = &report.Report{ Title: testErr.Title, Output: testErr.Output, } } if err := inst.reporter.Symbolize(testErr.Report); err != nil { // TODO(dvyukov): send such errors to dashboard. log.Logf(0, "failed to symbolize report: %v", err) } } 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 := FuzzerCmd(fuzzerBin, executorBin, "test", inst.cfg.TargetOS, inst.cfg.TargetArch, fwdAddr, inst.cfg.Sandbox, 0, 0, false, false, true, false) outc, errc, err := inst.vm.Run(5*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, true) 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.Build(target, inst.reproC) if err != nil { return err } 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, true) 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 } return fmt.Sprintf("%v -executor=%v -name=%v -arch=%v%v -manager=%v -sandbox=%v"+ " -procs=%v -v=%d -cover=%v -debug=%v -test=%v -runtest=%v", fuzzer, executor, name, arch, osArg, fwdAddr, sandbox, procs, verbosity, cover, debug, test, runtest) } 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) }