blob: 42566a400952dea0cd1158c6856fcf1704e31d14 [file] [log] [blame]
// 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"
"github.com/google/syzkaller/pkg/html"
"github.com/google/syzkaller/pkg/vcs"
"golang.org/x/net/context"
db "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("/admin", handlerWrapper(handleAdmin))
http.Handle("/x/.config", handlerWrapper(handleTextX(textKernelConfig)))
http.Handle("/x/log.txt", handlerWrapper(handleTextX(textCrashLog)))
http.Handle("/x/report.txt", handlerWrapper(handleTextX(textCrashReport)))
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/bisect.txt", handlerWrapper(handleTextX(textLog)))
http.Handle("/x/error.txt", handlerWrapper(handleTextX(textError)))
for ns := range config.Namespaces {
http.Handle("/"+ns, handlerWrapper(handleMain))
http.Handle("/"+ns+"/fixed", handlerWrapper(handleFixed))
http.Handle("/"+ns+"/invalid", handlerWrapper(handleInvalid))
}
}
type uiMainPage struct {
Header *uiHeader
Now time.Time
FixedLink string
FixedCount int
Managers []*uiManager
Groups []*uiBugGroup
}
type uiTerminalPage struct {
Header *uiHeader
Now time.Time
Bugs *uiBugGroup
}
type uiAdminPage struct {
Header *uiHeader
Log []byte
Managers []*uiManager
Jobs []*uiJob
}
type uiManager struct {
Now time.Time
Namespace string
Name string
Link string
CoverLink string
CurrentBuild *uiBuild
FailedBuildBugLink string
FailedSyzBuildBugLink 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
SyzkallerCommitLink string
SyzkallerCommitDate time.Time
KernelAlias string
KernelCommit string
KernelCommitLink string
KernelCommitTitle string
KernelCommitDate time.Time
KernelConfigLink string
}
type uiCommit struct {
Hash string
Title string
Link string
Author string
CC []string
Date time.Time
}
type uiBugPage struct {
Header *uiHeader
Now time.Time
Bug *uiBug
BisectCause *uiJob
BisectFix *uiJob
DupOf *uiBugGroup
Dups *uiBugGroup
Similar *uiBugGroup
SampleReport []byte
Crashes *uiCrashTable
FixBisections *uiCrashTable
}
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
BisectCauseDone bool
BisectFixDone 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 []*uiCommit
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 uiCrashTable struct {
Crashes []*uiCrash
Caption string
HasMaintainers bool
}
type uiJob struct {
Type JobType
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
Duration time.Duration
CrashTitle string
CrashLogLink string
CrashReportLink string
LogLink string
ErrorLink string
Commit *uiCommit // for conclusive bisection
Commits []*uiCommit // for inconclusive bisection
Crash *uiCrash
Reported bool
}
// handleMain serves main page.
func handleMain(c context.Context, w http.ResponseWriter, r *http.Request) error {
if ns := r.FormValue("fixed"); ns != "" {
http.Redirect(w, r, fmt.Sprintf("/%v/fixed", ns), http.StatusFound)
return nil
}
hdr, err := commonHeader(c, r, w, "")
if err != nil {
return err
}
accessLevel := accessLevel(c, r)
managers, err := loadManagers(c, accessLevel, hdr.Namespace)
if err != nil {
return err
}
manager := r.FormValue("manager")
groups, fixedCount, err := fetchNamespaceBugs(c, accessLevel, hdr.Namespace, manager)
if err != nil {
return err
}
fixedLink := fmt.Sprintf("/%v/fixed", hdr.Namespace)
if manager != "" {
fixedLink = fmt.Sprintf("%v?manager=%v", fixedLink, manager)
}
data := &uiMainPage{
Header: hdr,
Now: timeNow(c),
FixedCount: fixedCount,
FixedLink: fixedLink,
Groups: groups,
Managers: managers,
}
return serveTemplate(w, "main.html", data)
}
func handleFixed(c context.Context, w http.ResponseWriter, r *http.Request) error {
return handleTerminalBugList(c, w, r, &TerminalBug{
Status: BugStatusFixed,
Subpage: "/fixed",
Caption: "fixed",
ShowPatch: true,
})
}
func handleInvalid(c context.Context, w http.ResponseWriter, r *http.Request) error {
return handleTerminalBugList(c, w, r, &TerminalBug{
Status: BugStatusInvalid,
Subpage: "/invalid",
Caption: "invalid",
ShowPatch: false,
})
}
type TerminalBug struct {
Status int
Subpage string
Caption string
ShowPatch bool
}
func handleTerminalBugList(c context.Context, w http.ResponseWriter, r *http.Request, typ *TerminalBug) error {
accessLevel := accessLevel(c, r)
hdr, err := commonHeader(c, r, w, "")
if err != nil {
return err
}
hdr.Subpage = typ.Subpage
manager := r.FormValue("manager")
bugs, err := fetchTerminalBugs(c, accessLevel, hdr.Namespace, manager, typ)
if err != nil {
return err
}
data := &uiTerminalPage{
Header: hdr,
Now: timeNow(c),
Bugs: bugs,
}
return serveTemplate(w, "terminal.html", data)
}
func handleAdmin(c context.Context, w http.ResponseWriter, r *http.Request) error {
accessLevel := accessLevel(c, r)
if accessLevel != AccessAdmin {
return ErrAccess
}
hdr, err := commonHeader(c, r, w, "")
if err != nil {
return err
}
managers, err := loadManagers(c, accessLevel, "")
if err != nil {
return err
}
errorLog, err := fetchErrorLogs(c)
if err != nil {
return err
}
jobs, err := loadRecentJobs(c)
if err != nil {
return err
}
data := &uiAdminPage{
Header: hdr,
Log: errorLog,
Managers: managers,
Jobs: jobs,
}
return serveTemplate(w, "admin.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, err := findBugByID(c, r)
if err != nil {
return ErrDontLog{err}
}
accessLevel := accessLevel(c, r)
if err := checkAccessLevel(c, r, bug.sanitizeAccess(accessLevel)); err != nil {
return err
}
hdr, err := commonHeader(c, r, w, bug.Namespace)
if 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 := db.Get(c, db.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
}
crashesTable := &uiCrashTable{
Crashes: crashes,
Caption: fmt.Sprintf("Crashes (%d)", bug.NumCrashes),
}
for _, crash := range crashesTable.Crashes {
if len(crash.Maintainers) != 0 {
crashesTable.HasMaintainers = true
break
}
}
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
}
var bisectCause *uiJob
if bug.BisectCause > BisectPending {
bisectCause, err = getUIJob(c, bug, JobBisectCause)
if err != nil {
return err
}
}
var bisectFix *uiJob
if bug.BisectFix > BisectPending {
bisectFix, err = getUIJob(c, bug, JobBisectFix)
if err != nil {
return err
}
}
data := &uiBugPage{
Header: hdr,
Now: timeNow(c),
Bug: uiBug,
BisectCause: bisectCause,
BisectFix: bisectFix,
DupOf: dupOf,
Dups: dups,
Similar: similar,
SampleReport: sampleReport,
Crashes: crashesTable,
}
// bug.BisectFix is set to BisectNot in two cases :
// - no fix bisections have been performed on the bug
// - fix bisection was performed but resulted in a crash on HEAD
if bug.BisectFix == BisectNot {
fixBisections, err := loadFixBisectionsForBug(c, bug)
if err != nil {
return err
}
if len(fixBisections) != 0 {
data.FixBisections = &uiCrashTable{
Crashes: fixBisections,
Caption: fmt.Sprintf("Fix bisections (%v)", len(fixBisections)),
}
}
}
return serveTemplate(w, "bug.html", data)
}
func findBugByID(c context.Context, r *http.Request) (*Bug, error) {
if id := r.FormValue("id"); id != "" {
bug := new(Bug)
bugKey := db.NewKey(c, "Bug", id, 0, nil)
err := db.Get(c, bugKey, bug)
return bug, err
}
if extID := r.FormValue("extid"); extID != "" {
bug, _, err := findBugByReportingID(c, extID)
return bug, err
}
return nil, fmt.Errorf("mandatory parameter id/extid is missing")
}
func getUIJob(c context.Context, bug *Bug, jobType JobType) (*uiJob, error) {
job, _, jobKey, _, err := loadBisectJob(c, bug, jobType)
if err != nil {
return nil, err
}
crash := new(Crash)
crashKey := db.NewKey(c, "Crash", "", job.CrashID, bug.key(c))
if err := db.Get(c, crashKey, crash); err != nil {
return nil, fmt.Errorf("failed to get crash: %v", err)
}
build, err := loadBuild(c, bug.Namespace, crash.BuildID)
if err != nil {
return nil, err
}
return makeUIJob(job, jobKey, crash, build), nil
}
// 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
}
bug, crash, err := checkTextAccess(c, r, tag, id)
if err != nil {
return err
}
data, ns, err := getText(c, tag, id)
if err != nil {
if strings.Contains(err.Error(), "datastore: no such entity") {
err = ErrDontLog{err}
}
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))
augmentRepro(c, w, tag, bug, crash)
w.Write(data)
return nil
}
func augmentRepro(c context.Context, w http.ResponseWriter, tag string, bug *Bug, crash *Crash) {
if tag == textReproSyz || tag == textReproC {
// Users asked for the bug link in reproducers (in case you only saved the repro link).
if bug != nil {
prefix := "#"
if tag == textReproC {
prefix = "//"
}
fmt.Fprintf(w, "%v %v/bug?id=%v\n", prefix, appURL(c), bug.keyHash())
}
}
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)
}
}
}
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 textLog:
return "bisect.txt"
case textError:
return "error.txt"
default:
return "text.txt"
}
}
func fetchNamespaceBugs(c context.Context, accessLevel AccessLevel,
ns, manager string) ([]*uiBugGroup, int, error) {
filter := func(query *db.Query) *db.Query {
query = query.Filter("Namespace=", ns)
if manager != "" {
query = query.Filter("HappenedOn=", manager)
}
return query
}
bugs, err := loadAllBugs(c, filter)
if err != nil {
return nil, 0, err
}
state, err := loadReportingState(c)
if err != nil {
return nil, 0, err
}
managers, err := managerList(c, ns)
if err != nil {
return nil, 0, 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++
continue
}
if bug.Status == BugStatusInvalid {
continue
}
if accessLevel < bug.sanitizeAccess(accessLevel) {
continue
}
if bug.Status == BugStatusDup {
dups = append(dups, bug)
continue
}
uiBug := createUIBug(c, bug, state, managers)
bugMap[bug.keyHash()] = uiBug
id := uiBug.ReportingIndex
if len(uiBug.Commits) != 0 {
id = -1
}
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)
}
cfg := config.Namespaces[ns]
var uiGroups []*uiBugGroup
for index, bugs := range groups {
sort.Slice(bugs, func(i, j int) bool {
if bugs[i].Namespace != bugs[j].Namespace {
return bugs[i].Namespace < bugs[j].Namespace
}
if bugs[i].ClosedTime != bugs[j].ClosedTime {
return bugs[i].ClosedTime.After(bugs[j].ClosedTime)
}
return bugs[i].ReportedTime.After(bugs[j].ReportedTime)
})
caption, fragment, showPatched := "", "", false
switch index {
case -1:
caption, showPatched = "fix pending", true
fragment = "pending"
case len(cfg.Reporting) - 1:
caption, showPatched = "open", false
fragment = "open"
default:
reporting := &cfg.Reporting[index]
caption, showPatched = reporting.DisplayTitle, false
fragment = reporting.Name
}
uiGroups = append(uiGroups, &uiBugGroup{
Now: timeNow(c),
Caption: caption,
Fragment: fragment,
Namespace: ns,
ShowPatched: showPatched,
ShowIndex: index,
Bugs: bugs,
})
}
sort.Slice(uiGroups, func(i, j int) bool {
return uiGroups[i].ShowIndex > uiGroups[j].ShowIndex
})
return uiGroups, fixedCount, nil
}
func fetchTerminalBugs(c context.Context, accessLevel AccessLevel,
ns, manager string, typ *TerminalBug) (*uiBugGroup, error) {
bugs, err := loadAllBugs(c, func(query *db.Query) *db.Query {
query = query.Filter("Namespace=", ns).
Filter("Status=", typ.Status)
if manager != "" {
query = query.Filter("HappenedOn=", manager)
}
return query
})
if err != nil {
return nil, err
}
state, err := loadReportingState(c)
if err != nil {
return nil, err
}
managers, err := managerList(c, ns)
if err != nil {
return nil, err
}
res := &uiBugGroup{
Now: timeNow(c),
Caption: typ.Caption,
ShowPatch: typ.ShowPatch,
Namespace: ns,
}
for _, bug := range bugs {
if accessLevel < bug.sanitizeAccess(accessLevel) {
continue
}
res.Bugs = append(res.Bugs, createUIBug(c, bug, state, managers))
}
sort.Slice(res.Bugs, func(i, j int) bool {
return res.Bugs[i].ClosedTime.After(res.Bugs[j].ClosedTime)
})
return res, nil
}
func loadDupsForBug(c context.Context, r *http.Request, bug *Bug, state *ReportingState, managers []string) (
*uiBugGroup, error) {
bugHash := bug.keyHash()
var dups []*Bug
_, err := db.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 := db.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)
domain := config.Namespaces[bug.Namespace].SimilarityDomain
for _, similar := range similar {
if accessLevel < similar.sanitizeAccess(accessLevel) {
continue
}
if similar.Namespace == bug.Namespace && similar.Seq == bug.Seq {
continue
}
if config.Namespaces[similar.Namespace].SimilarityDomain != domain {
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"
if bugReporting.Auto {
status = "auto-" + status
}
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, html.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 := bug.keyHash()
uiBug := &uiBug{
Namespace: bug.Namespace,
Title: bug.displayTitle(),
BisectCauseDone: bug.BisectCause > BisectPending,
BisectFixDone: bug.BisectFix > BisectPending,
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 {
for i, com := range bug.Commits {
cfg := config.Namespaces[bug.Namespace]
info := bug.getCommitInfo(i)
uiBug.Commits = append(uiBug.Commits, &uiCommit{
Hash: info.Hash,
Title: com,
Link: vcs.CommitLink(cfg.Repos[0].URL, info.Hash),
})
}
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
bug.BisectCauseDone = bug.BisectCauseDone || dup.BisectCause > BisectPending
bug.BisectFixDone = bug.BisectFixDone || dup.BisectFix > BisectPending
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) {
bugKey := bug.key(c)
// We can have more than maxCrashes crashes, if we have lots of reproducers.
crashes, _, err := queryCrashesForBug(c, bugKey, 2*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
}
results = append(results, makeUICrash(crash, build))
}
sampleReport, _, err := getText(c, textCrashReport, crashes[0].Report)
if err != nil {
return nil, nil, err
}
return results, sampleReport, nil
}
func loadFixBisectionsForBug(c context.Context, bug *Bug) ([]*uiCrash, error) {
bugKey := bug.key(c)
jobs, _, err := queryJobsForBug(c, bugKey, JobBisectFix)
if err != nil {
return nil, err
}
var results []*uiCrash
for _, job := range jobs {
crash, err := queryCrashForJob(c, job, bugKey)
if err != nil {
return nil, err
}
if crash == nil {
continue
}
build, err := loadBuild(c, bug.Namespace, job.BuildID)
if err != nil {
return nil, err
}
results = append(results, makeUICrash(crash, build))
}
return results, nil
}
func makeUICrash(crash *Crash, build *Build) *uiCrash {
ui := &uiCrash{
Manager: crash.Manager,
Time: crash.Time,
Maintainers: strings.Join(crash.Maintainers, ", "),
LogLink: textLink(textCrashLog, crash.Log),
ReportLink: textLink(textCrashReport, crash.Report),
ReproSyzLink: textLink(textReproSyz, crash.ReproSyz),
ReproCLink: textLink(textReproC, crash.ReproC),
}
if build != nil {
ui.uiBuild = makeUIBuild(build)
}
return ui
}
func makeUIBuild(build *Build) *uiBuild {
return &uiBuild{
Time: build.Time,
SyzkallerCommit: build.SyzkallerCommit,
SyzkallerCommitLink: vcs.LogLink(vcs.SyzkallerRepo, build.SyzkallerCommit),
SyzkallerCommitDate: build.SyzkallerCommitDate,
KernelAlias: kernelRepoInfo(build).Alias,
KernelCommit: build.KernelCommit,
KernelCommitLink: vcs.LogLink(build.KernelRepo, build.KernelCommit),
KernelCommitTitle: build.KernelCommitTitle,
KernelCommitDate: build.KernelCommitDate,
KernelConfigLink: textLink(textKernelConfig, build.KernelConfig),
}
}
func loadManagers(c context.Context, accessLevel AccessLevel, ns string) ([]*uiManager, error) {
now := timeNow(c)
date := timeDate(now)
managers, managerKeys, err := loadAllManagers(c, ns)
if err != nil {
return nil, err
}
for i := 0; i < len(managers); i++ {
if accessLevel >= config.Namespaces[managers[i].Namespace].AccessLevel {
continue
}
last := len(managers) - 1
managers[i] = managers[last]
managers = managers[:last]
managerKeys[i] = managerKeys[last]
managerKeys = managerKeys[:last]
i--
}
var buildKeys []*db.Key
var statsKeys []*db.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,
db.NewKey(c, "ManagerStats", "", int64(date), managerKeys[i]))
}
}
builds := make([]*Build, len(buildKeys))
if err := db.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 := db.GetMulti(c, statsKeys, stats); err != nil {
return nil, fmt.Errorf("fetching manager stats: %v", 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]
link := mgr.Link
if accessLevel < AccessUser {
link = ""
}
results = append(results, &uiManager{
Now: timeNow(c),
Namespace: mgr.Namespace,
Name: mgr.Name,
Link: link,
CoverLink: config.CoverPath + mgr.Name + ".html",
CurrentBuild: uiBuilds[mgr.Namespace+"|"+mgr.CurrentBuild],
FailedBuildBugLink: bugLink(mgr.FailedBuildBug),
FailedSyzBuildBugLink: bugLink(mgr.FailedSyzBuildBug),
LastActive: mgr.LastAlive,
LastActiveBad: now.Sub(mgr.LastAlive) > 6*time.Hour,
CurrentUpTime: mgr.CurrentUpTime,
MaxCorpus: stats.MaxCorpus,
MaxCover: stats.MaxCover,
TotalFuzzingTime: stats.TotalFuzzingTime,
TotalCrashes: stats.TotalCrashes,
TotalExecs: stats.TotalExecs,
})
}
sort.Slice(results, func(i, j int) bool {
if results[i].Namespace != results[j].Namespace {
return results[i].Namespace < results[j].Namespace
}
return results[i].Name < results[j].Name
})
return results, nil
}
func loadRecentJobs(c context.Context) ([]*uiJob, error) {
var jobs []*Job
keys, err := db.NewQuery("Job").
Order("-Created").
Limit(80).
GetAll(c, &jobs)
if err != nil {
return nil, err
}
var results []*uiJob
for i, job := range jobs {
results = append(results, makeUIJob(job, keys[i], nil, nil))
}
return results, nil
}
func makeUIJob(job *Job, jobKey *db.Key, crash *Crash, build *Build) *uiJob {
ui := &uiJob{
Type: job.Type,
Created: job.Created,
BugLink: bugLink(jobKey.Parent().StringID()),
ExternalLink: job.Link,
User: job.User,
Reporting: job.Reporting,
Namespace: job.Namespace,
Manager: job.Manager,
BugTitle: job.BugTitle,
KernelAlias: kernelRepoInfoRaw(job.Namespace, 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),
LogLink: textLink(textLog, job.Log),
ErrorLink: textLink(textError, job.Error),
}
if !job.Finished.IsZero() {
ui.Duration = job.Finished.Sub(job.Started)
}
for _, com := range job.Commits {
ui.Commits = append(ui.Commits, &uiCommit{
Hash: com.Hash,
Title: com.Title,
Author: fmt.Sprintf("%v <%v>", com.AuthorName, com.Author),
CC: strings.Split(com.CC, "|"),
Date: com.Date,
})
}
if len(ui.Commits) == 1 {
ui.Commit = ui.Commits[0]
ui.Commits = nil
}
if crash != nil {
ui.Crash = makeUICrash(crash, build)
}
return ui
}
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
}