diff options
| author | Aleksandr Nogikh <nogikh@google.com> | 2022-03-31 18:26:42 +0000 |
|---|---|---|
| committer | Aleksandr Nogikh <wp32pw@gmail.com> | 2022-04-29 17:16:33 +0200 |
| commit | a04ae3093de7eebc147770fe38a5bda96b5c0634 (patch) | |
| tree | 5d7062a73b354b5f1906847835e1c00804a572a5 | |
| parent | 7f2625f8905f7d981475fa314c3d06c56c7bd4b0 (diff) | |
all: use the same prog execution code throughout the project
Previously it was copypasted in pkg/instance, pkg/repro,
tools/syz-crash. Use the single implementation instead.
Also, this commit fixes a bug - the previous code always set collide to
true while reproducing a bug, which led to an immediate syz-exexprog's
exit. As a result, newer bugs with .syz repro only were never actually
reproduced on #syz test requests.
| -rw-r--r-- | pkg/instance/execprog.go | 159 | ||||
| -rw-r--r-- | pkg/instance/instance.go | 86 | ||||
| -rw-r--r-- | pkg/repro/repro.go | 177 | ||||
| -rw-r--r-- | tools/syz-crush/crush.go | 72 |
4 files changed, 256 insertions, 238 deletions
diff --git a/pkg/instance/execprog.go b/pkg/instance/execprog.go new file mode 100644 index 000000000..7610298a6 --- /dev/null +++ b/pkg/instance/execprog.go @@ -0,0 +1,159 @@ +// Copyright 2022 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 + +import ( + "fmt" + "os" + "time" + + "github.com/google/syzkaller/pkg/csource" + "github.com/google/syzkaller/pkg/mgrconfig" + "github.com/google/syzkaller/pkg/osutil" + "github.com/google/syzkaller/pkg/report" + "github.com/google/syzkaller/prog" + "github.com/google/syzkaller/vm" +) + +type ExecutorLogger func(int, string, ...interface{}) + +type OptionalConfig struct { + ExitCondition vm.ExitCondition + Logf ExecutorLogger + OldFlagsCompatMode bool +} + +type ExecProgInstance struct { + execprogBin string + executorBin string + reporter *report.Reporter + mgrCfg *mgrconfig.Config + VMInstance *vm.Instance + OptionalConfig +} + +type RunResult struct { + Report *report.Report +} + +func SetupExecProg(vmInst *vm.Instance, mgrCfg *mgrconfig.Config, reporter *report.Reporter, + opt *OptionalConfig) (*ExecProgInstance, error) { + execprogBin, err := vmInst.Copy(mgrCfg.ExecprogBin) + if err != nil { + vmInst.Close() + return nil, &TestError{Title: fmt.Sprintf("failed to copy syz-execprog to VM: %v", err)} + } + executorBin := mgrCfg.SysTarget.ExecutorBin + if executorBin == "" { + executorBin, err = vmInst.Copy(mgrCfg.ExecutorBin) + if err != nil { + vmInst.Close() + return nil, &TestError{Title: fmt.Sprintf("failed to copy syz-executor to VM: %v", err)} + } + } + ret := &ExecProgInstance{ + execprogBin: execprogBin, + executorBin: executorBin, + reporter: reporter, + mgrCfg: mgrCfg, + VMInstance: vmInst, + } + if opt != nil { + ret.OptionalConfig = *opt + } + if ret.Logf == nil { + ret.Logf = func(int, string, ...interface{}) {} + } + if ret.ExitCondition == 0 { + ret.ExitCondition = vm.ExitTimeout | vm.ExitNormal | vm.ExitError + } + return ret, nil +} + +func CreateExecProgInstance(vmPool *vm.Pool, vmIndex int, mgrCfg *mgrconfig.Config, + reporter *report.Reporter, opt *OptionalConfig) (*ExecProgInstance, error) { + vmInst, err := vmPool.Create(vmIndex) + if err != nil { + return nil, fmt.Errorf("failed to create VM: %v", err) + } + ret, err := SetupExecProg(vmInst, mgrCfg, reporter, opt) + if err != nil { + vmInst.Close() + return nil, err + } + return ret, nil +} + +func (inst *ExecProgInstance) runCommand(command string, duration time.Duration) (*RunResult, error) { + outc, errc, err := inst.VMInstance.Run(duration, nil, command) + if err != nil { + return nil, fmt.Errorf("failed to run command in VM: %v", err) + } + result := &RunResult{} + result.Report = inst.VMInstance.MonitorExecution(outc, errc, inst.reporter, inst.ExitCondition) + if result.Report == nil { + inst.Logf(2, "program did not crash") + } else { + if err := inst.reporter.Symbolize(result.Report); err != nil { + return nil, fmt.Errorf("failed to symbolize report: %v", err) + } + inst.Logf(2, "program crashed: %v", result.Report.Title) + } + return result, nil +} + +func (inst *ExecProgInstance) runBinary(bin string, duration time.Duration) (*RunResult, error) { + bin, err := inst.VMInstance.Copy(bin) + if err != nil { + return nil, &TestError{Title: fmt.Sprintf("failed to copy binary to VM: %v", err)} + } + return inst.runCommand(bin, duration) +} + +func (inst *ExecProgInstance) RunCProg(p *prog.Prog, duration time.Duration, + opts csource.Options) (*RunResult, error) { + src, err := csource.Write(p, opts) + if err != nil { + return nil, err + } + inst.Logf(2, "testing compiled C program (duration=%v, %+v): %s", duration, opts, p) + return inst.RunCProgRaw(src, p.Target, duration) +} + +func (inst *ExecProgInstance) RunCProgRaw(src []byte, target *prog.Target, + duration time.Duration) (*RunResult, error) { + bin, err := csource.BuildNoWarn(target, src) + if err != nil { + return nil, err + } + defer os.Remove(bin) + return inst.runBinary(bin, duration) +} + +func (inst *ExecProgInstance) RunSyzProgFile(progFile string, duration time.Duration, + opts csource.Options) (*RunResult, error) { + vmProgFile, err := inst.VMInstance.Copy(progFile) + if err != nil { + return nil, &TestError{Title: fmt.Sprintf("failed to copy prog to VM: %v", err)} + } + target := inst.mgrCfg.SysTarget + faultCall := -1 + if opts.Fault { + faultCall = opts.FaultCall + } + command := ExecprogCmd(inst.execprogBin, inst.executorBin, target.OS, target.Arch, opts.Sandbox, + opts.Repeat, opts.Threaded, opts.Collide, opts.Procs, faultCall, opts.FaultNth, + !inst.OldFlagsCompatMode, inst.mgrCfg.Timeouts.Slowdown, vmProgFile) + return inst.runCommand(command, duration) +} + +func (inst *ExecProgInstance) RunSyzProg(syzProg []byte, duration time.Duration, + opts csource.Options) (*RunResult, error) { + progFile, err := osutil.WriteTempFile(syzProg) + if err != nil { + return nil, err + } + defer os.Remove(progFile) + return inst.RunSyzProgFile(progFile, duration, opts) +} diff --git a/pkg/instance/instance.go b/pkg/instance/instance.go index a3a87d338..218408191 100644 --- a/pkg/instance/instance.go +++ b/pkg/instance/instance.go @@ -360,30 +360,22 @@ func (inst *inst) testInstance() error { } func (inst *inst) testRepro() error { - cfg := inst.cfg - if len(inst.reproSyz) > 0 { - execprogBin, err := inst.vm.Copy(cfg.ExecprogBin) - 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 := 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)} - } - } - 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)} + var err error + execProg, err := SetupExecProg(inst.vm, inst.cfg, inst.reporter, &OptionalConfig{ + OldFlagsCompatMode: !inst.optionalFlags, + }) + if err != nil { + return err + } + transformError := func(res *RunResult, err error) error { + if err != nil && res.Report != nil { + return &CrashError{Report: res.Report} } - opts, err := csource.DeserializeOptions(inst.reproOpts) + return err + } + if len(inst.reproSyz) > 0 { + var opts csource.Options + opts, err = csource.DeserializeOptions(inst.reproOpts) if err != nil { return err } @@ -394,47 +386,17 @@ func (inst *inst) testRepro() error { if opts.Sandbox == "" { opts.Sandbox = "none" } - if !opts.Fault { - opts.FaultCall = -1 - } - cmdSyz := ExecprogCmd(execprogBin, executorBin, cfg.TargetOS, cfg.TargetArch, opts.Sandbox, - true, true, opts.Collide, cfg.Procs, opts.FaultCall, opts.FaultNth, inst.optionalFlags, - cfg.Timeouts.Slowdown, vmProgFile) - if err := inst.testProgram(cmdSyz, cfg.Timeouts.NoOutputRunningTime); err != nil { - return err - } - } - if len(inst.reproC) == 0 { - return nil - } - bin, err := csource.BuildNoWarn(cfg.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 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. - return inst.testProgram(vmBin, cfg.Timeouts.NoOutput/2) -} - -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 + opts.Repeat, opts.Threaded = true, true + err = transformError(execProg.RunSyzProg(inst.reproSyz, + inst.cfg.Timeouts.NoOutputRunningTime, opts)) } - if err := inst.reporter.Symbolize(rep); err != nil { - log.Logf(0, "failed to symbolize report: %v", err) + 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. + err = transformError(execProg.RunCProgRaw(inst.reproC, inst.cfg.Target, + inst.cfg.Timeouts.NoOutput/2)) } - return &CrashError{Report: rep} + return err } type OptionalFuzzerArgs struct { diff --git a/pkg/repro/repro.go b/pkg/repro/repro.go index 8726cbfd3..fe20c7330 100644 --- a/pkg/repro/repro.go +++ b/pkg/repro/repro.go @@ -6,17 +6,15 @@ package repro import ( "bytes" "fmt" - "os" "sort" "sync" "time" "github.com/google/syzkaller/pkg/csource" "github.com/google/syzkaller/pkg/host" - instancePkg "github.com/google/syzkaller/pkg/instance" + "github.com/google/syzkaller/pkg/instance" "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/prog" "github.com/google/syzkaller/sys/targets" @@ -42,12 +40,17 @@ type Stats struct { SimplifyCTime time.Duration } +type reproInstance struct { + index int + execProg *instance.ExecProgInstance +} + type context struct { target *targets.Target reporter *report.Reporter crashTitle string crashType report.Type - instances chan *instance + instances chan *reproInstance bootRequests chan int testTimeouts []time.Duration startOpts csource.Options @@ -56,13 +59,6 @@ type context struct { timeouts targets.Timeouts } -type instance struct { - *vm.Instance - index int - execprogBin string - executorBin string -} - func Run(crashLog []byte, cfg *mgrconfig.Config, features *host.Features, reporter *report.Reporter, vmPool *vm.Pool, vmIndexes []int) (*Result, *Stats, error) { if len(vmIndexes) == 0 { @@ -103,7 +99,7 @@ func Run(crashLog []byte, cfg *mgrconfig.Config, features *host.Features, report reporter: reporter, crashTitle: crashTitle, crashType: crashType, - instances: make(chan *instance, len(vmIndexes)), + instances: make(chan *reproInstance, len(vmIndexes)), bootRequests: make(chan int, len(vmIndexes)), testTimeouts: testTimeouts, startOpts: createStartOptions(cfg, features, crashType), @@ -118,7 +114,7 @@ func Run(crashLog []byte, cfg *mgrconfig.Config, features *host.Features, report go func() { defer wg.Done() for vmIndex := range ctx.bootRequests { - var inst *instance + var inst *instance.ExecProgInstance maxTry := 3 for try := 0; try < maxTry; try++ { select { @@ -128,7 +124,8 @@ func Run(crashLog []byte, cfg *mgrconfig.Config, features *host.Features, report default: } var err error - inst, err = ctx.initInstance(cfg, vmPool, vmIndex) + inst, err = instance.CreateExecProgInstance(vmPool, vmIndex, cfg, + reporter, &instance.OptionalConfig{Logf: ctx.reproLogf}) if err != nil { ctx.reproLogf(0, "failed to init instance: %v", err) time.Sleep(10 * time.Second) @@ -139,7 +136,7 @@ func Run(crashLog []byte, cfg *mgrconfig.Config, features *host.Features, report if inst == nil { break } - ctx.instances <- inst + ctx.instances <- &reproInstance{execProg: inst, index: vmIndex} } }() } @@ -150,7 +147,7 @@ func Run(crashLog []byte, cfg *mgrconfig.Config, features *host.Features, report defer func() { close(ctx.bootRequests) for inst := range ctx.instances { - inst.Close() + inst.execProg.VMInstance.Close() } }() @@ -211,32 +208,6 @@ func createStartOptions(cfg *mgrconfig.Config, features *host.Features, crashTyp return opts } -func (ctx *context) initInstance(cfg *mgrconfig.Config, vmPool *vm.Pool, vmIndex int) (*instance, error) { - vmInst, err := vmPool.Create(vmIndex) - if err != nil { - return nil, fmt.Errorf("failed to create VM: %v", err) - } - execprogBin, err := vmInst.Copy(cfg.ExecprogBin) - if err != nil { - vmInst.Close() - return nil, fmt.Errorf("failed to copy to VM: %v", err) - } - executorBin := ctx.target.ExecutorBin - if executorBin == "" { - executorBin, err = vmInst.Copy(cfg.ExecutorBin) - if err != nil { - vmInst.Close() - return nil, fmt.Errorf("failed to copy to VM: %v", err) - } - } - return &instance{ - Instance: vmInst, - index: vmIndex, - execprogBin: execprogBin, - executorBin: executorBin, - }, nil -} - func (ctx *context) repro(entries []*prog.LogEntry, crashStart int) (*Result, error) { // Cut programs that were executed after crash. for i, ent := range entries { @@ -546,28 +517,47 @@ func (ctx *context) testProg(p *prog.Prog, duration time.Duration, opts csource. return ctx.testProgs([]*prog.LogEntry{&entry}, duration, opts) } -func (ctx *context) testProgs(entries []*prog.LogEntry, duration time.Duration, opts csource.Options) ( - crashed bool, err error) { +func (ctx *context) testWithInstance(callback func(inst *instance.ExecProgInstance) (rep *instance.RunResult, + err error)) (bool, error) { inst := <-ctx.instances if inst == nil { return false, fmt.Errorf("all VMs failed to boot") } defer ctx.returnInstance(inst) - if len(entries) == 0 { - return false, fmt.Errorf("no programs to execute") - } - - pstr := encodeEntries(entries) - progFile, err := osutil.WriteTempFile(pstr) + result, err := callback(inst.execProg) if err != nil { return false, err } - defer os.Remove(progFile) - vmProgFile, err := inst.Copy(progFile) - if err != nil { - return false, fmt.Errorf("failed to copy to VM: %v", err) + rep := result.Report + if rep == nil { + return false, nil + } + if rep.Suppressed { + ctx.reproLogf(2, "suppressed program crash: %v", rep.Title) + return false, nil + } + if ctx.crashType == report.MemoryLeak && rep.Type != report.MemoryLeak { + ctx.reproLogf(2, "not a leak crash: %v", rep.Title) + return false, nil + } + ctx.report = rep + return true, nil +} + +func encodeEntries(entries []*prog.LogEntry) []byte { + buf := new(bytes.Buffer) + for _, ent := range entries { + fmt.Fprintf(buf, "executing program %v:\n%v", ent.Proc, string(ent.P.Serialize())) } + return buf.Bytes() +} +func (ctx *context) testProgs(entries []*prog.LogEntry, duration time.Duration, opts csource.Options) ( + crashed bool, err error) { + if len(entries) == 0 { + return false, fmt.Errorf("no programs to execute") + } + pstr := encodeEntries(entries) program := entries[0].P.String() if len(entries) > 1 { program = "[" @@ -579,77 +569,22 @@ func (ctx *context) testProgs(entries []*prog.LogEntry, duration time.Duration, } program += "]" } - - command := instancePkg.ExecprogCmd(inst.execprogBin, inst.executorBin, - ctx.target.OS, ctx.target.Arch, opts.Sandbox, opts.Repeat, - opts.Threaded, opts.Collide, opts.Procs, -1, -1, true, ctx.timeouts.Slowdown, vmProgFile) ctx.reproLogf(2, "testing program (duration=%v, %+v): %s", duration, opts, program) ctx.reproLogf(3, "detailed listing:\n%s", pstr) - return ctx.testImpl(inst.Instance, command, duration) + return ctx.testWithInstance(func(inst *instance.ExecProgInstance) (*instance.RunResult, error) { + return inst.RunSyzProg(pstr, duration, opts) + }) } func (ctx *context) testCProg(p *prog.Prog, duration time.Duration, opts csource.Options) (crashed bool, err error) { - src, err := csource.Write(p, opts) - if err != nil { - return false, err - } - bin, err := csource.BuildNoWarn(p.Target, src) - if err != nil { - return false, err - } - defer os.Remove(bin) - ctx.reproLogf(2, "testing compiled C program (duration=%v, %+v): %s", duration, opts, p) - crashed, err = ctx.testBin(bin, duration) - if err != nil { - return false, err - } - return crashed, nil -} - -func (ctx *context) testBin(bin string, duration time.Duration) (crashed bool, err error) { - inst := <-ctx.instances - if inst == nil { - return false, fmt.Errorf("all VMs failed to boot") - } - defer ctx.returnInstance(inst) - - bin, err = inst.Copy(bin) - if err != nil { - return false, fmt.Errorf("failed to copy to VM: %v", err) - } - return ctx.testImpl(inst.Instance, bin, duration) -} - -func (ctx *context) testImpl(inst *vm.Instance, command string, duration time.Duration) (crashed bool, err error) { - outc, errc, err := inst.Run(duration, nil, command) - if err != nil { - return false, fmt.Errorf("failed to run command in VM: %v", err) - } - rep := inst.MonitorExecution(outc, errc, ctx.reporter, - vm.ExitTimeout|vm.ExitNormal|vm.ExitError) - if rep == nil { - ctx.reproLogf(2, "program did not crash") - return false, nil - } - if err := ctx.reporter.Symbolize(rep); err != nil { - return false, fmt.Errorf("failed to symbolize report: %v", err) - } - if rep.Suppressed { - ctx.reproLogf(2, "suppressed program crash: %v", rep.Title) - return false, nil - } - if ctx.crashType == report.MemoryLeak && rep.Type != report.MemoryLeak { - ctx.reproLogf(2, "not a leak crash: %v", rep.Title) - return false, nil - } - ctx.report = rep - ctx.reproLogf(2, "program crashed: %v", rep.Title) - return true, nil + return ctx.testWithInstance(func(inst *instance.ExecProgInstance) (*instance.RunResult, error) { + return inst.RunCProg(p, duration, opts) + }) } -func (ctx *context) returnInstance(inst *instance) { +func (ctx *context) returnInstance(inst *reproInstance) { ctx.bootRequests <- inst.index - inst.Close() + inst.execProg.VMInstance.Close() } func (ctx *context) reproLogf(level int, format string, args ...interface{}) { @@ -775,14 +710,6 @@ func chunksToStr(chunks [][]*prog.LogEntry) string { return log } -func encodeEntries(entries []*prog.LogEntry) []byte { - buf := new(bytes.Buffer) - for _, ent := range entries { - fmt.Fprintf(buf, "executing program %v:\n%v", ent.Proc, string(ent.P.Serialize())) - } - return buf.Bytes() -} - type Simplify func(opts *csource.Options) bool var progSimplifies = []Simplify{ diff --git a/tools/syz-crush/crush.go b/tools/syz-crush/crush.go index 1c571a153..ac077befd 100644 --- a/tools/syz-crush/crush.go +++ b/tools/syz-crush/crush.go @@ -78,23 +78,10 @@ func main() { runType := LogFile if strings.HasSuffix(reproduceMe, ".c") { runType = CProg - } - if runType == CProg { - execprog, err := ioutil.ReadFile(reproduceMe) - if err != nil { - log.Fatalf("error reading source file from '%s'", reproduceMe) - } - - cfg.ExecprogBin, err = csource.BuildNoWarn(cfg.Target, execprog) - if err != nil { - log.Fatalf("failed to build source file: %v", err) - } - - log.Printf("compiled csource %v to cprog: %v", reproduceMe, cfg.ExecprogBin) + log.Printf("reproducing from C source file: %v", reproduceMe) } else { log.Printf("reproducing from log file: %v", reproduceMe) } - log.Printf("booting %v test machines...", vmPool.Count()) runDone := make(chan *report.Report) var shutdown, stoppedWorkers uint32 @@ -169,54 +156,37 @@ func storeCrash(cfg *mgrconfig.Config, rep *report.Report) { func runInstance(cfg *mgrconfig.Config, reporter *report.Reporter, vmPool *vm.Pool, index int, timeout time.Duration, runType FileType) *report.Report { log.Printf("vm-%v: starting", index) - inst, err := vmPool.Create(index) - if err != nil { - log.Printf("failed to create instance: %v", err) - return nil + optArgs := &instance.OptionalConfig{ + ExitCondition: vm.ExitTimeout, } - defer inst.Close() - - execprogBin, err := inst.Copy(cfg.ExecprogBin) + var err error + inst, err := instance.CreateExecProgInstance(vmPool, index, cfg, reporter, optArgs) if err != nil { - log.Printf("failed to copy execprog: %v", err) + log.Printf("failed to set up instance: %v", err) return nil } - - cmd := "" + defer inst.VMInstance.Close() + file := flag.Args()[0] + var res *instance.RunResult if runType == LogFile { - // If SyzExecutorCmd is provided, it means that syz-executor is already in - // the image, so no need to copy it. - executorBin := cfg.SysTarget.ExecutorBin - if executorBin == "" { - executorBin, err = inst.Copy(cfg.ExecutorBin) - if err != nil { - log.Printf("failed to copy executor: %v", err) - return nil - } - } - logFile, err := inst.Copy(flag.Args()[0]) + opts := csource.DefaultOpts(cfg) + opts.Repeat, opts.Threaded = true, true + res, err = inst.RunSyzProgFile(file, timeout, opts) + } else { + var src []byte + src, err = ioutil.ReadFile(file) if err != nil { - log.Printf("failed to copy log: %v", err) - return nil + log.Fatalf("error reading source file from '%s'", file) } - - cmd = instance.ExecprogCmd(execprogBin, executorBin, cfg.TargetOS, cfg.TargetArch, cfg.Sandbox, - true, true, true, cfg.Procs, -1, -1, true, cfg.Timeouts.Slowdown, logFile) - } else { - cmd = execprogBin + res, err = inst.RunCProgRaw(src, cfg.Target, timeout) } - - outc, errc, err := inst.Run(timeout, nil, cmd) if err != nil { - log.Printf("failed to run execprog: %v", err) + log.Printf("failed to execute program: %v", err) return nil } - - log.Printf("vm-%v: crushing...", index) - rep := inst.MonitorExecution(outc, errc, reporter, vm.ExitTimeout) - if rep != nil { - log.Printf("vm-%v: crash: %v", index, rep.Title) - return rep + if res.Report != nil { + log.Printf("vm-%v: crash: %v", index, res.Report.Title) + return res.Report } log.Printf("vm-%v: running long enough, stopping", index) return nil |
