// 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 main import ( "bytes" "compress/gzip" "encoding/json" "fmt" "io/ioutil" "net/http" "reflect" "sort" "strings" "time" "unicode/utf8" "github.com/google/syzkaller/dashboard/dashapi" "github.com/google/syzkaller/pkg/auth" "github.com/google/syzkaller/pkg/email" "github.com/google/syzkaller/pkg/hash" "github.com/google/syzkaller/sys/targets" "golang.org/x/net/context" "google.golang.org/appengine/v2" db "google.golang.org/appengine/v2/datastore" "google.golang.org/appengine/v2/log" ) func initAPIHandlers() { http.Handle("/api", handleJSON(handleAPI)) } var apiHandlers = map[string]APIHandler{ "log_error": apiLogError, "job_poll": apiJobPoll, "job_done": apiJobDone, "reporting_poll_bugs": apiReportingPollBugs, "reporting_poll_notifs": apiReportingPollNotifications, "reporting_poll_closed": apiReportingPollClosed, "reporting_update": apiReportingUpdate, } var apiNamespaceHandlers = map[string]APINamespaceHandler{ "upload_build": apiUploadBuild, "builder_poll": apiBuilderPoll, "report_build_error": apiReportBuildError, "report_crash": apiReportCrash, "report_failed_repro": apiReportFailedRepro, "need_repro": apiNeedRepro, "manager_stats": apiManagerStats, "commit_poll": apiCommitPoll, "upload_commits": apiUploadCommits, "bug_list": apiBugList, "load_bug": apiLoadBug, } type JSONHandler func(c context.Context, r *http.Request) (interface{}, error) type APIHandler func(c context.Context, r *http.Request, payload []byte) (interface{}, error) type APINamespaceHandler func(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) const ( maxReproPerBug = 10 reproRetryPeriod = 24 * time.Hour // try 1 repro per day until we have at least syz repro ) // Overridable for testing. var timeNow = func(c context.Context) time.Time { return time.Now() } func timeSince(c context.Context, t time.Time) time.Duration { return timeNow(c).Sub(t) } func handleJSON(fn JSONHandler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c := appengine.NewContext(r) reply, err := fn(c, r) if err != nil { // ErrAccess is logged earlier. if err != ErrAccess { log.Errorf(c, "%v", err) } http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { w.Header().Set("Content-Encoding", "gzip") gz := gzip.NewWriter(w) if err := json.NewEncoder(gz).Encode(reply); err != nil { log.Errorf(c, "failed to encode reply: %v", err) } gz.Close() } else { if err := json.NewEncoder(w).Encode(reply); err != nil { log.Errorf(c, "failed to encode reply: %v", err) } } }) } func handleAPI(c context.Context, r *http.Request) (reply interface{}, err error) { client := r.PostFormValue("client") method := r.PostFormValue("method") log.Infof(c, "api %q from %q", method, client) auth := auth.MakeEndpoint(auth.GoogleTokenInfoEndpoint) subj, err := auth.DetermineAuthSubj(timeNow(c), r.Header["Authorization"]) if err != nil { return nil, err } // Somewhat confusingly the "key" parameter is the password. ns, err := checkClient(config, client, r.PostFormValue("key"), subj) if err != nil { if client != "" { log.Errorf(c, "%v", err) } else { // Don't log as error if somebody just invokes /api. log.Infof(c, "%v", err) } return nil, err } var payload []byte if str := r.PostFormValue("payload"); str != "" { gr, err := gzip.NewReader(strings.NewReader(str)) if err != nil { return nil, fmt.Errorf("failed to ungzip payload: %v", err) } payload, err = ioutil.ReadAll(gr) if err != nil { return nil, fmt.Errorf("failed to ungzip payload: %v", err) } if err := gr.Close(); err != nil { return nil, fmt.Errorf("failed to ungzip payload: %v", err) } } handler := apiHandlers[method] if handler != nil { return handler(c, r, payload) } nsHandler := apiNamespaceHandlers[method] if nsHandler == nil { return nil, fmt.Errorf("unknown api method %q", method) } if ns == "" { return nil, fmt.Errorf("method %q must be called within a namespace", method) } return nsHandler(c, ns, r, payload) } func apiLogError(c context.Context, r *http.Request, payload []byte) (interface{}, error) { req := new(dashapi.LogEntry) if err := json.Unmarshal(payload, req); err != nil { return nil, fmt.Errorf("failed to unmarshal request: %v", err) } log.Errorf(c, "%v: %v", req.Name, req.Text) return nil, nil } func apiBuilderPoll(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) { req := new(dashapi.BuilderPollReq) if err := json.Unmarshal(payload, req); err != nil { return nil, fmt.Errorf("failed to unmarshal request: %v", err) } bugs, _, err := loadAllBugs(c, func(query *db.Query) *db.Query { return query.Filter("Namespace=", ns). Filter("Status<", BugStatusFixed) }) if err != nil { return nil, err } m := make(map[string]bool) loop: for _, bug := range bugs { // TODO(dvyukov): include this condition into the query if possible. if len(bug.Commits) == 0 { continue } for _, mgr := range bug.PatchedOn { if mgr == req.Manager { continue loop } } for _, com := range bug.Commits { m[com] = true } } commits := make([]string, 0, len(m)) for com := range m { commits = append(commits, com) } sort.Strings(commits) resp := &dashapi.BuilderPollResp{ PendingCommits: commits, ReportEmail: reportEmail(c, ns), } return resp, nil } func reportEmail(c context.Context, ns string) string { for _, reporting := range config.Namespaces[ns].Reporting { if _, ok := reporting.Config.(*EmailConfig); ok { return ownEmail(c) } } return "" } func apiCommitPoll(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) { resp := &dashapi.CommitPollResp{ ReportEmail: reportEmail(c, ns), } for _, repo := range config.Namespaces[ns].Repos { resp.Repos = append(resp.Repos, dashapi.Repo{ URL: repo.URL, Branch: repo.Branch, }) } var bugs []*Bug _, err := db.NewQuery("Bug"). Filter("Namespace=", ns). Filter("NeedCommitInfo=", true). Project("Commits"). Limit(100). GetAll(c, &bugs) if err != nil { return nil, fmt.Errorf("failed to query bugs: %v", err) } commits := make(map[string]bool) for _, bug := range bugs { for _, com := range bug.Commits { commits[com] = true } } for com := range commits { resp.Commits = append(resp.Commits, com) } return resp, nil } func apiUploadCommits(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) { req := new(dashapi.CommitPollResultReq) if err := json.Unmarshal(payload, req); err != nil { return nil, fmt.Errorf("failed to unmarshal request: %v", err) } // This adds fixing commits to bugs. err := addCommitsToBugs(c, ns, "", nil, req.Commits) if err != nil { return nil, err } // Now add commit info to commits. for _, com := range req.Commits { if com.Hash == "" { continue } if err := addCommitInfo(c, ns, com); err != nil { return nil, err } } return nil, nil } func addCommitInfo(c context.Context, ns string, com dashapi.Commit) error { var bugs []*Bug keys, err := db.NewQuery("Bug"). Filter("Namespace=", ns). Filter("Commits=", com.Title). GetAll(c, &bugs) if err != nil { return fmt.Errorf("failed to query bugs: %v", err) } for i, bug := range bugs { if err := addCommitInfoToBug(c, bug, keys[i], com); err != nil { return err } } return nil } func addCommitInfoToBug(c context.Context, bug *Bug, bugKey *db.Key, com dashapi.Commit) error { if needUpdate, err := addCommitInfoToBugImpl(c, bug, com); err != nil { return err } else if !needUpdate { return nil } tx := func(c context.Context) error { bug := new(Bug) if err := db.Get(c, bugKey, bug); err != nil { return fmt.Errorf("failed to get bug %v: %v", bugKey.StringID(), err) } if needUpdate, err := addCommitInfoToBugImpl(c, bug, com); err != nil { return err } else if !needUpdate { return nil } if _, err := db.Put(c, bugKey, bug); err != nil { return fmt.Errorf("failed to put bug: %v", err) } return nil } return db.RunInTransaction(c, tx, nil) } func addCommitInfoToBugImpl(c context.Context, bug *Bug, com dashapi.Commit) (bool, error) { ci := -1 for i, title := range bug.Commits { if title == com.Title { ci = i break } } if ci < 0 { return false, nil } for len(bug.CommitInfo) < len(bug.Commits) { bug.CommitInfo = append(bug.CommitInfo, Commit{}) } hash0 := bug.CommitInfo[ci].Hash date0 := bug.CommitInfo[ci].Date author0 := bug.CommitInfo[ci].Author needCommitInfo0 := bug.NeedCommitInfo bug.CommitInfo[ci].Hash = com.Hash bug.CommitInfo[ci].Date = com.Date bug.CommitInfo[ci].Author = com.Author bug.NeedCommitInfo = false for i := range bug.CommitInfo { if bug.CommitInfo[i].Hash == "" { bug.NeedCommitInfo = true break } } changed := hash0 != bug.CommitInfo[ci].Hash || date0 != bug.CommitInfo[ci].Date || author0 != bug.CommitInfo[ci].Author || needCommitInfo0 != bug.NeedCommitInfo return changed, nil } func apiJobPoll(c context.Context, r *http.Request, payload []byte) (interface{}, error) { req := new(dashapi.JobPollReq) if err := json.Unmarshal(payload, req); err != nil { return nil, fmt.Errorf("failed to unmarshal request: %v", err) } if len(req.Managers) == 0 { return nil, fmt.Errorf("no managers") } return pollPendingJobs(c, req.Managers) } func apiJobDone(c context.Context, r *http.Request, payload []byte) (interface{}, error) { req := new(dashapi.JobDoneReq) if err := json.Unmarshal(payload, req); err != nil { return nil, fmt.Errorf("failed to unmarshal request: %v", err) } err := doneJob(c, req) return nil, err } func apiUploadBuild(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) { req := new(dashapi.Build) if err := json.Unmarshal(payload, req); err != nil { return nil, fmt.Errorf("failed to unmarshal request: %v", err) } now := timeNow(c) _, isNewBuild, err := uploadBuild(c, now, ns, req, BuildNormal) if err != nil { return nil, err } if isNewBuild { err := updateManager(c, ns, req.Manager, func(mgr *Manager, stats *ManagerStats) error { prevKernel, prevSyzkaller := "", "" if mgr.CurrentBuild != "" { prevBuild, err := loadBuild(c, ns, mgr.CurrentBuild) if err != nil { return err } prevKernel = prevBuild.KernelCommit prevSyzkaller = prevBuild.SyzkallerCommit } log.Infof(c, "new build on %v: kernel %v->%v syzkaller %v->%v", req.Manager, prevKernel, req.KernelCommit, prevSyzkaller, req.SyzkallerCommit) mgr.CurrentBuild = req.ID if req.KernelCommit != prevKernel { mgr.FailedBuildBug = "" } if req.SyzkallerCommit != prevSyzkaller { mgr.FailedSyzBuildBug = "" } return nil }) if err != nil { return nil, err } } if len(req.Commits) != 0 || len(req.FixCommits) != 0 { for i := range req.FixCommits { // Reset hashes just to make sure, // the build does not necessary come from the master repo, so we must not remember hashes. req.FixCommits[i].Hash = "" } if err := addCommitsToBugs(c, ns, req.Manager, req.Commits, req.FixCommits); err != nil { // We've already uploaded the build successfully and manager can use it. // Moreover, addCommitsToBugs scans all bugs and can take long time. // So just log the error. log.Errorf(c, "failed to add commits to bugs: %v", err) } } return nil, nil } func uploadBuild(c context.Context, now time.Time, ns string, req *dashapi.Build, typ BuildType) ( *Build, bool, error) { if build, err := loadBuild(c, ns, req.ID); err == nil { return build, false, nil } checkStrLen := func(str, name string, maxLen int) error { if str == "" { return fmt.Errorf("%v is empty", name) } if len(str) > maxLen { return fmt.Errorf("%v is too long (%v)", name, len(str)) } return nil } if err := checkStrLen(req.Manager, "Build.Manager", MaxStringLen); err != nil { return nil, false, err } if err := checkStrLen(req.ID, "Build.ID", MaxStringLen); err != nil { return nil, false, err } if err := checkStrLen(req.KernelRepo, "Build.KernelRepo", MaxStringLen); err != nil { return nil, false, err } if len(req.KernelBranch) > MaxStringLen { return nil, false, fmt.Errorf("Build.KernelBranch is too long (%v)", len(req.KernelBranch)) } if err := checkStrLen(req.SyzkallerCommit, "Build.SyzkallerCommit", MaxStringLen); err != nil { return nil, false, err } if len(req.CompilerID) > MaxStringLen { return nil, false, fmt.Errorf("Build.CompilerID is too long (%v)", len(req.CompilerID)) } if len(req.KernelCommit) > MaxStringLen { return nil, false, fmt.Errorf("Build.KernelCommit is too long (%v)", len(req.KernelCommit)) } configID, err := putText(c, ns, textKernelConfig, req.KernelConfig, true) if err != nil { return nil, false, err } build := &Build{ Namespace: ns, Manager: req.Manager, ID: req.ID, Type: typ, Time: now, OS: req.OS, Arch: req.Arch, VMArch: req.VMArch, SyzkallerCommit: req.SyzkallerCommit, SyzkallerCommitDate: req.SyzkallerCommitDate, CompilerID: req.CompilerID, KernelRepo: req.KernelRepo, KernelBranch: req.KernelBranch, KernelCommit: req.KernelCommit, KernelCommitTitle: req.KernelCommitTitle, KernelCommitDate: req.KernelCommitDate, KernelConfig: configID, } if _, err := db.Put(c, buildKey(c, ns, req.ID), build); err != nil { return nil, false, err } return build, true, nil } func addCommitsToBugs(c context.Context, ns, manager string, titles []string, fixCommits []dashapi.Commit) error { presentCommits := make(map[string]bool) bugFixedBy := make(map[string][]string) for _, com := range titles { presentCommits[com] = true } for _, com := range fixCommits { presentCommits[com.Title] = true for _, bugID := range com.BugIDs { bugFixedBy[bugID] = append(bugFixedBy[bugID], com.Title) } } managers, err := managerList(c, ns) if err != nil { return err } // Fetching all bugs in a namespace can be slow, and there is no way to filter only Open/Dup statuses. // So we run a separate query for each status, this both avoids fetching unnecessary data // and splits a long query into two (two smaller queries have lower chances of trigerring // timeouts than one huge). for _, status := range []int{BugStatusOpen, BugStatusDup} { err := addCommitsToBugsInStatus(c, status, ns, manager, managers, presentCommits, bugFixedBy) if err != nil { return err } } return nil } func addCommitsToBugsInStatus(c context.Context, status int, ns, manager string, managers []string, presentCommits map[string]bool, bugFixedBy map[string][]string) error { bugs, _, err := loadAllBugs(c, func(query *db.Query) *db.Query { return query.Filter("Namespace=", ns). Filter("Status=", status) }) if err != nil { return err } for _, bug := range bugs { var fixCommits []string for i := range bug.Reporting { fixCommits = append(fixCommits, bugFixedBy[bug.Reporting[i].ID]...) } sort.Strings(fixCommits) if err := addCommitsToBug(c, bug, manager, managers, fixCommits, presentCommits); err != nil { return err } if bug.Status == BugStatusDup { canon, err := canonicalBug(c, bug) if err != nil { return err } if canon.Status == BugStatusOpen && len(bug.Commits) == 0 { if err := addCommitsToBug(c, canon, manager, managers, fixCommits, presentCommits); err != nil { return err } } } } return nil } func addCommitsToBug(c context.Context, bug *Bug, manager string, managers []string, fixCommits []string, presentCommits map[string]bool) error { if !bugNeedsCommitUpdate(c, bug, manager, fixCommits, presentCommits, true) { return nil } now := timeNow(c) bugKey := bug.key(c) tx := func(c context.Context) error { bug := new(Bug) if err := db.Get(c, bugKey, bug); err != nil { return fmt.Errorf("failed to get bug %v: %v", bugKey.StringID(), err) } if !bugNeedsCommitUpdate(c, bug, manager, fixCommits, presentCommits, false) { return nil } if len(fixCommits) != 0 && !reflect.DeepEqual(bug.Commits, fixCommits) { bug.updateCommits(fixCommits, now) } if manager != "" { bug.PatchedOn = append(bug.PatchedOn, manager) if bug.Status == BugStatusOpen { fixed := true for _, mgr := range managers { if !stringInList(bug.PatchedOn, mgr) { fixed = false break } } if fixed { bug.Status = BugStatusFixed bug.Closed = now } } } if _, err := db.Put(c, bugKey, bug); err != nil { return fmt.Errorf("failed to put bug: %v", err) } return nil } return db.RunInTransaction(c, tx, nil) } func bugNeedsCommitUpdate(c context.Context, bug *Bug, manager string, fixCommits []string, presentCommits map[string]bool, dolog bool) bool { if len(fixCommits) != 0 && !reflect.DeepEqual(bug.Commits, fixCommits) { if dolog { log.Infof(c, "bug %q is fixed with %q", bug.Title, fixCommits) } return true } if len(bug.Commits) == 0 || manager == "" || stringInList(bug.PatchedOn, manager) { return false } for _, com := range bug.Commits { if !presentCommits[com] { return false } } return true } func managerList(c context.Context, ns string) ([]string, error) { var builds []*Build _, err := db.NewQuery("Build"). Filter("Namespace=", ns). Project("Manager"). Distinct(). GetAll(c, &builds) if err != nil { return nil, fmt.Errorf("failed to query builds: %v", err) } configManagers := config.Namespaces[ns].Managers var managers []string for _, build := range builds { if configManagers[build.Manager].Decommissioned { continue } managers = append(managers, build.Manager) } return managers, nil } func apiReportBuildError(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) { req := new(dashapi.BuildErrorReq) if err := json.Unmarshal(payload, req); err != nil { return nil, fmt.Errorf("failed to unmarshal request: %v", err) } now := timeNow(c) build, _, err := uploadBuild(c, now, ns, &req.Build, BuildFailed) if err != nil { return nil, err } req.Crash.BuildID = req.Build.ID bug, err := reportCrash(c, build, &req.Crash) if err != nil { return nil, err } if err := updateManager(c, ns, req.Build.Manager, func(mgr *Manager, stats *ManagerStats) error { log.Infof(c, "failed build on %v: kernel=%v", req.Build.Manager, req.Build.KernelCommit) if req.Build.KernelCommit != "" { mgr.FailedBuildBug = bug.keyHash() } else { mgr.FailedSyzBuildBug = bug.keyHash() } return nil }); err != nil { return nil, err } return nil, nil } const ( corruptedReportTitle = "corrupted report" suppressedReportTitle = "suppressed report" ) func apiReportCrash(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) { req := new(dashapi.Crash) if err := json.Unmarshal(payload, req); err != nil { return nil, fmt.Errorf("failed to unmarshal request: %v", err) } build, err := loadBuild(c, ns, req.BuildID) if err != nil { return nil, err } if !config.Namespaces[ns].TransformCrash(build, req) { return new(dashapi.ReportCrashResp), nil } bug, err := reportCrash(c, build, req) if err != nil { return nil, err } resp := &dashapi.ReportCrashResp{ NeedRepro: needRepro(c, bug), } return resp, nil } func reportCrash(c context.Context, build *Build, req *dashapi.Crash) (*Bug, error) { req.Title = canonicalizeCrashTitle(req.Title, req.Corrupted, req.Suppressed) if req.Corrupted || req.Suppressed { req.AltTitles = []string{req.Title} } else { for i, t := range req.AltTitles { req.AltTitles[i] = normalizeCrashTitle(t) } req.AltTitles = mergeStringList([]string{req.Title}, req.AltTitles) // dedup } req.Maintainers = email.MergeEmailLists(req.Maintainers) ns := build.Namespace bug, err := findBugForCrash(c, ns, req.AltTitles) if err != nil { return nil, err } if bug == nil { bug, err = createBugForCrash(c, ns, req) if err != nil { return nil, err } } bugKey := bug.key(c) now := timeNow(c) reproLevel := ReproLevelNone if len(req.ReproC) != 0 { reproLevel = ReproLevelC } else if len(req.ReproSyz) != 0 { reproLevel = ReproLevelSyz } save := reproLevel != ReproLevelNone || bug.NumCrashes < maxCrashes || now.Sub(bug.LastSavedCrash) > time.Hour || bug.NumCrashes%20 == 0 || !stringInList(bug.MergedTitles, req.Title) if save { if err := saveCrash(c, ns, req, bug, bugKey, build); err != nil { return nil, err } } else { log.Infof(c, "not saving crash for %q", bug.Title) } tx := func(c context.Context) error { bug = new(Bug) if err := db.Get(c, bugKey, bug); err != nil { return fmt.Errorf("failed to get bug: %v", err) } bug.LastTime = now if save { bug.LastSavedCrash = now } if reproLevel != ReproLevelNone { bug.NumRepro++ bug.LastReproTime = now } if bug.ReproLevel < reproLevel { bug.ReproLevel = reproLevel } if len(req.Report) != 0 { bug.HasReport = true } bug.increaseCrashStats(now) bug.HappenedOn = mergeString(bug.HappenedOn, build.Manager) // Migration of older entities (for new bugs Title is always in MergedTitles). bug.MergedTitles = mergeString(bug.MergedTitles, bug.Title) bug.MergedTitles = mergeString(bug.MergedTitles, req.Title) bug.AltTitles = mergeStringList(bug.AltTitles, req.AltTitles) if _, err = db.Put(c, bugKey, bug); err != nil { return fmt.Errorf("failed to put bug: %v", err) } return nil } if err := db.RunInTransaction(c, tx, &db.TransactionOptions{XG: true}); err != nil { return nil, err } if save { purgeOldCrashes(c, bug, bugKey) } return bug, nil } func saveCrash(c context.Context, ns string, req *dashapi.Crash, bug *Bug, bugKey *db.Key, build *Build) error { // Reporting priority of this crash. prio := int64(kernelRepoInfo(build).ReportingPriority) * 1e6 if len(req.ReproC) != 0 { prio += 4e12 } else if len(req.ReproSyz) != 0 { prio += 2e12 } if req.Title == bug.Title { prio += 1e8 // prefer reporting crash that matches bug title } if build.Arch == targets.AMD64 { prio += 1e3 } crash := &Crash{ Title: req.Title, Manager: build.Manager, BuildID: req.BuildID, Time: timeNow(c), Maintainers: email.MergeEmailLists(req.Maintainers, GetEmails(req.Recipients, dashapi.To), GetEmails(req.Recipients, dashapi.Cc)), ReproOpts: req.ReproOpts, ReportLen: prio, } var err error if crash.Log, err = putText(c, ns, textCrashLog, req.Log, false); err != nil { return err } if crash.Report, err = putText(c, ns, textCrashReport, req.Report, false); err != nil { return err } if crash.ReproSyz, err = putText(c, ns, textReproSyz, req.ReproSyz, false); err != nil { return err } if crash.ReproC, err = putText(c, ns, textReproC, req.ReproC, false); err != nil { return err } if crash.MachineInfo, err = putText(c, ns, textMachineInfo, req.MachineInfo, true); err != nil { return err } crashKey := db.NewIncompleteKey(c, "Crash", bugKey) if _, err = db.Put(c, crashKey, crash); err != nil { return fmt.Errorf("failed to put crash: %v", err) } return nil } func purgeOldCrashes(c context.Context, bug *Bug, bugKey *db.Key) { const purgeEvery = 10 if bug.NumCrashes <= 2*maxCrashes || (bug.NumCrashes-1)%purgeEvery != 0 { return } var crashes []*Crash keys, err := db.NewQuery("Crash"). Ancestor(bugKey). Filter("Reported=", time.Time{}). GetAll(c, &crashes) if err != nil { log.Errorf(c, "failed to fetch purge crashes: %v", err) return } keyMap := make(map[*Crash]*db.Key) for i, crash := range crashes { keyMap[crash] = keys[i] } // Newest first. sort.Slice(crashes, func(i, j int) bool { return crashes[i].Time.After(crashes[j].Time) }) var toDelete []*db.Key latestOnManager := make(map[string]bool) uniqueTitle := make(map[string]bool) deleted, reproCount, noreproCount := 0, 0, 0 for _, crash := range crashes { if !crash.Reported.IsZero() { log.Errorf(c, "purging reported crash?") continue } // Preserve latest crash on each manager. if !latestOnManager[crash.Manager] { latestOnManager[crash.Manager] = true continue } // Preserve at least one crash with each title. if !uniqueTitle[crash.Title] { uniqueTitle[crash.Title] = true continue } // Preserve maxCrashes latest crashes with repro and without repro. count := &noreproCount if crash.ReproSyz != 0 || crash.ReproC != 0 { count = &reproCount } if *count < maxCrashes { *count++ continue } toDelete = append(toDelete, keyMap[crash]) if crash.Log != 0 { toDelete = append(toDelete, db.NewKey(c, textCrashLog, "", crash.Log, nil)) } if crash.Report != 0 { toDelete = append(toDelete, db.NewKey(c, textCrashReport, "", crash.Report, nil)) } if crash.ReproSyz != 0 { toDelete = append(toDelete, db.NewKey(c, textReproSyz, "", crash.ReproSyz, nil)) } if crash.ReproC != 0 { toDelete = append(toDelete, db.NewKey(c, textReproC, "", crash.ReproC, nil)) } deleted++ if deleted == 2*purgeEvery { break } } if len(toDelete) == 0 { return } if err := db.DeleteMulti(c, toDelete); err != nil { log.Errorf(c, "failed to delete old crashes: %v", err) return } log.Infof(c, "deleted %v crashes for bug %q", deleted, bug.Title) } func apiReportFailedRepro(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) { req := new(dashapi.CrashID) if err := json.Unmarshal(payload, req); err != nil { return nil, fmt.Errorf("failed to unmarshal request: %v", err) } req.Title = canonicalizeCrashTitle(req.Title, req.Corrupted, req.Suppressed) bug, err := findExistingBugForCrash(c, ns, []string{req.Title}) if err != nil { return nil, err } if bug == nil { return nil, fmt.Errorf("%v: can't find bug for crash %q", ns, req.Title) } bugKey := bug.key(c) now := timeNow(c) tx := func(c context.Context) error { bug := new(Bug) if err := db.Get(c, bugKey, bug); err != nil { return fmt.Errorf("failed to get bug: %v", err) } bug.NumRepro++ bug.LastReproTime = now if _, err := db.Put(c, bugKey, bug); err != nil { return fmt.Errorf("failed to put bug: %v", err) } return nil } err = db.RunInTransaction(c, tx, &db.TransactionOptions{ XG: true, Attempts: 30, }) return nil, err } func apiNeedRepro(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) { req := new(dashapi.CrashID) if err := json.Unmarshal(payload, req); err != nil { return nil, fmt.Errorf("failed to unmarshal request: %v", err) } if req.Corrupted { resp := &dashapi.NeedReproResp{ NeedRepro: false, } return resp, nil } req.Title = canonicalizeCrashTitle(req.Title, req.Corrupted, req.Suppressed) bug, err := findExistingBugForCrash(c, ns, []string{req.Title}) if err != nil { return nil, err } if bug == nil { if req.MayBeMissing { // Manager does not send leak reports w/o repro to dashboard, we want to reproduce them. resp := &dashapi.NeedReproResp{ NeedRepro: true, } return resp, nil } return nil, fmt.Errorf("%v: can't find bug for crash %q", ns, req.Title) } resp := &dashapi.NeedReproResp{ NeedRepro: needRepro(c, bug), } return resp, nil } func canonicalizeCrashTitle(title string, corrupted, suppressed bool) string { if corrupted { // The report is corrupted and the title is most likely invalid. // Such reports are usually unactionable and are discarded. // Collect them into a single bin. return corruptedReportTitle } if suppressed { // Collect all of them into a single bucket so that it's possible to control and assess them, // e.g. if there are some spikes in suppressed reports. return suppressedReportTitle } return normalizeCrashTitle(title) } func normalizeCrashTitle(title string) string { return strings.TrimSpace(limitLength(title, maxTextLen)) } func apiManagerStats(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) { req := new(dashapi.ManagerStatsReq) if err := json.Unmarshal(payload, req); err != nil { return nil, fmt.Errorf("failed to unmarshal request: %v", err) } now := timeNow(c) err := updateManager(c, ns, req.Name, func(mgr *Manager, stats *ManagerStats) error { mgr.Link = req.Addr mgr.LastAlive = now mgr.CurrentUpTime = req.UpTime if cur := int64(req.Corpus); cur > stats.MaxCorpus { stats.MaxCorpus = cur } if cur := int64(req.PCs); cur > stats.MaxPCs { stats.MaxPCs = cur } if cur := int64(req.Cover); cur > stats.MaxCover { stats.MaxCover = cur } if cur := int64(req.CrashTypes); cur > stats.CrashTypes { stats.CrashTypes = cur } stats.TotalFuzzingTime += req.FuzzingTime stats.TotalCrashes += int64(req.Crashes) stats.SuppressedCrashes += int64(req.SuppressedCrashes) stats.TotalExecs += int64(req.Execs) return nil }) return nil, err } func apiBugList(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) { keys, err := db.NewQuery("Bug"). Filter("Namespace=", ns). KeysOnly(). GetAll(c, nil) if err != nil { return nil, fmt.Errorf("failed to query bugs: %v", err) } resp := &dashapi.BugListResp{} for _, key := range keys { resp.List = append(resp.List, key.StringID()) } return resp, nil } func apiLoadBug(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) { req := new(dashapi.LoadBugReq) if err := json.Unmarshal(payload, req); err != nil { return nil, fmt.Errorf("failed to unmarshal request: %v", err) } bug := new(Bug) bugKey := db.NewKey(c, "Bug", req.ID, 0, nil) if err := db.Get(c, bugKey, bug); err != nil { return nil, fmt.Errorf("failed to get bug: %v", err) } if bug.Namespace != ns { return nil, fmt.Errorf("no such bug") } if bug.sanitizeAccess(AccessPublic) > AccessPublic { return nil, nil } return loadBugReport(c, bug) } func loadBugReport(c context.Context, bug *Bug) (*dashapi.BugReport, error) { crash, crashKey, err := findCrashForBug(c, bug) if err != nil { return nil, err } // Create report for the last reporting so that it's stable and ExtID does not change over time. bugReporting := &bug.Reporting[len(bug.Reporting)-1] reporting := config.Namespaces[bug.Namespace].ReportingByName(bugReporting.Name) if reporting == nil { return nil, fmt.Errorf("reporting %v is missing in config", bugReporting.Name) } return createBugReport(c, bug, crash, crashKey, bugReporting, reporting) } func findExistingBugForCrash(c context.Context, ns string, titles []string) (*Bug, error) { // First, try to find an existing bug that we already used to report this crash title. var bugs []*Bug _, err := db.NewQuery("Bug"). Filter("Namespace=", ns). Filter("MergedTitles=", titles[0]). GetAll(c, &bugs) if err != nil { return nil, fmt.Errorf("failed to query bugs: %v", err) } // We can find bugs with different bug.Title and uncomparable bug.Seq's. // But there should be only one active bug for each crash title, // so if we sort by Seq, the first active bug is our target bug. sort.Slice(bugs, func(i, j int) bool { return bugs[i].Seq > bugs[j].Seq }) for _, bug := range bugs { if active, err := isActiveBug(c, bug); err != nil { return nil, err } else if active { return bug, nil } } // This is required for incremental migration. // Older bugs don't have MergedTitles, so we need to check Title as well // (reportCrash will set MergedTitles later). for _, title := range titles { _, err = db.NewQuery("Bug"). Filter("Namespace=", ns). Filter("Title=", title). Order("-Seq"). Limit(1). GetAll(c, &bugs) if err != nil { return nil, fmt.Errorf("failed to query bugs: %v", err) } if len(bugs) != 0 { bug := bugs[0] if active, err := isActiveBug(c, bug); err != nil { return nil, err } else if active { return bug, nil } } } return nil, nil } func findBugForCrash(c context.Context, ns string, titles []string) (*Bug, error) { // First, try to find an existing bug that we already used to report this crash title. bug, err := findExistingBugForCrash(c, ns, titles) if bug != nil || err != nil { return bug, err } // If there is no active bug for this crash title, try to find an existing candidate based on AltTitles. var bugs []*Bug for _, title := range titles { var bugs1 []*Bug _, err := db.NewQuery("Bug"). Filter("Namespace=", ns). Filter("AltTitles=", title). GetAll(c, &bugs1) if err != nil { return nil, fmt.Errorf("failed to query bugs: %v", err) } bugs = append(bugs, bugs1...) } // Sort to get determinism and skip inactive bugs. sort.Slice(bugs, func(i, j int) bool { if bugs[i].Title != bugs[j].Title { return bugs[i].Title < bugs[j].Title } return bugs[i].Seq > bugs[j].Seq }) var best *Bug bestPrio := 0 for i, bug := range bugs { if i != 0 && bugs[i-1].Title == bug.Title { continue // skip inactive bugs } if active, err := isActiveBug(c, bug); err != nil { return nil, err } else if !active { continue } // Generally we should have few candidates (one in most cases). // However, it's possible if e.g. we first get a data race between A<->B, // then a race between C<->D and now we handle a race between B<->D, // it can be merged into any of the previous ones. // The priority here is very basic. The only known case we want to handle is bug title renaming // where we have an active bug with title A, but then A is renamed to B and A is attached as alt title. // In such case we want to merge the new crash into the old one. However, it's also unlikely that // in this case we have any other candidates. // Overall selection algorithm can be arbitrary changed because the selection for existing crashes // is fixed with bug.MergedTitles (stable for existing bugs/crashes). prio := 0 if stringInList(titles[1:], bug.Title) { prio = 2 } else if stringInList(bug.AltTitles[1:], titles[0]) { prio = 1 } if best == nil || prio > bestPrio { best, bestPrio = bug, prio } } return best, nil } func createBugForCrash(c context.Context, ns string, req *dashapi.Crash) (*Bug, error) { var bug *Bug now := timeNow(c) tx := func(c context.Context) error { for seq := int64(0); ; seq++ { bug = new(Bug) bugHash := bugKeyHash(ns, req.Title, seq) bugKey := db.NewKey(c, "Bug", bugHash, 0, nil) if err := db.Get(c, bugKey, bug); err != nil { if err != db.ErrNoSuchEntity { return fmt.Errorf("failed to get bug: %v", err) } bug = &Bug{ Namespace: ns, Seq: seq, Title: req.Title, MergedTitles: []string{req.Title}, AltTitles: req.AltTitles, Status: BugStatusOpen, NumCrashes: 0, NumRepro: 0, ReproLevel: ReproLevelNone, HasReport: false, FirstTime: now, LastTime: now, } createBugReporting(bug, config.Namespaces[ns]) if _, err = db.Put(c, bugKey, bug); err != nil { return fmt.Errorf("failed to put new bug: %v", err) } return nil } canon, err := canonicalBug(c, bug) if err != nil { return err } if canon.Status != BugStatusOpen { continue } return nil } } if err := db.RunInTransaction(c, tx, &db.TransactionOptions{ XG: true, Attempts: 30, }); err != nil { return nil, err } return bug, nil } func createBugReporting(bug *Bug, cfg *Config) { for len(bug.Reporting) < len(cfg.Reporting) { rep := &cfg.Reporting[len(bug.Reporting)] bug.Reporting = append(bug.Reporting, BugReporting{ Name: rep.Name, ID: bugReportingHash(bug.keyHash(), rep.Name), }) } } func isActiveBug(c context.Context, bug *Bug) (bool, error) { if bug == nil { return false, nil } canon, err := canonicalBug(c, bug) if err != nil { return false, err } return canon.Status == BugStatusOpen, nil } func needRepro(c context.Context, bug *Bug) bool { if !needReproForBug(c, bug) { return false } canon, err := canonicalBug(c, bug) if err != nil { log.Errorf(c, "failed to get canonical bug: %v", err) return false } return needReproForBug(c, canon) } func needReproForBug(c context.Context, bug *Bug) bool { return bug.ReproLevel < ReproLevelC && len(bug.Commits) == 0 && bug.Title != corruptedReportTitle && bug.Title != suppressedReportTitle && config.Namespaces[bug.Namespace].NeedRepro(bug) && (bug.NumRepro < maxReproPerBug || bug.ReproLevel == ReproLevelNone && timeSince(c, bug.LastReproTime) > reproRetryPeriod) } func putText(c context.Context, ns, tag string, data []byte, dedup bool) (int64, error) { if ns == "" { return 0, fmt.Errorf("putting text outside of namespace") } if len(data) == 0 { return 0, nil } const ( // Kernel crash log is capped at ~1MB, but vm.Diagnose can add more. // These text files usually compress very well. maxTextLen = 10 << 20 maxCompressedLen = 1000 << 10 // datastore entity limit is 1MB ) if len(data) > maxTextLen { data = data[:maxTextLen] } b := new(bytes.Buffer) for { z, _ := gzip.NewWriterLevel(b, gzip.BestCompression) z.Write(data) z.Close() if len(b.Bytes()) < maxCompressedLen { break } data = data[:len(data)/10*9] b.Reset() } var key *db.Key if dedup { h := hash.Hash([]byte(ns), b.Bytes()) key = db.NewKey(c, tag, "", h.Truncate64(), nil) } else { key = db.NewIncompleteKey(c, tag, nil) } text := &Text{ Namespace: ns, Text: b.Bytes(), } key, err := db.Put(c, key, text) if err != nil { return 0, err } return key.IntID(), nil } func getText(c context.Context, tag string, id int64) ([]byte, string, error) { if id == 0 { return nil, "", nil } text := new(Text) if err := db.Get(c, db.NewKey(c, tag, "", id, nil), text); err != nil { return nil, "", fmt.Errorf("failed to read text %v: %v", tag, err) } d, err := gzip.NewReader(bytes.NewBuffer(text.Text)) if err != nil { return nil, "", fmt.Errorf("failed to read text %v: %v", tag, err) } data, err := ioutil.ReadAll(d) if err != nil { return nil, "", fmt.Errorf("failed to read text %v: %v", tag, err) } return data, text.Namespace, nil } // limitLength essentially does return s[:max], // but it ensures that we dot not split UTF-8 rune in half. // Otherwise appengine python scripts will break badly. func limitLength(s string, max int) string { s = strings.TrimSpace(s) if len(s) <= max { return s } for { s = s[:max] r, size := utf8.DecodeLastRuneInString(s) if r != utf8.RuneError || size != 1 { return s } max-- } } func GetEmails(r dashapi.Recipients, filter dashapi.RecipientType) []string { emails := []string{} for _, user := range r { if user.Type == filter { emails = append(emails, user.Address.Address) } } sort.Strings(emails) return emails } // Verifies that the given credentials are acceptable and returns the // corresponding namespace. func checkClient(conf *GlobalConfig, name0, secretPassword, oauthSubject string) (string, error) { checkAuth := func(ns, a string) (string, error) { if strings.HasPrefix(a, auth.OauthMagic) && a == oauthSubject { return ns, nil } if a != secretPassword { return ns, ErrAccess } return ns, nil } for name, authenticator := range conf.Clients { if name == name0 { return checkAuth("", authenticator) } } for ns, cfg := range conf.Namespaces { for name, authenticator := range cfg.Clients { if name == name0 { return checkAuth(ns, authenticator) } } } return "", ErrAccess }