diff options
| -rw-r--r-- | fuzzer/fuzzer.go | 8 | ||||
| -rw-r--r-- | manager/html.go | 77 | ||||
| -rw-r--r-- | manager/manager.go | 62 | ||||
| -rw-r--r-- | prog/analysis.go | 27 | ||||
| -rw-r--r-- | prog/generation.go | 8 | ||||
| -rw-r--r-- | prog/mutation.go | 12 | ||||
| -rw-r--r-- | prog/prio.go | 274 | ||||
| -rw-r--r-- | prog/rand.go | 29 | ||||
| -rw-r--r-- | rpctype/rpctype.go | 1 | ||||
| -rw-r--r-- | sys/decl.go | 4 | ||||
| -rw-r--r-- | tools/stress/stress.go | 2 |
11 files changed, 430 insertions, 74 deletions
diff --git a/fuzzer/fuzzer.go b/fuzzer/fuzzer.go index a10250d01..3c74ee7f7 100644 --- a/fuzzer/fuzzer.go +++ b/fuzzer/fuzzer.go @@ -63,6 +63,7 @@ var ( corpusHashes map[Sig]struct{} triage []Input manager *rpc.Client + ct *prog.ChoiceTable workerIn = make(chan *prog.Prog, 10) workerOut = make(chan []Input, 10) @@ -98,6 +99,7 @@ func main() { if err := manager.Call("Manager.Connect", a, r); err != nil { panic(err) } + ct = prog.BuildChoiceTable(r.Prios, calls) if *flagParallel <= 0 { *flagParallel = 1 @@ -179,19 +181,19 @@ func main() { // Send new inputs to workers, if they need some. for len(workerIn) < *flagParallel { if len(corpus) == 0 || i%10 == 0 { - p := prog.Generate(rnd, programLength, calls) + p := prog.Generate(rnd, programLength, ct) logf(1, "#%v: generated: %s", i, p) workerIn <- p pending++ p = p.Clone() - p.Mutate(rnd, programLength, calls) + p.Mutate(rnd, programLength, ct) logf(1, "#%v: mutated: %s", i, p) workerIn <- p pending++ } else { inp := corpus[rnd.Intn(len(corpus))] p := inp.p.Clone() - p.Mutate(rs, programLength, calls) + p.Mutate(rs, programLength, ct) logf(1, "#%v: mutated: %s <- %s", i, p, inp.p) workerIn <- p pending++ diff --git a/manager/html.go b/manager/html.go index 242492e38..782fa1953 100644 --- a/manager/html.go +++ b/manager/html.go @@ -14,8 +14,21 @@ import ( "github.com/google/syzkaller/cover" "github.com/google/syzkaller/prog" + "github.com/google/syzkaller/sys" ) +func (mgr *Manager) initHttp() { + http.HandleFunc("/", mgr.httpInfo) + http.HandleFunc("/corpus", mgr.httpCorpus) + http.HandleFunc("/cover", mgr.httpCover) + http.HandleFunc("/prio", mgr.httpPrio) + http.HandleFunc("/current_corpus", mgr.httpCurrentCorpus) + go func() { + logf(0, "serving http on http://%v", mgr.cfg.Http) + panic(http.ListenAndServe(mgr.cfg.Http, nil)) + }() +} + func (mgr *Manager) httpInfo(w http.ResponseWriter, r *http.Request) { mgr.mu.Lock() defer mgr.mu.Unlock() @@ -39,6 +52,7 @@ func (mgr *Manager) httpInfo(w http.ResponseWriter, r *http.Request) { MasterHttp: mgr.masterHttp, MasterCorpusSize: len(mgr.masterCorpus), CorpusSize: len(mgr.corpus), + TriageQueue: len(mgr.candidates), } var cov cover.Cover @@ -103,6 +117,35 @@ func (mgr *Manager) httpCover(w http.ResponseWriter, r *http.Request) { } } +func (mgr *Manager) httpPrio(w http.ResponseWriter, r *http.Request) { + mgr.mu.Lock() + defer mgr.mu.Unlock() + + mgr.minimizeCorpus() + call := r.FormValue("call") + idx := -1 + for i, c := range sys.Calls { + if c.CallName == call { + idx = i + break + } + } + if idx == -1 { + http.Error(w, fmt.Sprintf("unknown call: %v", call), http.StatusInternalServerError) + return + } + + data := &UIPrioData{Call: call} + for i, p := range mgr.prios[idx] { + data.Prios = append(data.Prios, UIPrio{sys.Calls[i].Name, p}) + } + sort.Sort(UIPrioArray(data.Prios)) + + if err := prioTemplate.Execute(w, data); err != nil { + http.Error(w, fmt.Sprintf("failed to execute template: %v", err), http.StatusInternalServerError) + } +} + func (mgr *Manager) httpCurrentCorpus(w http.ResponseWriter, r *http.Request) { mgr.mu.Lock() defer mgr.mu.Unlock() @@ -126,6 +169,7 @@ type UIData struct { MasterHttp string MasterCorpusSize int CorpusSize int + TriageQueue int CoverSize int Calls []UICallType } @@ -166,10 +210,11 @@ var htmlTemplate = template.Must(template.New("").Parse(` Manager: {{.Name}} <a href='http://{{.MasterHttp}}'>[master]</a> <br> Master corpus: {{.MasterCorpusSize}} <br> Corpus: {{.CorpusSize}}<br> +Triage queue len: {{.TriageQueue}}<br> <a href='/cover'>Cover: {{.CoverSize}}</a> <br> <br> {{range $c := $.Calls}} - {{$c.Name}} <a href='/corpus?call={{$c.Name}}'>inputs:{{$c.Inputs}}</a> <a href='/cover?call={{$c.Name}}'>cover:{{$c.Cover}}</a><br> + {{$c.Name}} <a href='/corpus?call={{$c.Name}}'>inputs:{{$c.Inputs}}</a> <a href='/cover?call={{$c.Name}}'>cover:{{$c.Cover}}</a> <a href='/prio?call={{$c.Name}}'>prio</a> <br> {{end}} </body></html> `)) @@ -186,3 +231,33 @@ var corpusTemplate = template.Must(template.New("").Parse(` {{end}} </body></html> `)) + +type UIPrioData struct { + Call string + Prios []UIPrio +} + +type UIPrio struct { + Call string + Prio float32 +} + +type UIPrioArray []UIPrio + +func (a UIPrioArray) Len() int { return len(a) } +func (a UIPrioArray) Less(i, j int) bool { return a[i].Prio > a[j].Prio } +func (a UIPrioArray) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +var prioTemplate = template.Must(template.New("").Parse(` +<!doctype html> +<html> +<head> + <title>syzkaller priorities</title> +</head> +<body> +Priorities for {{$.Call}} <br> <br> +{{range $p := $.Prios}} + {{printf "%.4f\t%s" $p.Prio $p.Call}} <br> +{{end}} +</body></html> +`)) diff --git a/manager/manager.go b/manager/manager.go index 0c6841db0..eb1ca8a3b 100644 --- a/manager/manager.go +++ b/manager/manager.go @@ -7,7 +7,6 @@ import ( "crypto/sha1" "fmt" "net" - "net/http" "net/rpc" "sync" "time" @@ -39,6 +38,7 @@ type Manager struct { corpus []RpcInput corpusCover []cover.Cover + prios [][]float32 fuzzers map[string]*Fuzzer } @@ -72,14 +72,8 @@ func RunManager(cfg *Config, syscalls map[int]bool, instances []vm.Instance) { fuzzers: make(map[string]*Fuzzer), } - http.HandleFunc("/", mgr.httpInfo) - http.HandleFunc("/corpus", mgr.httpCorpus) - http.HandleFunc("/cover", mgr.httpCover) - http.HandleFunc("/current_corpus", mgr.httpCurrentCorpus) - go func() { - logf(0, "serving http on http://%v", cfg.Http) - panic(http.ListenAndServe(cfg.Http, nil)) - }() + // Create HTTP server. + mgr.initHttp() // Create RPC server for fuzzers. rpcAddr := fmt.Sprintf("localhost:%v", cfg.Port) @@ -148,30 +142,38 @@ func (mgr *Manager) pollMaster() { } func (mgr *Manager) minimizeCorpus() { - if len(mgr.corpus) == 0 { - return - } - // First, sort corpus per call. - type Call struct { - inputs []RpcInput - cov []cover.Cover + if len(mgr.corpus) != 0 { + // First, sort corpus per call. + type Call struct { + inputs []RpcInput + cov []cover.Cover + } + calls := make(map[string]Call) + for _, inp := range mgr.corpus { + c := calls[inp.Call] + c.inputs = append(c.inputs, inp) + c.cov = append(c.cov, inp.Cover) + calls[inp.Call] = c + } + // Now minimize and build new corpus. + var newCorpus []RpcInput + for _, c := range calls { + for _, idx := range cover.Minimize(c.cov) { + newCorpus = append(newCorpus, c.inputs[idx]) + } + } + logf(1, "minimized corpus: %v -> %v", len(mgr.corpus), len(newCorpus)) + mgr.corpus = newCorpus } - calls := make(map[string]Call) + var corpus []*prog.Prog for _, inp := range mgr.corpus { - c := calls[inp.Call] - c.inputs = append(c.inputs, inp) - c.cov = append(c.cov, inp.Cover) - calls[inp.Call] = c - } - // Now minimize and build new corpus. - var newCorpus []RpcInput - for _, c := range calls { - for _, idx := range cover.Minimize(c.cov) { - newCorpus = append(newCorpus, c.inputs[idx]) + p, err := prog.Deserialize(inp.Prog) + if err != nil { + panic(err) } + corpus = append(corpus, p) } - logf(1, "minimized corpus: %v -> %v", len(mgr.corpus), len(newCorpus)) - mgr.corpus = newCorpus + mgr.prios = prog.CalculatePriorities(corpus) } func (mgr *Manager) Connect(a *ManagerConnectArgs, r *ManagerConnectRes) error { @@ -184,6 +186,8 @@ func (mgr *Manager) Connect(a *ManagerConnectArgs, r *ManagerConnectRes) error { name: a.Name, input: 0, } + r.Prios = mgr.prios + return nil } diff --git a/prog/analysis.go b/prog/analysis.go index a0b411c89..43d0aa076 100644 --- a/prog/analysis.go +++ b/prog/analysis.go @@ -19,16 +19,16 @@ const ( ) type state struct { - enabledCalls []*sys.Call - files map[string]bool - resources map[sys.ResourceKind]map[sys.ResourceSubkind][]*Arg - strings map[string]bool - pages [maxPages]bool + ct *ChoiceTable + files map[string]bool + resources map[sys.ResourceKind]map[sys.ResourceSubkind][]*Arg + strings map[string]bool + pages [maxPages]bool } // analyze analyzes the program p up to but not including call c. -func analyze(enabledCalls []*sys.Call, p *Prog, c *Call) *state { - s := newState(enabledCalls) +func analyze(ct *ChoiceTable, p *Prog, c *Call) *state { + s := newState(ct) for _, c1 := range p.Calls { if c1 == c { break @@ -38,15 +38,12 @@ func analyze(enabledCalls []*sys.Call, p *Prog, c *Call) *state { return s } -func newState(enabledCalls []*sys.Call) *state { +func newState(ct *ChoiceTable) *state { s := &state{ - enabledCalls: enabledCalls, - files: make(map[string]bool), - resources: make(map[sys.ResourceKind]map[sys.ResourceSubkind][]*Arg), - strings: make(map[string]bool), - } - if len(s.enabledCalls) == 0 { - s.enabledCalls = sys.Calls + ct: ct, + files: make(map[string]bool), + resources: make(map[sys.ResourceKind]map[sys.ResourceSubkind][]*Arg), + strings: make(map[string]bool), } return s } diff --git a/prog/generation.go b/prog/generation.go index 43e527aa0..b08e98d31 100644 --- a/prog/generation.go +++ b/prog/generation.go @@ -5,18 +5,16 @@ package prog import ( "math/rand" - - "github.com/google/syzkaller/sys" ) // Generate generates a random program of length ~ncalls. // calls is a set of allowed syscalls, if nil all syscalls are used. -func Generate(rs rand.Source, ncalls int, enabledCalls []*sys.Call) *Prog { +func Generate(rs rand.Source, ncalls int, ct *ChoiceTable) *Prog { p := new(Prog) r := newRand(rs) - s := newState(enabledCalls) + s := newState(ct) for len(p.Calls) < ncalls { - calls := r.generateCall(s) + calls := r.generateCall(s, p) for _, c := range calls { s.analyze(c) p.Calls = append(p.Calls, c) diff --git a/prog/mutation.go b/prog/mutation.go index ffd093d7e..39b011cf1 100644 --- a/prog/mutation.go +++ b/prog/mutation.go @@ -10,7 +10,7 @@ import ( "github.com/google/syzkaller/sys" ) -func (p *Prog) Mutate(rs rand.Source, ncalls int, enabledCalls []*sys.Call) { +func (p *Prog) Mutate(rs rand.Source, ncalls int, ct *ChoiceTable) { r := newRand(rs) for stop := false; !stop; stop = r.bin() { r.choose( @@ -24,8 +24,8 @@ func (p *Prog) Mutate(rs rand.Source, ncalls int, enabledCalls []*sys.Call) { if idx < len(p.Calls) { c = p.Calls[idx] } - s := analyze(enabledCalls, p, c) - calls := r.generateCall(s) + s := analyze(ct, p, c) + calls := r.generateCall(s, p) p.insertBefore(c, calls) }, 10, func() { @@ -37,7 +37,7 @@ func (p *Prog) Mutate(rs rand.Source, ncalls int, enabledCalls []*sys.Call) { if len(c.Args) == 0 { return } - s := analyze(enabledCalls, p, c) + s := analyze(ct, p, c) for stop := false; !stop; stop = r.bin() { args, bases, parents := mutationArgs(c) idx := r.Intn(len(args)) @@ -120,7 +120,7 @@ func (p *Prog) Mutate(rs rand.Source, ncalls int, enabledCalls []*sys.Call) { for _, arg := range referencedArgs(removed, nil) { c1 := arg.Call - s := analyze(enabledCalls, p, c1) + s := analyze(ct, p, c1) arg1, _, calls1 := r.generateArg(s, arg.Type, arg.Dir, nil) replaceArg(p, arg, arg1, calls1) } @@ -203,7 +203,7 @@ func (p *Prog) Mutate(rs rand.Source, ncalls int, enabledCalls []*sys.Call) { for _, arg := range referencedArgs(c.Args, c.Ret) { c1 := arg.Call - s := analyze(enabledCalls, p, c1) + s := analyze(ct, p, c1) arg1, _, calls1 := r.generateArg(s, arg.Type, arg.Dir, nil) replaceArg(p, arg, arg1, calls1) } diff --git a/prog/prio.go b/prog/prio.go new file mode 100644 index 000000000..e8de453f4 --- /dev/null +++ b/prog/prio.go @@ -0,0 +1,274 @@ +// Copyright 2015 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 prog + +import ( + "fmt" + "math/rand" + "sort" + "sync" + + "github.com/google/syzkaller/sys" +) + +// Calulation of call-to-call priorities. +// For a given pair of calls X and Y, the priority is our guess as to whether +// additional of call Y into a program containing call X is likely to give +// new coverage or not. +// The current algorithm has two components: static and dynamic. +// The static component is based on analysis of argument types. For example, +// if call X and call Y both accept fd[sock], then they are more likely to give +// new coverage together. +// The dynamic component is based on frequency of occurrence of a particular +// pair of syscalls in a single program in corpus. For example, if socket and +// connect frequently occur in programs together, we give higher priority to +// this pair of syscalls. +// Note: the current implementation is very basic, there is no theory behind any +// constants. + +func CalculatePriorities(corpus []*Prog) [][]float32 { + static := getStaticPrio() + dynamic := calcDynamicPrio(corpus) + for i, prios := range static { + for j, p := range prios { + dynamic[i][j] *= p + } + } + return dynamic +} + +var ( + staticOnce sync.Once + staticPrio [][]float32 +) + +func getStaticPrio() [][]float32 { + staticOnce.Do(func() { + staticPrio = calcStaticPriorities() + }) + return staticPrio +} + +func calcStaticPriorities() [][]float32 { + uses := make(map[string]map[int]float32) + for _, c := range sys.Calls { + noteUsage := func(weight float32, str string, args ...interface{}) { + id := fmt.Sprintf(str, args...) + if uses[id] == nil { + uses[id] = make(map[int]float32) + } + old := uses[id][c.ID] + if weight > old { + uses[id][c.ID] = weight + } + } + foreachArgType(c, func(t sys.Type, d ArgDir) { + switch a := t.(type) { + case sys.ResourceType: + if a.Kind == sys.ResPid || a.Kind == sys.ResUid || a.Kind == sys.ResGid { + // Pid/uid/gid usually play auxiliary role, + // but massively happen in some structs. + noteUsage(0.1, "res%v", a.Kind) + } else if a.Subkind == sys.ResAny { + noteUsage(1.0, "res%v", a.Kind) + } else { + noteUsage(0.2, "res%v", a.Kind) + noteUsage(1.0, "res%v-%v", a.Kind, a.Subkind) + } + case sys.PtrType: + if _, ok := a.Type.(sys.StructType); ok { + noteUsage(1.0, "ptrto-%v", a.Type.Name()) + } + case sys.BufferType: + switch a.Kind { + case sys.BufferBlob: + case sys.BufferString: + noteUsage(0.2, "str") + case sys.BufferSockaddr: + noteUsage(1.0, "sockaddr") + default: + panic("unknown buffer kind") + } + case sys.VmaType: + noteUsage(0.5, "vma") + case sys.FilenameType: + noteUsage(1.0, "filename") + case sys.IntType: + switch a.Kind { + case sys.IntPlain: + case sys.IntSignalno: + noteUsage(1.0, "signalno") + case sys.IntInaddr: + noteUsage(1.0, "inaddr") + default: + panic("unknown int kind") + } + } + }) + } + prios := make([][]float32, len(sys.Calls)) + for i := range prios { + prios[i] = make([]float32, len(sys.Calls)) + } + for _, calls := range uses { + for c0, w0 := range calls { + for c1, w1 := range calls { + if c0 == c1 { + // Self-priority is assigned below. + continue + } + prios[c0][c1] += w0 * w1 + } + } + } + // Self-priority (call wrt itself) is assigned to the maximum priority + // this call has wrt other calls. This way the priority is high, but not too high. + for c0, pp := range prios { + var max float32 + for _, p := range pp { + if max < p { + max = p + } + } + pp[c0] = max + } + normalizePrio(prios) + return prios +} + +func calcDynamicPrio(corpus []*Prog) [][]float32 { + prios := make([][]float32, len(sys.Calls)) + for i := range prios { + prios[i] = make([]float32, len(sys.Calls)) + } + for _, p := range corpus { + for i0 := 0; i0 < len(p.Calls); i0++ { + for i1 := 0; i1 < len(p.Calls); i1++ { + if i0 == i1 { + continue + } + prios[i0][i1] += 1.0 + } + } + } + normalizePrio(prios) + return prios +} + +// normalizePrio assigns some minimal priorities to calls with zero priority, +// and then normalizes priorities to 0.1..1 range. +func normalizePrio(prios [][]float32) { + for _, prio := range prios { + max := float32(0) + min := float32(1e10) + nzero := 0 + for _, p := range prio { + if max < p { + max = p + } + if p != 0 && min > p { + min = p + } + if p == 0 { + nzero++ + } + } + if nzero != 0 { + min /= 2 * float32(nzero) + } + for i, p := range prio { + if max == 0 { + prio[i] = 1 + continue + } + if p == 0 { + p = min + } + p = (p-min)/(max-min)*0.9 + 0.1 + if p > 1 { + p = 1 + } + prio[i] = p + } + } +} + +func foreachArgType(meta *sys.Call, f func(sys.Type, ArgDir)) { + var rec func(t sys.Type, dir ArgDir) + rec = func(t sys.Type, d ArgDir) { + f(t, d) + switch a := t.(type) { + case sys.ArrayType: + rec(a.Type, d) + case sys.PtrType: + rec(a.Type, ArgDir(a.Dir)) + case sys.StructType: + for _, f := range a.Fields { + rec(f, d) + } + case sys.ResourceType, sys.FileoffType, sys.BufferType, + sys.VmaType, sys.LenType, sys.FlagsType, sys.IntType, sys.FilenameType: + default: + panic("unknown type") + } + } + for _, t := range meta.Args { + rec(t, DirIn) + } + if meta.Ret != nil { + rec(meta.Ret, DirOut) + } +} + +// ChooseTable allows to do a weighted choice of a syscall for a given syscall +// based on call-to-call priorities and a set of enabled syscalls. +type ChoiceTable struct { + run [][]int +} + +func BuildChoiceTable(prios [][]float32, enabledCalls []*sys.Call) *ChoiceTable { + enabled := make(map[int]bool) + for _, c := range enabledCalls { + enabled[c.ID] = true + } + run := make([][]int, len(sys.Calls)) + for i := range run { + if !enabled[i] { + continue + } + run[i] = make([]int, len(sys.Calls)) + sum := 0 + for j := range run[i] { + if enabled[j] { + sum += int(prios[i][j] * 1000) + } + + run[i][j] = sum + } + } + return &ChoiceTable{run} +} + +func (ct *ChoiceTable) Choose(r *rand.Rand, call int) int { + if ct == nil || call < 0 { + return r.Intn(len(sys.Calls)) + } + run := ct.run[call] + if run == nil { + return r.Intn(len(sys.Calls)) + } + for { + x := r.Intn(run[len(run)-1]) + i := sort.SearchInts(run, x) + p := 0 + if i > 0 { + p = ct.run[call][i-1] + } + if ct.run[call][i] == p { + // Call with weight 0, retry. + continue + } + return i + } +} diff --git a/prog/rand.go b/prog/rand.go index 82259a04a..498807afe 100644 --- a/prog/rand.go +++ b/prog/rand.go @@ -416,7 +416,10 @@ func (r *randGen) createResource(s *state, res sys.ResourceType) (arg *Arg, call } return false } - for _, meta := range s.enabledCalls { + for i, meta := range sys.Calls { + if s.ct == nil || s.ct.run[i] == nil { + continue + } ok := false for _, arg := range meta.Args { if checkArg(arg, DirIn) { @@ -432,12 +435,7 @@ func (r *randGen) createResource(s *state, res sys.ResourceType) (arg *Arg, call } } if len(metas) == 0 { - if len(s.enabledCalls) != len(sys.Calls) { - // We used only a subset of all syscalls, - // so we legitimately may not be able to create the resource. - return constArg(res.Default()), nil - } - panic(fmt.Sprintf("can't create resource %v/%v", res.Kind, sk)) + return constArg(res.Default()), nil } // Now we have a set of candidate calls that can create the necessary resource. @@ -446,7 +444,7 @@ func (r *randGen) createResource(s *state, res sys.ResourceType) (arg *Arg, call meta := metas[r.Intn(len(metas))] calls := r.generateParticularCall(s, meta) assignTypeAndDir(calls[len(calls)-1]) - s1 := newState(s.enabledCalls) + s1 := newState(s.ct) s1.analyze(calls[len(calls)-1]) // Now see if we have what we want. var allres []*Arg @@ -504,8 +502,19 @@ func (r *randGen) choose(args ...interface{}) { panic("choose is broken") } -func (r *randGen) generateCall(s *state) []*Call { - meta := s.enabledCalls[r.rand(len(s.enabledCalls))] +func (r *randGen) generateCall(s *state, p *Prog) []*Call { + call := -1 + if len(p.Calls) != 0 { + for i := 0; i < 5; i++ { + c := p.Calls[r.Intn(len(p.Calls))].Meta + call = c.ID + // There is roughly half of mmap's so ignore them. + if c.Name != "mmap" { + break + } + } + } + meta := sys.Calls[s.ct.Choose(r.Rand, call)] return r.generateParticularCall(s, meta) } diff --git a/rpctype/rpctype.go b/rpctype/rpctype.go index a74c78e6e..55adf636e 100644 --- a/rpctype/rpctype.go +++ b/rpctype/rpctype.go @@ -39,6 +39,7 @@ type ManagerConnectArgs struct { } type ManagerConnectRes struct { + Prios [][]float32 } type NewManagerInputArgs struct { diff --git a/sys/decl.go b/sys/decl.go index aad0e7408..88f683d75 100644 --- a/sys/decl.go +++ b/sys/decl.go @@ -180,10 +180,6 @@ type FileoffType struct { File string } -type AddrType struct { - TypeCommon -} - type BufferKind int const ( diff --git a/tools/stress/stress.go b/tools/stress/stress.go index 388e2767d..30dd84f39 100644 --- a/tools/stress/stress.go +++ b/tools/stress/stress.go @@ -24,7 +24,7 @@ var ( flagOutput = flag.Bool("output", false, "print executor output to console") flagDebug = flag.Bool("debug", false, "executor debug output") - failedRe = regexp.MustCompile("runtime error: |panic: ") + failedRe = regexp.MustCompile("runtime error: |panic: |Panic: ") ) func main() { |
