// 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 ( "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/report/crash" "github.com/google/syzkaller/pkg/tool" "github.com/google/syzkaller/pkg/vcs" "github.com/google/syzkaller/sys/targets" "github.com/google/syzkaller/vm" ) type Env interface { BuildSyzkaller(string, string) (string, error) BuildKernel(*BuildKernelConfig) (string, build.ImageDetails, error) Test(numVMs int, reproSyz, reproOpts, reproC []byte) ([]EnvTestResult, error) } type env struct { cfg *mgrconfig.Config optionalFlags bool buildSem *Semaphore testSem *Semaphore } type BuildKernelConfig struct { CompilerBin string LinkerBin string CcacheBin string UserspaceDir string CmdlineFile string SysctlFile string KernelConfig []byte } func NewEnv(cfg *mgrconfig.Config, buildSem, testSem *Semaphore) (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, optionalFlags: true, buildSem: buildSem, testSem: testSem, } return env, nil } func (env *env) BuildSyzkaller(repoURL, commit string) (string, error) { if env.buildSem != nil { env.buildSem.Wait() defer env.buildSem.Signal() } cfg := env.cfg srcIndex := strings.LastIndex(cfg.Syzkaller, "/src/") if srcIndex == -1 { return "", fmt.Errorf("syzkaller path %q is not in GOPATH", cfg.Syzkaller) } repo := vcs.NewSyzkallerRepo(cfg.Syzkaller) if _, err := repo.CheckoutCommit(repoURL, commit); err != nil { return "", fmt.Errorf("failed to checkout syzkaller repo: %v", err) } // The following commit ("syz-fuzzer: support optional flags") adds support for optional flags // in syz-fuzzer and syz-execprog. This is required to invoke older binaries with newer flags // without failing due to unknown flags. optionalFlags, err := repo.Contains("64435345f0891706a7e0c7885f5f7487581e6005") if err != nil { return "", fmt.Errorf("optional flags check failed: %v", err) } env.optionalFlags = optionalFlags cmd := osutil.Command(MakeBin, "target") cmd.Dir = cfg.Syzkaller goEnvOptions := []string{ "GOPATH=" + cfg.Syzkaller[:srcIndex], "GO111MODULE=auto", } cmd.Env = append(append([]string{}, os.Environ()...), goEnvOptions...) cmd.Env = append(cmd.Env, "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", ) // We collect the potentially useful debug info here unconditionally, because we will // only figure out later whether we actually need it (e.g. if the patch testing fails). goEnvCmd := osutil.Command("go", "env") goEnvCmd.Dir = cfg.Syzkaller goEnvCmd.Env = append(append([]string{}, os.Environ()...), goEnvOptions...) goEnvOut, goEnvErr := osutil.Run(time.Hour, goEnvCmd) gitStatusOut, gitStatusErr := osutil.RunCmd(time.Hour, cfg.Syzkaller, "git", "status") // Compile syzkaller. buildOutput, buildErr := osutil.Run(time.Hour, cmd) buildLog := fmt.Sprintf("go env (err=%v)\n%s\ngit status (err=%v)\n%s\n\n%s", goEnvErr, goEnvOut, gitStatusErr, gitStatusOut, buildOutput) if buildErr != nil { return buildLog, fmt.Errorf("syzkaller build failed: %v\n%s", buildErr, buildLog) } return buildLog, nil } func (env *env) BuildKernel(buildCfg *BuildKernelConfig) ( string, build.ImageDetails, error) { if env.buildSem != nil { env.buildSem.Wait() defer env.buildSem.Signal() } imageDir := filepath.Join(env.cfg.Workdir, "image") params := build.Params{ TargetOS: env.cfg.TargetOS, TargetArch: env.cfg.TargetVMArch, VMType: env.cfg.Type, KernelDir: env.cfg.KernelSrc, OutputDir: imageDir, Compiler: buildCfg.CompilerBin, Linker: buildCfg.LinkerBin, Ccache: buildCfg.CcacheBin, UserspaceDir: buildCfg.UserspaceDir, CmdlineFile: buildCfg.CmdlineFile, SysctlFile: buildCfg.SysctlFile, Config: buildCfg.KernelConfig, } details, err := build.Image(params) if err != nil { return "", details, err } if err := SetConfigImage(env.cfg, imageDir, true); err != nil { return "", details, err } kernelConfigFile := filepath.Join(imageDir, "kernel.config") if !osutil.IsExist(kernelConfigFile) { kernelConfigFile = "" } return kernelConfigFile, details, 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 Infra bool // whether the problem is related to some infrastructure problems 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) ([]EnvTestResult, error) { if env.testSem != nil { env.testSem.Wait() defer env.testSem.Signal() } 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 EnvTestResult, numVMs) for i := 0; i < numVMs; i++ { inst := &inst{ cfg: env.cfg, optionalFlags: env.optionalFlags, reporter: reporter, vmPool: vmPool, vmIndex: i, reproSyz: reproSyz, reproOpts: reproOpts, reproC: reproC, } go func() { res <- inst.test() }() } var ret []EnvTestResult for i := 0; i < numVMs; i++ { ret = append(ret, <-res) } return ret, nil } type inst struct { cfg *mgrconfig.Config optionalFlags bool reporter *report.Reporter vmPool *vm.Pool vm *vm.Instance vmIndex int reproSyz []byte reproOpts []byte reproC []byte } type EnvTestResult struct { Error error RawOutput []byte } func (inst *inst) test() EnvTestResult { vmInst, err := inst.vmPool.Create(inst.vmIndex) if err != nil { testErr := &TestError{ Boot: true, Title: err.Error(), } ret := EnvTestResult{ Error: testErr, } if bootErr, ok := err.(vm.BootErrorer); ok { testErr.Title, testErr.Output = bootErr.BootError() ret.RawOutput = testErr.Output rep := inst.reporter.Parse(testErr.Output) if rep != nil && rep.Type == crash.UnexpectedReboot { // Avoid detecting any boot crash as "unexpected kernel reboot". rep = inst.reporter.ParseFrom(testErr.Output, rep.SkipPos) } 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 } else { testErr.Infra = true if infraErr, ok := err.(vm.InfraErrorer); ok { // In case there's more info available. testErr.Title, testErr.Output = infraErr.InfraError() } } return ret } defer vmInst.Close() inst.vm = vmInst ret := EnvTestResult{} if ret.Error = inst.testInstance(); ret.Error != nil { return ret } if len(inst.reproSyz) != 0 || len(inst.reproC) != 0 { ret.RawOutput, ret.Error = inst.testRepro() } return ret } // 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.FuzzerBin) if err != nil { return &TestError{Title: fmt.Sprintf("failed to copy test binary to VM: %v", err)} } // If ExecutorBin is provided, it means that syz-executor is already in the image, // so no need to copy it. executorBin := inst.cfg.SysTarget.ExecutorBin if executorBin == "" { executorBin, err = inst.vm.Copy(inst.cfg.ExecutorBin) if err != nil { return &TestError{Title: fmt.Sprintf("failed to copy test binary to VM: %v", err)} } } cmd := OldFuzzerCmd(fuzzerBin, executorBin, targets.TestOS, inst.cfg.TargetOS, inst.cfg.TargetArch, fwdAddr, inst.cfg.Sandbox, inst.cfg.SandboxArg, 0, inst.cfg.Cover, true, inst.optionalFlags, inst.cfg.Timeouts.Slowdown) outc, errc, err := inst.vm.Run(10*time.Minute*inst.cfg.Timeouts.Scale, 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() ([]byte, error) { var err error execProg, err := SetupExecProg(inst.vm, inst.cfg, inst.reporter, &OptionalConfig{ OldFlagsCompatMode: !inst.optionalFlags, }) if err != nil { return nil, err } transformError := func(res *RunResult, err error) ([]byte, error) { if err != nil { return nil, err } if res != nil && res.Report != nil { return res.RawOutput, &CrashError{Report: res.Report} } return res.RawOutput, nil } out := []byte{} if len(inst.reproSyz) > 0 { var opts csource.Options opts, err = csource.DeserializeOptions(inst.reproOpts) if err != nil { return nil, 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" } opts.Repeat, opts.Threaded = true, true out, err = transformError(execProg.RunSyzProg(inst.reproSyz, inst.cfg.Timeouts.NoOutputRunningTime, opts)) } if err == nil && len(inst.reproC) > 0 { // We should test for more than full "no output" timeout, but the problem is that C reproducers // don't print anything, so we will get a false "no output" crash. out, err = transformError(execProg.RunCProgRaw(inst.reproC, inst.cfg.Target, inst.cfg.Timeouts.NoOutput/2)) } return out, err } type OptionalFuzzerArgs struct { Slowdown int RawCover bool SandboxArg int } type FuzzerCmdArgs struct { Fuzzer string Executor string Name string OS string Arch string FwdAddr string Sandbox string Procs int Verbosity int Cover bool Debug bool Test bool Runtest bool Optional *OptionalFuzzerArgs } func FuzzerCmd(args *FuzzerCmdArgs) string { osArg := "" if targets.Get(args.OS, args.Arch).HostFuzzer { // Only these OSes need the flag, 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=" + args.OS } runtestArg := "" if args.Runtest { runtestArg = " -runtest" } verbosityArg := "" if args.Verbosity != 0 { verbosityArg = fmt.Sprintf(" -vv=%v", args.Verbosity) } optionalArg := "" if args.Optional != nil { flags := []tool.Flag{ {Name: "slowdown", Value: fmt.Sprint(args.Optional.Slowdown)}, {Name: "raw_cover", Value: fmt.Sprint(args.Optional.RawCover)}, {Name: "sandbox_arg", Value: fmt.Sprint(args.Optional.SandboxArg)}, } optionalArg = " " + tool.OptionalFlags(flags) } 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%v", args.Fuzzer, args.Executor, args.Name, args.Arch, osArg, args.FwdAddr, args.Sandbox, args.Procs, args.Cover, args.Debug, args.Test, runtestArg, verbosityArg, optionalArg) } func OldFuzzerCmd(fuzzer, executor, name, OS, arch, fwdAddr, sandbox string, sandboxArg, procs int, cover, test, optionalFlags bool, slowdown int) string { var optional *OptionalFuzzerArgs if optionalFlags { optional = &OptionalFuzzerArgs{Slowdown: slowdown, SandboxArg: sandboxArg} } return FuzzerCmd(&FuzzerCmdArgs{Fuzzer: fuzzer, Executor: executor, Name: name, OS: OS, Arch: arch, FwdAddr: fwdAddr, Sandbox: sandbox, Procs: procs, Verbosity: 0, Cover: cover, Debug: false, Test: test, Runtest: false, Optional: optional}) } func ExecprogCmd(execprog, executor, OS, arch, sandbox string, sandboxArg int, repeat, threaded, collide bool, procs, faultCall, faultNth int, optionalFlags bool, slowdown int, progFile string) string { repeatCount := 1 if repeat { repeatCount = 0 } osArg := "" if targets.Get(OS, arch).HostFuzzer { osArg = " -os=" + OS } optionalArg := "" if faultCall >= 0 { optionalArg = fmt.Sprintf(" -fault_call=%v -fault_nth=%v", faultCall, faultNth) } if optionalFlags { optionalArg += " " + tool.OptionalFlags([]tool.Flag{ {Name: "slowdown", Value: fmt.Sprint(slowdown)}, {Name: "sandboxArg", Value: fmt.Sprint(sandboxArg)}, }) } return fmt.Sprintf("%v -executor=%v -arch=%v%v -sandbox=%v"+ " -procs=%v -repeat=%v -threaded=%v -collide=%v -cover=0%v %v", execprog, executor, arch, osArg, sandbox, procs, repeatCount, threaded, collide, optionalArg, progFile) } var MakeBin = func() string { if runtime.GOOS == targets.FreeBSD || runtime.GOOS == targets.OpenBSD { return "gmake" } return "make" }() func RunnerCmd(prog, fwdAddr, os, arch string, poolIdx, vmIdx int, threaded, newEnv bool) string { return fmt.Sprintf("%s -addr=%s -os=%s -arch=%s -pool=%d -vm=%d "+ "-threaded=%t -new-env=%t", prog, fwdAddr, os, arch, poolIdx, vmIdx, threaded, newEnv) } type Semaphore struct { ch chan struct{} } func NewSemaphore(count int) *Semaphore { s := &Semaphore{ ch: make(chan struct{}, count), } for i := 0; i < count; i++ { s.Signal() } return s } func (s *Semaphore) Wait() { <-s.ch } func (s *Semaphore) WaitC() <-chan struct{} { return s.ch } func (s *Semaphore) Available() int { return len(s.ch) } func (s *Semaphore) Signal() { if av := s.Available(); av == cap(s.ch) { // Not super reliable, but let it be here just in case. panic(fmt.Sprintf("semaphore capacity (%d) is exceeded (%d)", cap(s.ch), av)) } s.ch <- struct{}{} }