// Copyright 2017 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 dash import ( "bytes" "fmt" "net/http" "sort" "strconv" "strings" "time" "github.com/google/syzkaller/dashboard/dashapi" "github.com/google/syzkaller/pkg/email" "golang.org/x/net/context" "google.golang.org/appengine/datastore" "google.golang.org/appengine/log" ) // This file contains web UI http handlers. func initHTTPHandlers() { http.Handle("/", handlerWrapper(handleMain)) http.Handle("/bug", handlerWrapper(handleBug)) http.Handle("/text", handlerWrapper(handleText)) http.Handle("/x/.config", handlerWrapper(handleTextX(textKernelConfig))) http.Handle("/x/log.txt", handlerWrapper(handleTextX(textCrashLog))) http.Handle("/x/repro.syz", handlerWrapper(handleTextX(textReproSyz))) http.Handle("/x/repro.c", handlerWrapper(handleTextX(textReproC))) http.Handle("/x/patch.diff", handlerWrapper(handleTextX(textPatch))) http.Handle("/x/error.txt", handlerWrapper(handleTextX(textError))) } type uiMain struct { Header *uiHeader Now time.Time Log []byte Managers []*uiManager Jobs []*uiJob BugNamespaces []*uiBugNamespace } type uiManager struct { Namespace string Name string Link string CurrentBuild *uiBuild FailedBuildBugLink string LastActive time.Time LastActiveBad bool CurrentUpTime time.Duration MaxCorpus int64 MaxCover int64 TotalFuzzingTime time.Duration TotalCrashes int64 TotalExecs int64 } type uiBuild struct { Time time.Time SyzkallerCommit string KernelAlias string KernelCommit string KernelConfigLink string } type uiBugPage struct { Header *uiHeader Now time.Time Bug *uiBug DupOf *uiBugGroup Dups *uiBugGroup Similar *uiBugGroup SampleReport []byte Crashes []*uiCrash } type uiBugNamespace struct { Name string Caption string CoverLink string FixedLink string FixedCount int Groups []*uiBugGroup } type uiBugGroup struct { Now time.Time Caption string Fragment string Namespace string ShowNamespace bool ShowPatch bool ShowPatched bool ShowStatus bool ShowIndex int Bugs []*uiBug } type uiBug struct { Namespace string Title string NumCrashes int64 NumCrashesBad bool FirstTime time.Time LastTime time.Time ReportedTime time.Time ClosedTime time.Time ReproLevel dashapi.ReproLevel ReportingIndex int Status string Link string ExternalLink string CreditEmail string Commits string PatchedOn []string MissingOn []string NumManagers int } type uiCrash struct { Manager string Time time.Time Maintainers string LogLink string ReportLink string ReproSyzLink string ReproCLink string *uiBuild } type uiJob struct { Created time.Time BugLink string ExternalLink string User string Reporting string Namespace string Manager string BugTitle string BugID string KernelAlias string KernelCommit string PatchLink string Attempts int Started time.Time Finished time.Time CrashTitle string CrashLogLink string CrashReportLink string ErrorLink string Reported bool } // handleMain serves main page. func handleMain(c context.Context, w http.ResponseWriter, r *http.Request) error { var errorLog []byte var managers []*uiManager var jobs []*uiJob if accessLevel(c, r) == AccessAdmin && r.FormValue("fixed") == "" { var err error errorLog, err = fetchErrorLogs(c) if err != nil { return err } managers, err = loadManagers(c) if err != nil { return err } jobs, err = loadRecentJobs(c) if err != nil { return err } } bugNamespaces, err := fetchBugs(c, r) if err != nil { return err } data := &uiMain{ Header: commonHeader(c, r), Now: timeNow(c), Log: errorLog, Managers: managers, Jobs: jobs, BugNamespaces: bugNamespaces, } return serveTemplate(w, "main.html", data) } // handleBug serves page about a single bug (which is passed in id argument). func handleBug(c context.Context, w http.ResponseWriter, r *http.Request) error { bug := new(Bug) if id := r.FormValue("id"); id != "" { bugKey := datastore.NewKey(c, "Bug", id, 0, nil) if err := datastore.Get(c, bugKey, bug); err != nil { return err } } else if extID := r.FormValue("extid"); extID != "" { var err error bug, _, err = findBugByReportingID(c, extID) if err != nil { return err } } else { return ErrDontLog(fmt.Errorf("mandatory parameter id/extid is missing")) } accessLevel := accessLevel(c, r) if err := checkAccessLevel(c, r, bug.sanitizeAccess(accessLevel)); err != nil { return err } state, err := loadReportingState(c) if err != nil { return err } managers, err := managerList(c, bug.Namespace) if err != nil { return err } var dupOf *uiBugGroup if bug.DupOf != "" { dup := new(Bug) if err := datastore.Get(c, datastore.NewKey(c, "Bug", bug.DupOf, 0, nil), dup); err != nil { return err } if accessLevel >= dup.sanitizeAccess(accessLevel) { dupOf = &uiBugGroup{ Now: timeNow(c), Caption: "Duplicate of", Bugs: []*uiBug{createUIBug(c, dup, state, managers)}, } } } uiBug := createUIBug(c, bug, state, managers) crashes, sampleReport, err := loadCrashesForBug(c, bug) if err != nil { return err } dups, err := loadDupsForBug(c, r, bug, state, managers) if err != nil { return err } similar, err := loadSimilarBugs(c, r, bug, state) if err != nil { return err } data := &uiBugPage{ Header: commonHeader(c, r), Now: timeNow(c), Bug: uiBug, DupOf: dupOf, Dups: dups, Similar: similar, SampleReport: sampleReport, Crashes: crashes, } return serveTemplate(w, "bug.html", data) } // handleText serves plain text blobs (crash logs, reports, reproducers, etc). func handleTextImpl(c context.Context, w http.ResponseWriter, r *http.Request, tag string) error { var id int64 if x := r.FormValue("x"); x != "" { xid, err := strconv.ParseUint(x, 16, 64) if err != nil || xid == 0 { return ErrDontLog(fmt.Errorf("failed to parse text id: %v", err)) } id = int64(xid) } else { // Old link support, don't remove. xid, err := strconv.ParseInt(r.FormValue("id"), 10, 64) if err != nil || xid == 0 { return ErrDontLog(fmt.Errorf("failed to parse text id: %v", err)) } id = xid } crash, err := checkTextAccess(c, r, tag, id) if err != nil { return err } data, ns, err := getText(c, tag, id) if err != nil { return err } if err := checkAccessLevel(c, r, config.Namespaces[ns].AccessLevel); err != nil { return err } w.Header().Set("Content-Type", "text/plain; charset=utf-8") // Unfortunately filename does not work in chrome on linux due to: // https://bugs.chromium.org/p/chromium/issues/detail?id=608342 w.Header().Set("Content-Disposition", "inline; filename="+textFilename(tag)) if tag == textReproSyz { // Add link to documentation and repro opts for syzkaller reproducers. w.Write([]byte(syzReproPrefix)) if crash != nil { fmt.Fprintf(w, "#%s\n", crash.ReproOpts) } } w.Write(data) return nil } func handleText(c context.Context, w http.ResponseWriter, r *http.Request) error { return handleTextImpl(c, w, r, r.FormValue("tag")) } func handleTextX(tag string) contextHandler { return func(c context.Context, w http.ResponseWriter, r *http.Request) error { return handleTextImpl(c, w, r, tag) } } func textFilename(tag string) string { switch tag { case textKernelConfig: return ".config" case textCrashLog: return "log.txt" case textCrashReport: return "report.txt" case textReproSyz: return "repro.syz" case textReproC: return "repro.c" case textPatch: return "patch.diff" case textError: return "error.txt" default: return "text.txt" } } func fetchBugs(c context.Context, r *http.Request) ([]*uiBugNamespace, error) { state, err := loadReportingState(c) if err != nil { return nil, err } accessLevel := accessLevel(c, r) onlyFixed := r.FormValue("fixed") var res []*uiBugNamespace for ns, cfg := range config.Namespaces { if accessLevel < cfg.AccessLevel { continue } if onlyFixed != "" && onlyFixed != ns { continue } uiNamespace, err := fetchNamespaceBugs(c, accessLevel, ns, state, onlyFixed != "") if err != nil { return nil, err } res = append(res, uiNamespace) } sort.Sort(uiBugNamespaceSorter(res)) return res, nil } func fetchNamespaceBugs(c context.Context, accessLevel AccessLevel, ns string, state *ReportingState, onlyFixed bool) (*uiBugNamespace, error) { query := datastore.NewQuery("Bug").Filter("Namespace=", ns) if onlyFixed { query = query.Filter("Status=", BugStatusFixed) } var bugs []*Bug _, err := query.GetAll(c, &bugs) if err != nil { return nil, err } managers, err := managerList(c, ns) if err != nil { return nil, err } fixedCount := 0 groups := make(map[int][]*uiBug) bugMap := make(map[string]*uiBug) var dups []*Bug for _, bug := range bugs { if bug.Status == BugStatusFixed { fixedCount++ } if bug.Status == BugStatusInvalid || bug.Status == BugStatusFixed != onlyFixed { continue } if accessLevel < bug.sanitizeAccess(accessLevel) { continue } if bug.Status == BugStatusDup { dups = append(dups, bug) continue } uiBug := createUIBug(c, bug, state, managers) bugMap[bugKeyHash(bug.Namespace, bug.Title, bug.Seq)] = uiBug id := uiBug.ReportingIndex if bug.Status == BugStatusFixed { id = -1 } else if uiBug.Commits != "" { id = -2 } groups[id] = append(groups[id], uiBug) } for _, dup := range dups { bug := bugMap[dup.DupOf] if bug == nil { continue // this can be an invalid bug which we filtered above } mergeUIBug(c, bug, dup) } var uiGroups []*uiBugGroup for index, bugs := range groups { sort.Sort(uiBugSorter(bugs)) caption, fragment, showPatch, showPatched := "", "", false, false switch index { case -1: caption, showPatch, showPatched = "fixed", true, false case -2: caption, showPatch, showPatched = "fix pending", false, true fragment = ns + "-pending" case len(config.Namespaces[ns].Reporting) - 1: caption, showPatch, showPatched = "open", false, false fragment = ns + "-open" default: reporting := &config.Namespaces[ns].Reporting[index] caption, showPatch, showPatched = reporting.DisplayTitle, false, false fragment = ns + "-" + reporting.Name } uiGroups = append(uiGroups, &uiBugGroup{ Now: timeNow(c), Caption: fmt.Sprintf("%v (%v)", caption, len(bugs)), Fragment: fragment, Namespace: ns, ShowPatch: showPatch, ShowPatched: showPatched, ShowIndex: index, Bugs: bugs, }) } sort.Sort(uiBugGroupSorter(uiGroups)) fixedLink := "" if !onlyFixed { fixedLink = fmt.Sprintf("?fixed=%v", ns) } cfg := config.Namespaces[ns] uiNamespace := &uiBugNamespace{ Name: ns, Caption: cfg.DisplayTitle, CoverLink: cfg.CoverLink, FixedCount: fixedCount, FixedLink: fixedLink, Groups: uiGroups, } return uiNamespace, nil } func loadDupsForBug(c context.Context, r *http.Request, bug *Bug, state *ReportingState, managers []string) ( *uiBugGroup, error) { bugHash := bugKeyHash(bug.Namespace, bug.Title, bug.Seq) var dups []*Bug _, err := datastore.NewQuery("Bug"). Filter("Status=", BugStatusDup). Filter("DupOf=", bugHash). GetAll(c, &dups) if err != nil { return nil, err } var results []*uiBug accessLevel := accessLevel(c, r) for _, dup := range dups { if accessLevel < dup.sanitizeAccess(accessLevel) { continue } results = append(results, createUIBug(c, dup, state, managers)) } group := &uiBugGroup{ Now: timeNow(c), Caption: "duplicates", ShowPatched: true, ShowStatus: true, Bugs: results, } return group, nil } func loadSimilarBugs(c context.Context, r *http.Request, bug *Bug, state *ReportingState) (*uiBugGroup, error) { var similar []*Bug _, err := datastore.NewQuery("Bug"). Filter("Title=", bug.Title). GetAll(c, &similar) if err != nil { return nil, err } managers := make(map[string][]string) var results []*uiBug accessLevel := accessLevel(c, r) for _, similar := range similar { if accessLevel < similar.sanitizeAccess(accessLevel) { continue } if similar.Namespace == bug.Namespace && similar.Seq == bug.Seq { continue } if managers[similar.Namespace] == nil { mgrs, err := managerList(c, similar.Namespace) if err != nil { return nil, err } managers[similar.Namespace] = mgrs } results = append(results, createUIBug(c, similar, state, managers[similar.Namespace])) } group := &uiBugGroup{ Now: timeNow(c), Caption: "similar bugs", ShowNamespace: true, ShowPatched: true, ShowStatus: true, Bugs: results, } return group, nil } func createUIBug(c context.Context, bug *Bug, state *ReportingState, managers []string) *uiBug { reportingIdx, status, link := 0, "", "" var reported time.Time var err error if bug.Status == BugStatusOpen { _, _, _, _, reportingIdx, status, link, err = needReport(c, "", state, bug) reported = bug.Reporting[reportingIdx].Reported if err != nil { status = err.Error() } if status == "" { status = "???" } } else { for i := range bug.Reporting { bugReporting := &bug.Reporting[i] if i == len(bug.Reporting)-1 || bug.Status == BugStatusInvalid && !bugReporting.Closed.IsZero() && bug.Reporting[i+1].Closed.IsZero() || (bug.Status == BugStatusFixed || bug.Status == BugStatusDup) && bugReporting.Closed.IsZero() { reportingIdx = i reported = bugReporting.Reported link = bugReporting.Link switch bug.Status { case BugStatusInvalid: status = "closed as invalid" case BugStatusFixed: status = "fixed" case BugStatusDup: status = "closed as dup" default: status = fmt.Sprintf("unknown (%v)", bug.Status) } status = fmt.Sprintf("%v on %v", status, formatTime(bug.Closed)) break } } } creditEmail, err := email.AddAddrContext(ownEmail(c), bug.Reporting[reportingIdx].ID) if err != nil { log.Errorf(c, "failed to generate credit email: %v", err) } id := bugKeyHash(bug.Namespace, bug.Title, bug.Seq) uiBug := &uiBug{ Namespace: bug.Namespace, Title: bug.displayTitle(), NumCrashes: bug.NumCrashes, FirstTime: bug.FirstTime, LastTime: bug.LastTime, ReportedTime: reported, ClosedTime: bug.Closed, ReproLevel: bug.ReproLevel, ReportingIndex: reportingIdx, Status: status, Link: bugLink(id), ExternalLink: link, CreditEmail: creditEmail, NumManagers: len(managers), } updateBugBadness(c, uiBug) if len(bug.Commits) != 0 { uiBug.Commits = bug.Commits[0] if len(bug.Commits) > 1 { uiBug.Commits = fmt.Sprintf("%q", bug.Commits) } for _, mgr := range managers { found := false for _, mgr1 := range bug.PatchedOn { if mgr == mgr1 { found = true break } } if found { uiBug.PatchedOn = append(uiBug.PatchedOn, mgr) } else { uiBug.MissingOn = append(uiBug.MissingOn, mgr) } } sort.Strings(uiBug.PatchedOn) sort.Strings(uiBug.MissingOn) } return uiBug } func mergeUIBug(c context.Context, bug *uiBug, dup *Bug) { bug.NumCrashes += dup.NumCrashes if bug.LastTime.Before(dup.LastTime) { bug.LastTime = dup.LastTime } if bug.ReproLevel < dup.ReproLevel { bug.ReproLevel = dup.ReproLevel } updateBugBadness(c, bug) } func updateBugBadness(c context.Context, bug *uiBug) { bug.NumCrashesBad = bug.NumCrashes >= 10000 && timeNow(c).Sub(bug.LastTime) < 24*time.Hour } func loadCrashesForBug(c context.Context, bug *Bug) ([]*uiCrash, []byte, error) { bugHash := bugKeyHash(bug.Namespace, bug.Title, bug.Seq) bugKey := datastore.NewKey(c, "Bug", bugHash, 0, nil) // We can have more than maxCrashes crashes, if we have lots of reproducers. crashes, _, err := queryCrashesForBug(c, bugKey, maxCrashes+200) if err != nil || len(crashes) == 0 { return nil, nil, err } builds := make(map[string]*Build) var results []*uiCrash for _, crash := range crashes { build := builds[crash.BuildID] if build == nil { build, err = loadBuild(c, bug.Namespace, crash.BuildID) if err != nil { return nil, nil, err } builds[crash.BuildID] = build } ui := &uiCrash{ Manager: crash.Manager, Time: crash.Time, Maintainers: fmt.Sprintf("%q", crash.Maintainers), LogLink: textLink(textCrashLog, crash.Log), ReportLink: textLink(textCrashReport, crash.Report), ReproSyzLink: textLink(textReproSyz, crash.ReproSyz), ReproCLink: textLink(textReproC, crash.ReproC), uiBuild: makeUIBuild(build), } results = append(results, ui) } sampleReport, _, err := getText(c, textCrashReport, crashes[0].Report) if err != nil { return nil, nil, err } return results, sampleReport, nil } func makeUIBuild(build *Build) *uiBuild { return &uiBuild{ Time: build.Time, SyzkallerCommit: build.SyzkallerCommit, KernelAlias: kernelRepoInfo(build).Alias, KernelCommit: build.KernelCommit, KernelConfigLink: textLink(textKernelConfig, build.KernelConfig), } } func loadManagers(c context.Context) ([]*uiManager, error) { now := timeNow(c) date := timeDate(now) managers, managerKeys, err := loadAllManagers(c) if err != nil { return nil, err } var buildKeys []*datastore.Key var statsKeys []*datastore.Key for i, mgr := range managers { if mgr.CurrentBuild != "" { buildKeys = append(buildKeys, buildKey(c, mgr.Namespace, mgr.CurrentBuild)) } if timeDate(mgr.LastAlive) == date { statsKeys = append(statsKeys, datastore.NewKey(c, "ManagerStats", "", int64(date), managerKeys[i])) } } builds := make([]*Build, len(buildKeys)) if err := datastore.GetMulti(c, buildKeys, builds); err != nil { return nil, err } uiBuilds := make(map[string]*uiBuild) for _, build := range builds { uiBuilds[build.Namespace+"|"+build.ID] = makeUIBuild(build) } stats := make([]*ManagerStats, len(statsKeys)) if err := datastore.GetMulti(c, statsKeys, stats); err != nil { return nil, err } var fullStats []*ManagerStats for _, mgr := range managers { if timeDate(mgr.LastAlive) != date { fullStats = append(fullStats, &ManagerStats{}) continue } fullStats = append(fullStats, stats[0]) stats = stats[1:] } var results []*uiManager for i, mgr := range managers { stats := fullStats[i] results = append(results, &uiManager{ Namespace: mgr.Namespace, Name: mgr.Name, Link: mgr.Link, CurrentBuild: uiBuilds[mgr.Namespace+"|"+mgr.CurrentBuild], FailedBuildBugLink: bugLink(mgr.FailedBuildBug), LastActive: mgr.LastAlive, LastActiveBad: now.Sub(mgr.LastAlive) > 12*time.Hour, CurrentUpTime: mgr.CurrentUpTime, MaxCorpus: stats.MaxCorpus, MaxCover: stats.MaxCover, TotalFuzzingTime: stats.TotalFuzzingTime, TotalCrashes: stats.TotalCrashes, TotalExecs: stats.TotalExecs, }) } sort.Sort(uiManagerSorter(results)) return results, nil } func loadRecentJobs(c context.Context) ([]*uiJob, error) { var jobs []*Job keys, err := datastore.NewQuery("Job"). Order("-Created"). Limit(20). GetAll(c, &jobs) if err != nil { return nil, err } var results []*uiJob for i, job := range jobs { ui := &uiJob{ Created: job.Created, BugLink: bugLink(keys[i].Parent().StringID()), ExternalLink: job.Link, User: job.User, Reporting: job.Reporting, Namespace: job.Namespace, Manager: job.Manager, BugTitle: job.BugTitle, KernelAlias: kernelRepoInfoRaw(job.KernelRepo, job.KernelBranch).Alias, PatchLink: textLink(textPatch, job.Patch), Attempts: job.Attempts, Started: job.Started, Finished: job.Finished, CrashTitle: job.CrashTitle, CrashLogLink: textLink(textCrashLog, job.CrashLog), CrashReportLink: textLink(textCrashReport, job.CrashReport), ErrorLink: textLink(textError, job.Error), } results = append(results, ui) } return results, nil } func fetchErrorLogs(c context.Context) ([]byte, error) { const ( minLogLevel = 3 maxLines = 100 maxLineLen = 1000 reportPeriod = 7 * 24 * time.Hour ) q := &log.Query{ StartTime: time.Now().Add(-reportPeriod), AppLogs: true, ApplyMinLevel: true, MinLevel: minLogLevel, } result := q.Run(c) var lines []string for i := 0; i < maxLines; i++ { rec, err := result.Next() if rec == nil { break } if err != nil { entry := fmt.Sprintf("ERROR FETCHING LOGS: %v\n", err) lines = append(lines, entry) break } for _, al := range rec.AppLogs { if al.Level < minLogLevel { continue } text := strings.Replace(al.Message, "\n", " ", -1) text = strings.Replace(text, "\r", "", -1) if len(text) > maxLineLen { text = text[:maxLineLen] } res := "" if !strings.Contains(rec.Resource, "method=log_error") { res = fmt.Sprintf(" (%v)", rec.Resource) } entry := fmt.Sprintf("%v: %v%v\n", al.Time.Format("Jan 02 15:04"), text, res) lines = append(lines, entry) } } buf := new(bytes.Buffer) for i := len(lines) - 1; i >= 0; i-- { buf.WriteString(lines[i]) } return buf.Bytes(), nil } func bugLink(id string) string { if id == "" { return "" } return "/bug?id=" + id } type uiManagerSorter []*uiManager func (a uiManagerSorter) Len() int { return len(a) } func (a uiManagerSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a uiManagerSorter) Less(i, j int) bool { if a[i].Namespace != a[j].Namespace { return a[i].Namespace < a[j].Namespace } return a[i].Name < a[j].Name } type uiBugSorter []*uiBug func (a uiBugSorter) Len() int { return len(a) } func (a uiBugSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a uiBugSorter) Less(i, j int) bool { if a[i].Namespace != a[j].Namespace { return a[i].Namespace < a[j].Namespace } if a[i].ClosedTime != a[j].ClosedTime { return a[i].ClosedTime.After(a[j].ClosedTime) } return a[i].ReportedTime.After(a[j].ReportedTime) } type uiBugGroupSorter []*uiBugGroup func (a uiBugGroupSorter) Len() int { return len(a) } func (a uiBugGroupSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a uiBugGroupSorter) Less(i, j int) bool { return a[i].ShowIndex > a[j].ShowIndex } type uiBugNamespaceSorter []*uiBugNamespace func (a uiBugNamespaceSorter) Len() int { return len(a) } func (a uiBugNamespaceSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a uiBugNamespaceSorter) Less(i, j int) bool { return a[i].Caption < a[j].Caption }