// 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 main import ( "bytes" "encoding/json" "fmt" "html/template" "io" "io/ioutil" "net/http" _ "net/http/pprof" "os" "path/filepath" "runtime" "sort" "strconv" "strings" "time" "github.com/google/syzkaller/pkg/cover" "github.com/google/syzkaller/pkg/html" "github.com/google/syzkaller/pkg/log" "github.com/google/syzkaller/pkg/osutil" "github.com/google/syzkaller/pkg/signal" "github.com/google/syzkaller/pkg/vcs" "github.com/google/syzkaller/prog" "github.com/gorilla/handlers" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" ) func (mgr *Manager) initHTTP() { mux := http.NewServeMux() mux.HandleFunc("/", mgr.httpSummary) mux.HandleFunc("/config", mgr.httpConfig) mux.HandleFunc("/metrics", promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{}).ServeHTTP) mux.HandleFunc("/syscalls", mgr.httpSyscalls) mux.HandleFunc("/corpus", mgr.httpCorpus) mux.HandleFunc("/corpus.db", mgr.httpDownloadCorpus) mux.HandleFunc("/crash", mgr.httpCrash) mux.HandleFunc("/cover", mgr.httpCover) mux.HandleFunc("/subsystemcover", mgr.httpSubsystemCover) mux.HandleFunc("/modulecover", mgr.httpModuleCover) mux.HandleFunc("/prio", mgr.httpPrio) mux.HandleFunc("/file", mgr.httpFile) mux.HandleFunc("/report", mgr.httpReport) mux.HandleFunc("/rawcover", mgr.httpRawCover) mux.HandleFunc("/rawcoverfiles", mgr.httpRawCoverFiles) mux.HandleFunc("/filterpcs", mgr.httpFilterPCs) mux.HandleFunc("/funccover", mgr.httpFuncCover) mux.HandleFunc("/filecover", mgr.httpFileCover) mux.HandleFunc("/input", mgr.httpInput) mux.HandleFunc("/debuginput", mgr.httpDebugInput) // Browsers like to request this, without special handler this goes to / handler. mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {}) log.Logf(0, "serving http on http://%v", mgr.cfg.HTTP) go func() { err := http.ListenAndServe(mgr.cfg.HTTP, handlers.CompressHandler(mux)) if err != nil { log.Fatalf("failed to listen on %v: %v", mgr.cfg.HTTP, err) } }() } func (mgr *Manager) httpSummary(w http.ResponseWriter, r *http.Request) { data := &UISummaryData{ Name: mgr.cfg.Name, Log: log.CachedLogOutput(), Stats: mgr.collectStats(), } var err error if data.Crashes, err = mgr.collectCrashes(mgr.cfg.Workdir); err != nil { http.Error(w, fmt.Sprintf("failed to collect crashes: %v", err), http.StatusInternalServerError) return } executeTemplate(w, summaryTemplate, data) } func (mgr *Manager) httpConfig(w http.ResponseWriter, r *http.Request) { data, err := json.MarshalIndent(mgr.cfg, "", "\t") if err != nil { http.Error(w, fmt.Sprintf("failed to encode json: %v", err), http.StatusInternalServerError) return } w.Write(data) } func (mgr *Manager) httpSyscalls(w http.ResponseWriter, r *http.Request) { data := &UISyscallsData{ Name: mgr.cfg.Name, } for c, cc := range mgr.collectSyscallInfo() { var syscallID *int if syscall, ok := mgr.target.SyscallMap[c]; ok { syscallID = &syscall.ID } data.Calls = append(data.Calls, UICallType{ Name: c, ID: syscallID, Inputs: cc.count, Cover: len(cc.cov), }) } sort.Slice(data.Calls, func(i, j int) bool { return data.Calls[i].Name < data.Calls[j].Name }) executeTemplate(w, syscallsTemplate, data) } func (mgr *Manager) collectStats() []UIStat { mgr.mu.Lock() defer mgr.mu.Unlock() configName := mgr.cfg.Name if configName == "" { configName = "config" } rawStats := mgr.stats.all() head := prog.GitRevisionBase stats := []UIStat{ {Name: "revision", Value: fmt.Sprint(head[:8]), Link: vcs.LogLink(vcs.SyzkallerRepo, head)}, {Name: "config", Value: configName, Link: "/config"}, {Name: "uptime", Value: fmt.Sprint(time.Since(mgr.startTime) / 1e9 * 1e9)}, {Name: "fuzzing", Value: fmt.Sprint(mgr.fuzzingTime / 60e9 * 60e9)}, {Name: "corpus", Value: fmt.Sprint(len(mgr.corpus)), Link: "/corpus"}, {Name: "triage queue", Value: fmt.Sprint(len(mgr.candidates))}, {Name: "signal", Value: fmt.Sprint(rawStats["signal"])}, {Name: "coverage", Value: fmt.Sprint(rawStats["coverage"]), Link: "/cover"}, } if mgr.coverFilter != nil { stats = append(stats, UIStat{ Name: "filtered coverage", Value: fmt.Sprintf("%v / %v (%v%%)", rawStats["filtered coverage"], len(mgr.coverFilter), rawStats["filtered coverage"]*100/uint64(len(mgr.coverFilter))), Link: "/cover?filter=yes", }) } delete(rawStats, "signal") delete(rawStats, "coverage") delete(rawStats, "filtered coverage") if mgr.checkResult != nil { stats = append(stats, UIStat{ Name: "syscalls", Value: fmt.Sprint(len(mgr.checkResult.EnabledCalls[mgr.cfg.Sandbox])), Link: "/syscalls", }) } secs := uint64(1) if !mgr.firstConnect.IsZero() { secs = uint64(time.Since(mgr.firstConnect))/1e9 + 1 } intStats := convertStats(rawStats, secs) sort.Slice(intStats, func(i, j int) bool { return intStats[i].Name < intStats[j].Name }) stats = append(stats, intStats...) return stats } func convertStats(stats map[string]uint64, secs uint64) []UIStat { var intStats []UIStat for k, v := range stats { val := fmt.Sprintf("%v", v) if x := v / secs; x >= 10 { val += fmt.Sprintf(" (%v/sec)", x) } else if x := v * 60 / secs; x >= 10 { val += fmt.Sprintf(" (%v/min)", x) } else { x := v * 60 * 60 / secs val += fmt.Sprintf(" (%v/hour)", x) } intStats = append(intStats, UIStat{Name: k, Value: val}) } return intStats } func (mgr *Manager) httpCrash(w http.ResponseWriter, r *http.Request) { crashID := r.FormValue("id") crash := readCrash(mgr.cfg.Workdir, crashID, nil, mgr.startTime, true) if crash == nil { http.Error(w, "failed to read crash info", http.StatusInternalServerError) return } executeTemplate(w, crashTemplate, crash) } func (mgr *Manager) httpCorpus(w http.ResponseWriter, r *http.Request) { mgr.mu.Lock() defer mgr.mu.Unlock() data := UICorpus{ Call: r.FormValue("call"), RawCover: mgr.cfg.RawCover, } for sig, inp := range mgr.corpus { if data.Call != "" && data.Call != inp.Call { continue } p, err := mgr.target.Deserialize(inp.Prog, prog.NonStrict) if err != nil { http.Error(w, fmt.Sprintf("failed to deserialize program: %v", err), http.StatusInternalServerError) return } data.Inputs = append(data.Inputs, &UIInput{ Sig: sig, Short: p.String(), Cover: len(inp.Cover), }) } sort.Slice(data.Inputs, func(i, j int) bool { a, b := data.Inputs[i], data.Inputs[j] if a.Cover != b.Cover { return a.Cover > b.Cover } return a.Short < b.Short }) executeTemplate(w, corpusTemplate, data) } func (mgr *Manager) httpDownloadCorpus(w http.ResponseWriter, r *http.Request) { corpus := filepath.Join(mgr.cfg.Workdir, "corpus.db") file, err := os.Open(corpus) if err != nil { http.Error(w, fmt.Sprintf("failed to open corpus : %v", err), http.StatusInternalServerError) return } defer file.Close() buf, err := ioutil.ReadAll(file) if err != nil { http.Error(w, fmt.Sprintf("failed to read corpus : %v", err), http.StatusInternalServerError) return } w.Write(buf) } const ( DoHTML int = iota DoHTMLTable DoModuleCover DoCSV DoCSVFiles DoRawCoverFiles DoRawCover DoFilterPCs ) func (mgr *Manager) httpCover(w http.ResponseWriter, r *http.Request) { mgr.httpCoverCover(w, r, DoHTML, true) } func (mgr *Manager) httpSubsystemCover(w http.ResponseWriter, r *http.Request) { mgr.httpCoverCover(w, r, DoHTMLTable, true) } func (mgr *Manager) httpModuleCover(w http.ResponseWriter, r *http.Request) { mgr.httpCoverCover(w, r, DoModuleCover, true) } func (mgr *Manager) httpCoverCover(w http.ResponseWriter, r *http.Request, funcFlag int, isHTMLCover bool) { if !mgr.cfg.Cover { if isHTMLCover { mgr.httpCoverFallback(w, r) } else { http.Error(w, "coverage is not enabled", http.StatusInternalServerError) } return } // Don't hold the mutex while creating report generator and generating the report, // these operations take lots of time. mgr.mu.Lock() initialized := mgr.modulesInitialized mgr.mu.Unlock() if !initialized { http.Error(w, "coverage is not ready, please try again later after fuzzer started", http.StatusInternalServerError) return } rg, err := getReportGenerator(mgr.cfg, mgr.modules) if err != nil { http.Error(w, fmt.Sprintf("failed to generate coverage profile: %v", err), http.StatusInternalServerError) return } mgr.mu.Lock() var progs []cover.Prog if sig := r.FormValue("input"); sig != "" { inp := mgr.corpus[sig] if r.FormValue("update_id") != "" { updateID, err := strconv.Atoi(r.FormValue("update_id")) if err != nil || updateID < 0 || updateID >= len(inp.Updates) { http.Error(w, "bad call_id", http.StatusBadRequest) } progs = append(progs, cover.Prog{ Sig: sig, Data: string(inp.Prog), PCs: coverToPCs(rg, inp.Updates[updateID].RawCover), }) } else { progs = append(progs, cover.Prog{ Sig: sig, Data: string(inp.Prog), PCs: coverToPCs(rg, inp.Cover), }) } } else { call := r.FormValue("call") for sig, inp := range mgr.corpus { if call != "" && call != inp.Call { continue } progs = append(progs, cover.Prog{ Sig: sig, Data: string(inp.Prog), PCs: coverToPCs(rg, inp.Cover), }) } } mgr.mu.Unlock() var coverFilter map[uint32]uint32 if r.FormValue("filter") != "" { coverFilter = mgr.coverFilter } if funcFlag == DoRawCoverFiles { if err := rg.DoRawCoverFiles(w, progs, coverFilter); err != nil { http.Error(w, fmt.Sprintf("failed to generate coverage profile: %v", err), http.StatusInternalServerError) return } runtime.GC() return } else if funcFlag == DoRawCover { rg.DoRawCover(w, progs, coverFilter) return } else if funcFlag == DoFilterPCs { rg.DoFilterPCs(w, progs, coverFilter) return } do := rg.DoHTML if funcFlag == DoHTMLTable { do = rg.DoHTMLTable } else if funcFlag == DoModuleCover { do = rg.DoModuleCover } else if funcFlag == DoCSV { do = rg.DoCSV } else if funcFlag == DoCSVFiles { do = rg.DoCSVFiles } if err := do(w, progs, coverFilter); err != nil { http.Error(w, fmt.Sprintf("failed to generate coverage profile: %v", err), http.StatusInternalServerError) return } runtime.GC() } func (mgr *Manager) httpCoverFallback(w http.ResponseWriter, r *http.Request) { mgr.mu.Lock() defer mgr.mu.Unlock() var maxSignal signal.Signal for _, inp := range mgr.corpus { maxSignal.Merge(inp.Signal.Deserialize()) } calls := make(map[int][]int) for s := range maxSignal { id, errno := prog.DecodeFallbackSignal(uint32(s)) calls[id] = append(calls[id], errno) } data := &UIFallbackCoverData{} for _, id := range mgr.checkResult.EnabledCalls[mgr.cfg.Sandbox] { errnos := calls[id] sort.Ints(errnos) successful := 0 for len(errnos) != 0 && errnos[0] == 0 { successful++ errnos = errnos[1:] } data.Calls = append(data.Calls, UIFallbackCall{ Name: mgr.target.Syscalls[id].Name, Successful: successful, Errnos: errnos, }) } sort.Slice(data.Calls, func(i, j int) bool { return data.Calls[i].Name < data.Calls[j].Name }) executeTemplate(w, fallbackCoverTemplate, data) } func (mgr *Manager) httpFuncCover(w http.ResponseWriter, r *http.Request) { mgr.httpCoverCover(w, r, DoCSV, false) } func (mgr *Manager) httpFileCover(w http.ResponseWriter, r *http.Request) { mgr.httpCoverCover(w, r, DoCSVFiles, false) } func (mgr *Manager) httpPrio(w http.ResponseWriter, r *http.Request) { mgr.mu.Lock() defer mgr.mu.Unlock() callName := r.FormValue("call") call := mgr.target.SyscallMap[callName] if call == nil { http.Error(w, fmt.Sprintf("unknown call: %v", callName), http.StatusInternalServerError) return } var corpus []*prog.Prog for _, inp := range mgr.corpus { p, err := mgr.target.Deserialize(inp.Prog, prog.NonStrict) if err != nil { http.Error(w, fmt.Sprintf("failed to deserialize program: %v", err), http.StatusInternalServerError) return } corpus = append(corpus, p) } prios := mgr.target.CalculatePriorities(corpus) data := &UIPrioData{Call: callName} for i, p := range prios[call.ID] { data.Prios = append(data.Prios, UIPrio{mgr.target.Syscalls[i].Name, p}) } sort.Slice(data.Prios, func(i, j int) bool { return data.Prios[i].Prio > data.Prios[j].Prio }) executeTemplate(w, prioTemplate, data) } func (mgr *Manager) httpFile(w http.ResponseWriter, r *http.Request) { file := filepath.Clean(r.FormValue("name")) if !strings.HasPrefix(file, "crashes/") && !strings.HasPrefix(file, "corpus/") { http.Error(w, "oh, oh, oh!", http.StatusInternalServerError) return } file = filepath.Join(mgr.cfg.Workdir, file) f, err := os.Open(file) if err != nil { http.Error(w, "failed to open the file", http.StatusInternalServerError) return } defer f.Close() w.Header().Set("Content-Type", "text/plain; charset=utf-8") io.Copy(w, f) } func (mgr *Manager) httpInput(w http.ResponseWriter, r *http.Request) { mgr.mu.Lock() defer mgr.mu.Unlock() inp, ok := mgr.corpus[r.FormValue("sig")] if !ok { http.Error(w, "can't find the input", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Write(inp.Prog) } func (mgr *Manager) httpDebugInput(w http.ResponseWriter, r *http.Request) { mgr.mu.Lock() defer mgr.mu.Unlock() inp, ok := mgr.corpus[r.FormValue("sig")] if !ok { http.Error(w, "can't find the input", http.StatusInternalServerError) return } getIDs := func(callID int) []int { ret := []int{} for id, update := range inp.Updates { if update.CallID == callID { ret = append(ret, id) } } return ret } data := []UIRawCallCover{} for pos, line := range strings.Split(string(inp.Prog), "\n") { line = strings.TrimSpace(line) if line == "" { continue } data = append(data, UIRawCallCover{ Sig: r.FormValue("sig"), Call: line, UpdateIDs: getIDs(pos), }) } extraIDs := getIDs(-1) if len(extraIDs) > 0 { data = append(data, UIRawCallCover{ Sig: r.FormValue("sig"), Call: ".extra", UpdateIDs: extraIDs, }) } executeTemplate(w, rawCoverTemplate, data) } func (mgr *Manager) httpReport(w http.ResponseWriter, r *http.Request) { mgr.mu.Lock() defer mgr.mu.Unlock() crashID := r.FormValue("id") desc, err := ioutil.ReadFile(filepath.Join(mgr.crashdir, crashID, "description")) if err != nil { http.Error(w, "failed to read description file", http.StatusInternalServerError) return } tag, _ := ioutil.ReadFile(filepath.Join(mgr.crashdir, crashID, "repro.tag")) prog, _ := ioutil.ReadFile(filepath.Join(mgr.crashdir, crashID, "repro.prog")) cprog, _ := ioutil.ReadFile(filepath.Join(mgr.crashdir, crashID, "repro.cprog")) rep, _ := ioutil.ReadFile(filepath.Join(mgr.crashdir, crashID, "repro.report")) commitDesc := "" if len(tag) != 0 { commitDesc = fmt.Sprintf(" on commit %s.", trimNewLines(tag)) } fmt.Fprintf(w, "Syzkaller hit '%s' bug%s.\n\n", trimNewLines(desc), commitDesc) if len(rep) != 0 { fmt.Fprintf(w, "%s\n\n", rep) } if len(prog) == 0 && len(cprog) == 0 { fmt.Fprintf(w, "The bug is not reproducible.\n") } else { fmt.Fprintf(w, "Syzkaller reproducer:\n%s\n\n", prog) if len(cprog) != 0 { fmt.Fprintf(w, "C reproducer:\n%s\n\n", cprog) } } } func (mgr *Manager) httpRawCover(w http.ResponseWriter, r *http.Request) { mgr.httpCoverCover(w, r, DoRawCover, false) } func (mgr *Manager) httpRawCoverFiles(w http.ResponseWriter, r *http.Request) { mgr.httpCoverCover(w, r, DoRawCoverFiles, false) } func (mgr *Manager) httpFilterPCs(w http.ResponseWriter, r *http.Request) { if mgr.coverFilter == nil { fmt.Fprintf(w, "cover is not filtered in config.\n") return } mgr.httpCoverCover(w, r, DoFilterPCs, false) } func (mgr *Manager) collectCrashes(workdir string) ([]*UICrashType, error) { // Note: mu is not locked here. reproReply := make(chan map[string]bool) mgr.reproRequest <- reproReply repros := <-reproReply crashdir := filepath.Join(workdir, "crashes") dirs, err := osutil.ListDir(crashdir) if err != nil { return nil, err } var crashTypes []*UICrashType for _, dir := range dirs { crash := readCrash(workdir, dir, repros, mgr.startTime, false) if crash != nil { crashTypes = append(crashTypes, crash) } } sort.Slice(crashTypes, func(i, j int) bool { return strings.ToLower(crashTypes[i].Description) < strings.ToLower(crashTypes[j].Description) }) return crashTypes, nil } func readCrash(workdir, dir string, repros map[string]bool, start time.Time, full bool) *UICrashType { if len(dir) != 40 { return nil } crashdir := filepath.Join(workdir, "crashes") descFile, err := os.Open(filepath.Join(crashdir, dir, "description")) if err != nil { return nil } defer descFile.Close() descBytes, err := ioutil.ReadAll(descFile) if err != nil || len(descBytes) == 0 { return nil } desc := string(trimNewLines(descBytes)) stat, err := descFile.Stat() if err != nil { return nil } modTime := stat.ModTime() descFile.Close() files, err := osutil.ListDir(filepath.Join(crashdir, dir)) if err != nil { return nil } var crashes []*UICrash reproAttempts := 0 hasRepro, hasCRepro := false, false reports := make(map[string]bool) for _, f := range files { if strings.HasPrefix(f, "log") { index, err := strconv.ParseUint(f[3:], 10, 64) if err == nil { crashes = append(crashes, &UICrash{ Index: int(index), }) } } else if strings.HasPrefix(f, "report") { reports[f] = true } else if f == "repro.prog" { hasRepro = true } else if f == "repro.cprog" { hasCRepro = true } else if f == "repro.report" { } else if f == "repro0" || f == "repro1" || f == "repro2" { reproAttempts++ } } if full { for _, crash := range crashes { index := strconv.Itoa(crash.Index) crash.Log = filepath.Join("crashes", dir, "log"+index) if stat, err := os.Stat(filepath.Join(workdir, crash.Log)); err == nil { crash.Time = stat.ModTime() crash.Active = crash.Time.After(start) } tag, _ := ioutil.ReadFile(filepath.Join(crashdir, dir, "tag"+index)) crash.Tag = string(tag) reportFile := filepath.Join("crashes", dir, "report"+index) if osutil.IsExist(filepath.Join(workdir, reportFile)) { crash.Report = reportFile } } sort.Slice(crashes, func(i, j int) bool { return crashes[i].Time.After(crashes[j].Time) }) } triaged := reproStatus(hasRepro, hasCRepro, repros[desc], reproAttempts >= maxReproAttempts) return &UICrashType{ Description: desc, LastTime: modTime, Active: modTime.After(start), ID: dir, Count: len(crashes), Triaged: triaged, Crashes: crashes, } } func reproStatus(hasRepro, hasCRepro, reproducing, nonReproducible bool) string { status := "" if hasRepro { status = "has repro" if hasCRepro { status = "has C repro" } } else if reproducing { status = "reproducing" } else if nonReproducible { status = "non-reproducible" } return status } func executeTemplate(w http.ResponseWriter, templ *template.Template, data interface{}) { buf := new(bytes.Buffer) if err := templ.Execute(buf, data); err != nil { log.Logf(0, "failed to execute template: %v", err) http.Error(w, fmt.Sprintf("failed to execute template: %v", err), http.StatusInternalServerError) return } w.Write(buf.Bytes()) } func trimNewLines(data []byte) []byte { for len(data) > 0 && data[len(data)-1] == '\n' { data = data[:len(data)-1] } return data } type UISummaryData struct { Name string Stats []UIStat Crashes []*UICrashType Log string } type UISyscallsData struct { Name string Calls []UICallType } type UICrashType struct { Description string LastTime time.Time Active bool ID string Count int Triaged string Crashes []*UICrash } type UICrash struct { Index int Time time.Time Active bool Log string Report string Tag string } type UIStat struct { Name string Value string Link string } type UICallType struct { Name string ID *int Inputs int Cover int } type UICorpus struct { Call string RawCover bool Inputs []*UIInput } type UIInput struct { Sig string Short string Cover int } var summaryTemplate = html.CreatePage(` {{.Name }} syzkaller {{HEAD}} {{.Name }} syzkaller
{{range $s := $.Stats}} {{end}}
Stats:
{{$s.Name}} {{if $s.Link}} {{$s.Value}} {{else}} {{$s.Value}} {{end}}
{{range $c := $.Crashes}} {{end}}
Crashes:
Description Count Last Time Report
{{$c.Description}} {{$c.Count}} {{formatTime $c.LastTime}} {{if $c.Triaged}} {{$c.Triaged}} {{end}}
Log:
`) var syscallsTemplate = html.CreatePage(` {{.Name }} syzkaller {{HEAD}} {{range $c := $.Calls}} {{end}}
Per-syscall coverage:
Syscall Inputs Coverage Prio
{{$c.Name}}{{if $c.ID }} [{{$c.ID}}]{{end}} {{$c.Inputs}} {{$c.Cover}} prio
`) var crashTemplate = html.CreatePage(` {{.Description}} {{HEAD}} {{.Description}} {{if .Triaged}} Report: {{.Triaged}} {{end}} {{range $c := $.Crashes}} {{end}} {{end}}
# Log Report Time Tag
{{$c.Index}} log {{if $c.Report}} report {{formatTime $c.Time}} {{formatTagHash $c.Tag}}
`) var corpusTemplate = html.CreatePage(` syzkaller corpus {{HEAD}} {{range $inp := $.Inputs}} {{end}}
Corpus{{if $.Call}} for {{$.Call}}{{end}}:
Coverage Program
{{$inp.Cover}} {{if $.RawCover}} / [raw] {{end}} {{$inp.Short}}
`) type UIPrioData struct { Call string Prios []UIPrio } type UIPrio struct { Call string Prio int32 } var prioTemplate = html.CreatePage(` syzkaller priorities {{HEAD}} {{range $p := $.Prios}} {{end}}
Priorities for {{$.Call}}:
Prio Call
{{printf "%5v" $p.Prio}} {{$p.Call}}
`) type UIFallbackCoverData struct { Calls []UIFallbackCall } type UIFallbackCall struct { Name string Successful int Errnos []int } var fallbackCoverTemplate = html.CreatePage(` syzkaller coverage {{HEAD}} {{range $c := $.Calls}} {{end}}
Call Successful Errnos
{{$c.Name}} {{if $c.Successful}}{{$c.Successful}}{{end}} {{range $e := $c.Errnos}}{{$e}} {{end}}
`) type UIRawCallCover struct { Sig string Call string UpdateIDs []int } var rawCoverTemplate = html.CreatePage(` syzkaller raw cover {{HEAD}} {{range $line := .}} {{end}}
Raw cover
Line Links
{{$line.Call}} {{range $id := $line.UpdateIDs}} [{{$id}}] {{end}}
`)