| // 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" |
| "encoding/json" |
| "fmt" |
| "reflect" |
| "sort" |
| "strings" |
| "time" |
| |
| "github.com/google/syzkaller/dashboard/dashapi" |
| "github.com/google/syzkaller/pkg/email" |
| "github.com/google/syzkaller/pkg/html" |
| "golang.org/x/net/context" |
| db "google.golang.org/appengine/datastore" |
| "google.golang.org/appengine/log" |
| ) |
| |
| // Backend-independent reporting logic. |
| // Two main entry points: |
| // - reportingPoll is called by backends to get list of bugs that need to be reported. |
| // - incomingCommand is called by backends to update bug statuses. |
| |
| const ( |
| maxMailLogLen = 1 << 20 |
| maxMailReportLen = 64 << 10 |
| maxInlineError = 16 << 10 |
| notifyResendPeriod = 14 * 24 * time.Hour |
| notifyAboutBadCommitPeriod = 90 * 24 * time.Hour |
| autoObsoletePeriod = 180 * 24 * time.Hour |
| internalError = "internal error" |
| // This is embedded as first line of syzkaller reproducer files. |
| syzReproPrefix = "# See https://goo.gl/kgGztJ for information about syzkaller reproducers.\n" |
| ) |
| |
| // reportingPoll is called by backends to get list of bugs that need to be reported. |
| func reportingPollBugs(c context.Context, typ string) []*dashapi.BugReport { |
| state, err := loadReportingState(c) |
| if err != nil { |
| log.Errorf(c, "%v", err) |
| return nil |
| } |
| var bugs []*Bug |
| _, err = db.NewQuery("Bug"). |
| Filter("Status<", BugStatusFixed). |
| GetAll(c, &bugs) |
| if err != nil { |
| log.Errorf(c, "%v", err) |
| return nil |
| } |
| log.Infof(c, "fetched %v bugs", len(bugs)) |
| sort.Sort(bugReportSorter(bugs)) |
| var reports []*dashapi.BugReport |
| for _, bug := range bugs { |
| rep, err := handleReportBug(c, typ, state, bug) |
| if err != nil { |
| log.Errorf(c, "%v: failed to report bug %v: %v", bug.Namespace, bug.Title, err) |
| continue |
| } |
| if rep == nil { |
| continue |
| } |
| reports = append(reports, rep) |
| } |
| return reports |
| } |
| |
| func handleReportBug(c context.Context, typ string, state *ReportingState, bug *Bug) ( |
| *dashapi.BugReport, error) { |
| reporting, bugReporting, crash, crashKey, _, _, _, err := needReport(c, typ, state, bug) |
| if err != nil || reporting == nil { |
| return nil, err |
| } |
| rep, err := createBugReport(c, bug, crash, crashKey, bugReporting, reporting) |
| if err != nil { |
| return nil, err |
| } |
| log.Infof(c, "bug %q: reporting to %v", bug.Title, reporting.Name) |
| return rep, nil |
| } |
| |
| func needReport(c context.Context, typ string, state *ReportingState, bug *Bug) ( |
| reporting *Reporting, bugReporting *BugReporting, crash *Crash, |
| crashKey *db.Key, reportingIdx int, status, link string, err error) { |
| reporting, bugReporting, reportingIdx, status, err = currentReporting(c, bug) |
| if err != nil || reporting == nil { |
| return |
| } |
| if typ != "" && typ != reporting.Config.Type() { |
| status = "on a different reporting" |
| reporting, bugReporting = nil, nil |
| return |
| } |
| link = bugReporting.Link |
| if !bugReporting.Reported.IsZero() && bugReporting.ReproLevel >= bug.ReproLevel { |
| status = fmt.Sprintf("%v: reported%v on %v", |
| reporting.DisplayTitle, reproStr(bugReporting.ReproLevel), |
| html.FormatTime(bugReporting.Reported)) |
| reporting, bugReporting = nil, nil |
| return |
| } |
| ent := state.getEntry(timeNow(c), bug.Namespace, reporting.Name) |
| cfg := config.Namespaces[bug.Namespace] |
| if timeSince(c, bug.FirstTime) < cfg.ReportingDelay { |
| status = fmt.Sprintf("%v: initial reporting delay", reporting.DisplayTitle) |
| reporting, bugReporting = nil, nil |
| return |
| } |
| if bug.ReproLevel < ReproLevelC && timeSince(c, bug.FirstTime) < cfg.WaitForRepro { |
| status = fmt.Sprintf("%v: waiting for C repro", reporting.DisplayTitle) |
| reporting, bugReporting = nil, nil |
| return |
| } |
| if !cfg.MailWithoutReport && !bug.HasReport { |
| status = fmt.Sprintf("%v: no report", reporting.DisplayTitle) |
| reporting, bugReporting = nil, nil |
| return |
| } |
| |
| crash, crashKey, err = findCrashForBug(c, bug) |
| if err != nil { |
| status = fmt.Sprintf("%v: no crashes!", reporting.DisplayTitle) |
| reporting, bugReporting = nil, nil |
| return |
| } |
| |
| // Limit number of reports sent per day, |
| // but don't limit sending repros to already reported bugs. |
| if bugReporting.Reported.IsZero() && reporting.DailyLimit != 0 && |
| ent.Sent >= reporting.DailyLimit { |
| status = fmt.Sprintf("%v: out of quota for today", reporting.DisplayTitle) |
| reporting, bugReporting = nil, nil |
| return |
| } |
| |
| // Ready to be reported. |
| if bugReporting.Reported.IsZero() { |
| // This update won't be committed, but it will prevent us from |
| // reporting too many bugs in a single poll. |
| ent.Sent++ |
| } |
| status = fmt.Sprintf("%v: ready to report", reporting.DisplayTitle) |
| if !bugReporting.Reported.IsZero() { |
| status += fmt.Sprintf(" (reported%v on %v)", |
| reproStr(bugReporting.ReproLevel), html.FormatTime(bugReporting.Reported)) |
| } |
| return |
| } |
| |
| func reportingPollNotifications(c context.Context, typ string) []*dashapi.BugNotification { |
| var bugs []*Bug |
| _, err := db.NewQuery("Bug"). |
| Filter("Status<", BugStatusFixed). |
| GetAll(c, &bugs) |
| if err != nil { |
| log.Errorf(c, "%v", err) |
| return nil |
| } |
| log.Infof(c, "fetched %v bugs", len(bugs)) |
| var notifs []*dashapi.BugNotification |
| for _, bug := range bugs { |
| notif, err := handleReportNotif(c, typ, bug) |
| if err != nil { |
| log.Errorf(c, "%v: failed to create bug notif %v: %v", bug.Namespace, bug.Title, err) |
| continue |
| } |
| if notif == nil { |
| continue |
| } |
| notifs = append(notifs, notif) |
| if len(notifs) >= 10 { |
| break // don't send too many at once just in case |
| } |
| } |
| return notifs |
| } |
| |
| func handleReportNotif(c context.Context, typ string, bug *Bug) (*dashapi.BugNotification, error) { |
| reporting, bugReporting, _, _, err := currentReporting(c, bug) |
| if err != nil || reporting == nil { |
| return nil, nil |
| } |
| if typ != "" && typ != reporting.Config.Type() { |
| return nil, nil |
| } |
| if bug.Status != BugStatusOpen || bugReporting.Reported.IsZero() { |
| return nil, nil |
| } |
| |
| if reporting.moderation && |
| reporting.Embargo != 0 && |
| len(bug.Commits) == 0 && |
| bugReporting.OnHold.IsZero() && |
| timeSince(c, bugReporting.Reported) > reporting.Embargo { |
| log.Infof(c, "%v: upstreaming (embargo): %v", bug.Namespace, bug.Title) |
| return createNotification(c, dashapi.BugNotifUpstream, true, "", bug, reporting, bugReporting) |
| } |
| if reporting.moderation && |
| len(bug.Commits) == 0 && |
| bugReporting.OnHold.IsZero() && |
| reporting.Filter(bug) == FilterSkip { |
| log.Infof(c, "%v: upstreaming (skip): %v", bug.Namespace, bug.Title) |
| return createNotification(c, dashapi.BugNotifUpstream, true, "", bug, reporting, bugReporting) |
| } |
| if len(bug.Commits) == 0 && |
| bug.ReproLevel == ReproLevelNone && |
| timeSince(c, bug.LastActivity) > notifyResendPeriod && |
| timeSince(c, bug.LastTime) > autoObsoletePeriod { |
| log.Infof(c, "%v: obsoleting: %v", bug.Namespace, bug.Title) |
| return createNotification(c, dashapi.BugNotifObsoleted, false, "", bug, reporting, bugReporting) |
| } |
| if len(bug.Commits) > 0 && |
| len(bug.PatchedOn) == 0 && |
| timeSince(c, bug.LastActivity) > notifyResendPeriod && |
| timeSince(c, bug.FixTime) > notifyAboutBadCommitPeriod { |
| log.Infof(c, "%v: bad fix commit: %v", bug.Namespace, bug.Title) |
| commits := strings.Join(bug.Commits, "\n") |
| return createNotification(c, dashapi.BugNotifBadCommit, true, commits, bug, reporting, bugReporting) |
| } |
| return nil, nil |
| } |
| |
| func createNotification(c context.Context, typ dashapi.BugNotif, public bool, text string, bug *Bug, |
| reporting *Reporting, bugReporting *BugReporting) (*dashapi.BugNotification, error) { |
| reportingConfig, err := json.Marshal(reporting.Config) |
| if err != nil { |
| return nil, err |
| } |
| crash, _, err := findCrashForBug(c, bug) |
| if err != nil { |
| return nil, fmt.Errorf("no crashes for bug") |
| } |
| build, err := loadBuild(c, bug.Namespace, crash.BuildID) |
| if err != nil { |
| return nil, err |
| } |
| kernelRepo := kernelRepoInfo(build) |
| notif := &dashapi.BugNotification{ |
| Type: typ, |
| Namespace: bug.Namespace, |
| Config: reportingConfig, |
| ID: bugReporting.ID, |
| ExtID: bugReporting.ExtID, |
| Title: bug.displayTitle(), |
| Text: text, |
| Public: public, |
| } |
| if public { |
| notif.Maintainers = append(crash.Maintainers, kernelRepo.CC...) |
| } |
| if (public || reporting.moderation) && bugReporting.CC != "" { |
| notif.CC = strings.Split(bugReporting.CC, "|") |
| } |
| return notif, nil |
| } |
| |
| func currentReporting(c context.Context, bug *Bug) (*Reporting, *BugReporting, int, string, error) { |
| for i := range bug.Reporting { |
| bugReporting := &bug.Reporting[i] |
| if !bugReporting.Closed.IsZero() { |
| continue |
| } |
| reporting := config.Namespaces[bug.Namespace].ReportingByName(bugReporting.Name) |
| if reporting == nil { |
| return nil, nil, 0, "", fmt.Errorf("%v: missing in config", bugReporting.Name) |
| } |
| switch reporting.Filter(bug) { |
| case FilterSkip: |
| if bugReporting.Reported.IsZero() { |
| continue |
| } |
| fallthrough |
| case FilterReport: |
| return reporting, bugReporting, i, "", nil |
| case FilterHold: |
| return nil, nil, 0, fmt.Sprintf("%v: reporting suspended", reporting.DisplayTitle), nil |
| } |
| } |
| return nil, nil, 0, "", fmt.Errorf("no reporting left") |
| } |
| |
| func reproStr(level dashapi.ReproLevel) string { |
| switch level { |
| case ReproLevelSyz: |
| return " syz repro" |
| case ReproLevelC: |
| return " C repro" |
| default: |
| return "" |
| } |
| } |
| |
| func createBugReport(c context.Context, bug *Bug, crash *Crash, crashKey *db.Key, |
| bugReporting *BugReporting, reporting *Reporting) (*dashapi.BugReport, error) { |
| reportingConfig, err := json.Marshal(reporting.Config) |
| if err != nil { |
| return nil, err |
| } |
| var job *Job |
| if bug.BisectCause == BisectYes { |
| // If we have bisection results, report the crash/repro used for bisection. |
| job1, crash1, _, crashKey1, err := loadBisectJob(c, bug) |
| if err != nil { |
| return nil, err |
| } |
| job = job1 |
| if crash1.ReproC != 0 || crash.ReproC == 0 { |
| // Don't override the crash in this case, |
| // otherwise we will always think that we haven't reported the C repro. |
| crash, crashKey = crash1, crashKey1 |
| } |
| } |
| crashLog, _, err := getText(c, textCrashLog, crash.Log) |
| if err != nil { |
| return nil, err |
| } |
| if len(crashLog) > maxMailLogLen { |
| crashLog = crashLog[len(crashLog)-maxMailLogLen:] |
| } |
| report, _, err := getText(c, textCrashReport, crash.Report) |
| if err != nil { |
| return nil, err |
| } |
| if len(report) > maxMailReportLen { |
| report = report[:maxMailReportLen] |
| } |
| reproC, _, err := getText(c, textReproC, crash.ReproC) |
| if err != nil { |
| return nil, err |
| } |
| reproSyz, _, err := getText(c, textReproSyz, crash.ReproSyz) |
| if err != nil { |
| return nil, err |
| } |
| if len(reproSyz) != 0 { |
| buf := new(bytes.Buffer) |
| buf.WriteString(syzReproPrefix) |
| if len(crash.ReproOpts) != 0 { |
| fmt.Fprintf(buf, "#%s\n", crash.ReproOpts) |
| } |
| buf.Write(reproSyz) |
| reproSyz = buf.Bytes() |
| } |
| build, err := loadBuild(c, bug.Namespace, crash.BuildID) |
| if err != nil { |
| return nil, err |
| } |
| typ := dashapi.ReportNew |
| if !bugReporting.Reported.IsZero() { |
| typ = dashapi.ReportRepro |
| } |
| |
| kernelRepo := kernelRepoInfo(build) |
| rep := &dashapi.BugReport{ |
| Type: typ, |
| Config: reportingConfig, |
| ExtID: bugReporting.ExtID, |
| First: bugReporting.Reported.IsZero(), |
| Moderation: reporting.moderation, |
| Log: crashLog, |
| LogLink: externalLink(c, textCrashLog, crash.Log), |
| Report: report, |
| ReportLink: externalLink(c, textCrashReport, crash.Report), |
| Maintainers: append(crash.Maintainers, kernelRepo.CC...), |
| ReproC: reproC, |
| ReproCLink: externalLink(c, textReproC, crash.ReproC), |
| ReproSyz: reproSyz, |
| ReproSyzLink: externalLink(c, textReproSyz, crash.ReproSyz), |
| CrashID: crashKey.IntID(), |
| NumCrashes: bug.NumCrashes, |
| HappenedOn: managersToRepos(c, bug.Namespace, bug.HappenedOn), |
| } |
| if bugReporting.CC != "" { |
| rep.CC = strings.Split(bugReporting.CC, "|") |
| } |
| if bug.BisectCause == BisectYes { |
| rep.BisectCause = bisectFromJob(c, rep, job) |
| } |
| if err := fillBugReport(c, rep, bug, bugReporting, build); err != nil { |
| return nil, err |
| } |
| return rep, nil |
| } |
| |
| // fillBugReport fills common report fields for bug and job reports. |
| func fillBugReport(c context.Context, rep *dashapi.BugReport, bug *Bug, bugReporting *BugReporting, |
| build *Build) error { |
| kernelConfig, _, err := getText(c, textKernelConfig, build.KernelConfig) |
| if err != nil { |
| return err |
| } |
| creditEmail, err := email.AddAddrContext(ownEmail(c), bugReporting.ID) |
| if err != nil { |
| return err |
| } |
| rep.Namespace = bug.Namespace |
| rep.ID = bugReporting.ID |
| rep.Title = bug.displayTitle() |
| rep.Link = fmt.Sprintf("%v/bug?extid=%v", appURL(c), bugReporting.ID) |
| rep.CreditEmail = creditEmail |
| rep.OS = build.OS |
| rep.Arch = build.Arch |
| rep.VMArch = build.VMArch |
| rep.UserSpaceArch = kernelArch(build.Arch) |
| rep.CompilerID = build.CompilerID |
| rep.KernelRepo = build.KernelRepo |
| rep.KernelRepoAlias = kernelRepoInfo(build).Alias |
| rep.KernelBranch = build.KernelBranch |
| rep.KernelCommit = build.KernelCommit |
| rep.KernelCommitTitle = build.KernelCommitTitle |
| rep.KernelCommitDate = build.KernelCommitDate |
| rep.KernelConfig = kernelConfig |
| rep.KernelConfigLink = externalLink(c, textKernelConfig, build.KernelConfig) |
| for _, addr := range bug.UNCC { |
| rep.CC = email.RemoveFromEmailList(rep.CC, addr) |
| rep.Maintainers = email.RemoveFromEmailList(rep.Maintainers, addr) |
| } |
| return nil |
| } |
| |
| func loadBisectJob(c context.Context, bug *Bug) (*Job, *Crash, *db.Key, *db.Key, error) { |
| bugKey := bug.key(c) |
| var jobs []*Job |
| keys, err := db.NewQuery("Job"). |
| Ancestor(bugKey). |
| Filter("Type=", JobBisectCause). |
| Filter("Finished>", time.Time{}). |
| Order("-Finished"). |
| Limit(1). |
| GetAll(c, &jobs) |
| if err != nil { |
| return nil, nil, nil, nil, fmt.Errorf("failed to query jobs: %v", err) |
| } |
| if len(jobs) == 0 { |
| return nil, nil, nil, nil, fmt.Errorf("can't find bisect cause job for bug") |
| } |
| job := jobs[0] |
| crash := new(Crash) |
| crashKey := db.NewKey(c, "Crash", "", job.CrashID, bugKey) |
| if err := db.Get(c, crashKey, crash); err != nil { |
| return nil, nil, nil, nil, fmt.Errorf("failed to get crash: %v", err) |
| } |
| return job, crash, keys[0], crashKey, nil |
| } |
| |
| func managersToRepos(c context.Context, ns string, managers []string) []string { |
| var repos []string |
| dedup := make(map[string]bool) |
| for _, manager := range managers { |
| build, err := lastManagerBuild(c, ns, manager) |
| if err != nil { |
| log.Errorf(c, "failed to get manager %q build: %v", manager, err) |
| continue |
| } |
| repo := kernelRepoInfo(build).Alias |
| if dedup[repo] { |
| continue |
| } |
| dedup[repo] = true |
| repos = append(repos, repo) |
| } |
| sort.Strings(repos) |
| return repos |
| } |
| |
| func foreachBug(c context.Context, fn func(bug *Bug) error) error { |
| const batchSize = 1000 |
| for offset := 0; ; offset += batchSize { |
| var bugs []*Bug |
| _, err := db.NewQuery("Bug"). |
| Offset(offset). |
| Limit(batchSize). |
| GetAll(c, &bugs) |
| if err != nil { |
| return fmt.Errorf("foreachBug: failed to query bugs: %v", err) |
| } |
| for _, bug := range bugs { |
| if err := fn(bug); err != nil { |
| return err |
| } |
| } |
| if len(bugs) < batchSize { |
| return nil |
| } |
| } |
| } |
| |
| // reportingPollClosed is called by backends to get list of closed bugs. |
| func reportingPollClosed(c context.Context, ids []string) ([]string, error) { |
| idMap := make(map[string]bool, len(ids)) |
| for _, id := range ids { |
| idMap[id] = true |
| } |
| var closed []string |
| err := foreachBug(c, func(bug *Bug) error { |
| for i := range bug.Reporting { |
| bugReporting := &bug.Reporting[i] |
| if !idMap[bugReporting.ID] { |
| continue |
| } |
| var err error |
| bug, err = canonicalBug(c, bug) |
| if err != nil { |
| log.Errorf(c, "%v", err) |
| break |
| } |
| if bug.Status >= BugStatusFixed || !bugReporting.Closed.IsZero() { |
| closed = append(closed, bugReporting.ID) |
| } |
| break |
| } |
| return nil |
| }) |
| return closed, err |
| } |
| |
| // incomingCommand is entry point to bug status updates. |
| func incomingCommand(c context.Context, cmd *dashapi.BugUpdate) (bool, string, error) { |
| log.Infof(c, "got command: %+v", cmd) |
| ok, reason, err := incomingCommandImpl(c, cmd) |
| if err != nil { |
| log.Errorf(c, "%v (%v)", reason, err) |
| } else if !ok && reason != "" { |
| log.Errorf(c, "invalid update: %v", reason) |
| } |
| return ok, reason, err |
| } |
| |
| func incomingCommandImpl(c context.Context, cmd *dashapi.BugUpdate) (bool, string, error) { |
| for i, com := range cmd.FixCommits { |
| if len(com) >= 2 && com[0] == '"' && com[len(com)-1] == '"' { |
| com = com[1 : len(com)-1] |
| cmd.FixCommits[i] = com |
| } |
| if len(com) < 3 { |
| return false, fmt.Sprintf("bad commit title: %q", com), nil |
| } |
| } |
| bug, bugKey, err := findBugByReportingID(c, cmd.ID) |
| if err != nil { |
| return false, internalError, err |
| } |
| now := timeNow(c) |
| dupHash := "" |
| if cmd.Status == dashapi.BugStatusDup { |
| dupHash1, ok, reason, err := findDupBug(c, cmd, bug, bugKey) |
| if !ok || err != nil { |
| return ok, reason, err |
| } |
| dupHash = dupHash1 |
| } |
| |
| ok, reply := false, "" |
| tx := func(c context.Context) error { |
| var err error |
| ok, reply, err = incomingCommandTx(c, now, cmd, bugKey, dupHash) |
| return err |
| } |
| err = db.RunInTransaction(c, tx, &db.TransactionOptions{ |
| XG: true, |
| // Default is 3 which fails sometimes. |
| // We don't want incoming bug updates to fail, |
| // because for e.g. email we won't have an external retry. |
| Attempts: 30, |
| }) |
| if err != nil { |
| return false, internalError, err |
| } |
| return ok, reply, nil |
| } |
| |
| func findDupBug(c context.Context, cmd *dashapi.BugUpdate, bug *Bug, bugKey *db.Key) ( |
| string, bool, string, error) { |
| bugReporting, _ := bugReportingByID(bug, cmd.ID) |
| dup, dupKey, err := findBugByReportingID(c, cmd.DupOf) |
| if err != nil { |
| // Email reporting passes bug title in cmd.DupOf, try to find bug by title. |
| dup, dupKey, err = findDupByTitle(c, bug.Namespace, cmd.DupOf) |
| if err != nil { |
| return "", false, "can't find the dup bug", err |
| } |
| dupReporting := lastReportedReporting(dup) |
| if dupReporting == nil { |
| return "", false, "can't find the dup bug", |
| fmt.Errorf("dup does not have reporting %q", bugReporting.Name) |
| } |
| cmd.DupOf = dupReporting.ID |
| } |
| dupReporting, _ := bugReportingByID(dup, cmd.DupOf) |
| if bugReporting == nil || dupReporting == nil { |
| return "", false, internalError, fmt.Errorf("can't find bug reporting") |
| } |
| if bugKey.StringID() == dupKey.StringID() { |
| if bugReporting.Name == dupReporting.Name { |
| return "", false, "Can't dup bug to itself.", nil |
| } |
| return "", false, fmt.Sprintf("Can't dup bug to itself in different reporting (%v->%v).\n"+ |
| "Please dup syzbot bugs only onto syzbot bugs for the same kernel/reporting.", |
| bugReporting.Name, dupReporting.Name), nil |
| } |
| if bug.Namespace != dup.Namespace { |
| return "", false, fmt.Sprintf("Duplicate bug corresponds to a different kernel (%v->%v).\n"+ |
| "Please dup syzbot bugs only onto syzbot bugs for the same kernel.", |
| bug.Namespace, dup.Namespace), nil |
| } |
| if !allowCrossReportingDup(c, bug, dup, bugReporting, dupReporting) { |
| return "", false, fmt.Sprintf("Can't dup bug to a bug in different reporting (%v->%v)."+ |
| "Please dup syzbot bugs only onto syzbot bugs for the same kernel/reporting.", |
| bugReporting.Name, dupReporting.Name), nil |
| } |
| dupCanon, err := canonicalBug(c, dup) |
| if err != nil { |
| return "", false, internalError, fmt.Errorf("failed to get canonical bug for dup: %v", err) |
| } |
| if !dupReporting.Closed.IsZero() && dupCanon.Status == BugStatusOpen { |
| return "", false, "Dup bug is already upstreamed.", nil |
| } |
| dupHash := dup.keyHash() |
| return dupHash, true, "", nil |
| } |
| |
| func allowCrossReportingDup(c context.Context, bug, dup *Bug, |
| bugReporting, dupReporting *BugReporting) bool { |
| bugIdx := getReportingIdx(c, bug, bugReporting) |
| dupIdx := getReportingIdx(c, dup, dupReporting) |
| if bugIdx < 0 || dupIdx < 0 { |
| return false |
| } |
| if bugIdx == dupIdx { |
| return true |
| } |
| // We generally allow duping only within the same reporting. |
| // But there is one exception: we also allow duping from last but one |
| // reporting to the last one (which is stable, final destination) |
| // provided that these two reportings have the same access level and type. |
| // The rest of the combinations can lead to surprising states and |
| // information hiding, so we don't allow them. |
| cfg := config.Namespaces[bug.Namespace] |
| bugConfig := &cfg.Reporting[bugIdx] |
| dupConfig := &cfg.Reporting[dupIdx] |
| lastIdx := len(cfg.Reporting) - 1 |
| return bugIdx == lastIdx-1 && dupIdx == lastIdx && |
| bugConfig.AccessLevel == dupConfig.AccessLevel && |
| bugConfig.Config.Type() == dupConfig.Config.Type() |
| } |
| |
| func getReportingIdx(c context.Context, bug *Bug, bugReporting *BugReporting) int { |
| for i := range bug.Reporting { |
| if bug.Reporting[i].Name == bugReporting.Name { |
| return i |
| } |
| } |
| log.Errorf(c, "failed to find bug reporting by name: %q/%q", bug.Title, bugReporting.Name) |
| return -1 |
| } |
| |
| func incomingCommandTx(c context.Context, now time.Time, cmd *dashapi.BugUpdate, |
| bugKey *db.Key, dupHash string) (bool, string, error) { |
| bug := new(Bug) |
| if err := db.Get(c, bugKey, bug); err != nil { |
| return false, internalError, fmt.Errorf("can't find the corresponding bug: %v", err) |
| } |
| bugReporting, final := bugReportingByID(bug, cmd.ID) |
| if bugReporting == nil { |
| return false, internalError, fmt.Errorf("can't find bug reporting") |
| } |
| if ok, reply, err := checkBugStatus(c, cmd, bug, bugReporting); !ok { |
| return false, reply, err |
| } |
| state, err := loadReportingState(c) |
| if err != nil { |
| return false, internalError, err |
| } |
| stateEnt := state.getEntry(now, bug.Namespace, bugReporting.Name) |
| if ok, reply, err := incomingCommandCmd(c, now, cmd, bug, bugReporting, final, dupHash, stateEnt); !ok { |
| return false, reply, err |
| } |
| if len(cmd.FixCommits) != 0 && (bug.Status == BugStatusOpen || bug.Status == BugStatusDup) { |
| sort.Strings(cmd.FixCommits) |
| if !reflect.DeepEqual(bug.Commits, cmd.FixCommits) { |
| bug.updateCommits(cmd.FixCommits, now) |
| } |
| } |
| if cmd.CrashID != 0 { |
| // Rememeber that we've reported this crash. |
| if err := markCrashReported(c, cmd.CrashID, bugKey, now); err != nil { |
| return false, internalError, err |
| } |
| bugReporting.CrashID = cmd.CrashID |
| } |
| if bugReporting.ExtID == "" { |
| bugReporting.ExtID = cmd.ExtID |
| } |
| if bugReporting.Link == "" { |
| bugReporting.Link = cmd.Link |
| } |
| if len(cmd.CC) != 0 && cmd.Status != dashapi.BugStatusUnCC { |
| merged := email.MergeEmailLists(strings.Split(bugReporting.CC, "|"), cmd.CC) |
| bugReporting.CC = strings.Join(merged, "|") |
| } |
| if bugReporting.ReproLevel < cmd.ReproLevel { |
| bugReporting.ReproLevel = cmd.ReproLevel |
| } |
| if bug.Status != BugStatusDup { |
| bug.DupOf = "" |
| } |
| if cmd.Status != dashapi.BugStatusOpen || !cmd.OnHold { |
| bugReporting.OnHold = time.Time{} |
| } |
| bug.LastActivity = now |
| if _, err := db.Put(c, bugKey, bug); err != nil { |
| return false, internalError, fmt.Errorf("failed to put bug: %v", err) |
| } |
| if err := saveReportingState(c, state); err != nil { |
| return false, internalError, err |
| } |
| return true, "", nil |
| } |
| |
| func incomingCommandCmd(c context.Context, now time.Time, cmd *dashapi.BugUpdate, |
| bug *Bug, bugReporting *BugReporting, final bool, dupHash string, |
| stateEnt *ReportingStateEntry) (bool, string, error) { |
| switch cmd.Status { |
| case dashapi.BugStatusOpen: |
| bug.Status = BugStatusOpen |
| bug.Closed = time.Time{} |
| if bugReporting.Reported.IsZero() { |
| bugReporting.Reported = now |
| stateEnt.Sent++ // sending repro does not count against the quota |
| } |
| if bugReporting.OnHold.IsZero() && cmd.OnHold { |
| bugReporting.OnHold = now |
| } |
| // Close all previous reporting if they are not closed yet |
| // (can happen due to Status == ReportingDisabled). |
| for i := range bug.Reporting { |
| if bugReporting == &bug.Reporting[i] { |
| break |
| } |
| if bug.Reporting[i].Closed.IsZero() { |
| bug.Reporting[i].Closed = now |
| } |
| } |
| if bug.ReproLevel < cmd.ReproLevel { |
| return false, internalError, |
| fmt.Errorf("bug update with invalid repro level: %v/%v", |
| bug.ReproLevel, cmd.ReproLevel) |
| } |
| case dashapi.BugStatusUpstream: |
| if final { |
| return false, "Can't upstream, this is final destination.", nil |
| } |
| if len(bug.Commits) != 0 { |
| // We could handle this case, but how/when it will occur |
| // in real life is unclear now. |
| return false, "Can't upstream this bug, the bug has fixing commits.", nil |
| } |
| bug.Status = BugStatusOpen |
| bug.Closed = time.Time{} |
| bugReporting.Closed = now |
| bugReporting.Auto = cmd.Notification |
| case dashapi.BugStatusInvalid: |
| bug.Closed = now |
| bug.Status = BugStatusInvalid |
| bugReporting.Closed = now |
| bugReporting.Auto = cmd.Notification |
| case dashapi.BugStatusDup: |
| bug.Status = BugStatusDup |
| bug.Closed = now |
| bug.DupOf = dupHash |
| case dashapi.BugStatusUpdate: |
| // Just update Link, Commits, etc below. |
| case dashapi.BugStatusUnCC: |
| bug.UNCC = email.MergeEmailLists(bug.UNCC, cmd.CC) |
| default: |
| return false, internalError, fmt.Errorf("unknown bug status %v", cmd.Status) |
| } |
| return true, "", nil |
| } |
| |
| func checkBugStatus(c context.Context, cmd *dashapi.BugUpdate, bug *Bug, bugReporting *BugReporting) ( |
| bool, string, error) { |
| switch bug.Status { |
| case BugStatusOpen: |
| case BugStatusDup: |
| canon, err := canonicalBug(c, bug) |
| if err != nil { |
| return false, internalError, err |
| } |
| if canon.Status != BugStatusOpen { |
| // We used to reject updates to closed bugs, |
| // but this is confusing and non-actionable for users. |
| // So now we fail the update, but give empty reason, |
| // which means "don't notify user". |
| if cmd.Status == dashapi.BugStatusUpdate { |
| // This happens when people discuss old bugs. |
| log.Infof(c, "Dup bug is already closed") |
| } else { |
| log.Errorf(c, "Dup bug is already closed") |
| } |
| return false, "", nil |
| } |
| case BugStatusFixed, BugStatusInvalid: |
| if cmd.Status != dashapi.BugStatusUpdate { |
| log.Errorf(c, "This bug is already closed") |
| } |
| return false, "", nil |
| default: |
| return false, internalError, fmt.Errorf("unknown bug status %v", bug.Status) |
| } |
| if !bugReporting.Closed.IsZero() { |
| if cmd.Status != dashapi.BugStatusUpdate { |
| log.Errorf(c, "This bug reporting is already closed") |
| } |
| return false, "", nil |
| } |
| return true, "", nil |
| } |
| |
| func findBugByReportingID(c context.Context, id string) (*Bug, *db.Key, error) { |
| var bugs []*Bug |
| keys, err := db.NewQuery("Bug"). |
| Filter("Reporting.ID=", id). |
| Limit(2). |
| GetAll(c, &bugs) |
| if err != nil { |
| return nil, nil, fmt.Errorf("failed to fetch bugs: %v", err) |
| } |
| if len(bugs) == 0 { |
| return nil, nil, fmt.Errorf("failed to find bug by reporting id %q", id) |
| } |
| if len(bugs) > 1 { |
| return nil, nil, fmt.Errorf("multiple bugs for reporting id %q", id) |
| } |
| return bugs[0], keys[0], nil |
| } |
| |
| func findDupByTitle(c context.Context, ns, title string) (*Bug, *db.Key, error) { |
| title, seq, err := splitDisplayTitle(title) |
| if err != nil { |
| return nil, nil, err |
| } |
| bugHash := bugKeyHash(ns, title, seq) |
| bugKey := db.NewKey(c, "Bug", bugHash, 0, nil) |
| bug := new(Bug) |
| if err := db.Get(c, bugKey, bug); err != nil { |
| return nil, nil, fmt.Errorf("failed to get dup: %v", err) |
| } |
| return bug, bugKey, nil |
| } |
| |
| func bugReportingByID(bug *Bug, id string) (*BugReporting, bool) { |
| for i := range bug.Reporting { |
| if bug.Reporting[i].ID == id { |
| return &bug.Reporting[i], i == len(bug.Reporting)-1 |
| } |
| } |
| return nil, false |
| } |
| |
| func bugReportingByName(bug *Bug, name string) *BugReporting { |
| for i := range bug.Reporting { |
| if bug.Reporting[i].Name == name { |
| return &bug.Reporting[i] |
| } |
| } |
| return nil |
| } |
| |
| func lastReportedReporting(bug *Bug) *BugReporting { |
| for i := len(bug.Reporting) - 1; i >= 0; i-- { |
| if !bug.Reporting[i].Reported.IsZero() { |
| return &bug.Reporting[i] |
| } |
| } |
| return nil |
| } |
| |
| func queryCrashesForBug(c context.Context, bugKey *db.Key, limit int) ( |
| []*Crash, []*db.Key, error) { |
| var crashes []*Crash |
| keys, err := db.NewQuery("Crash"). |
| Ancestor(bugKey). |
| Order("-ReportLen"). |
| Order("-Reported"). |
| Order("-Time"). |
| Limit(limit). |
| GetAll(c, &crashes) |
| if err != nil { |
| return nil, nil, fmt.Errorf("failed to fetch crashes: %v", err) |
| } |
| return crashes, keys, nil |
| } |
| |
| func findCrashForBug(c context.Context, bug *Bug) (*Crash, *db.Key, error) { |
| bugKey := bug.key(c) |
| crashes, keys, err := queryCrashesForBug(c, bugKey, 1) |
| if err != nil { |
| return nil, nil, err |
| } |
| if len(crashes) < 1 { |
| return nil, nil, fmt.Errorf("no crashes") |
| } |
| crash, key := crashes[0], keys[0] |
| if bug.ReproLevel == ReproLevelC { |
| if crash.ReproC == 0 { |
| log.Errorf(c, "bug '%v': has C repro, but crash without C repro", bug.Title) |
| } |
| } else if bug.ReproLevel == ReproLevelSyz { |
| if crash.ReproSyz == 0 { |
| log.Errorf(c, "bug '%v': has syz repro, but crash without syz repro", bug.Title) |
| } |
| } else if bug.HasReport { |
| if crash.Report == 0 { |
| log.Errorf(c, "bug '%v': has report, but crash without report", bug.Title) |
| } |
| } |
| return crash, key, nil |
| } |
| |
| func loadReportingState(c context.Context) (*ReportingState, error) { |
| state := new(ReportingState) |
| key := db.NewKey(c, "ReportingState", "", 1, nil) |
| if err := db.Get(c, key, state); err != nil && err != db.ErrNoSuchEntity { |
| return nil, fmt.Errorf("failed to get reporting state: %v", err) |
| } |
| return state, nil |
| } |
| |
| func saveReportingState(c context.Context, state *ReportingState) error { |
| key := db.NewKey(c, "ReportingState", "", 1, nil) |
| if _, err := db.Put(c, key, state); err != nil { |
| return fmt.Errorf("failed to put reporting state: %v", err) |
| } |
| return nil |
| } |
| |
| func (state *ReportingState) getEntry(now time.Time, namespace, name string) *ReportingStateEntry { |
| if namespace == "" || name == "" { |
| panic(fmt.Sprintf("requesting reporting state for %v/%v", namespace, name)) |
| } |
| // Convert time to date of the form 20170125. |
| date := timeDate(now) |
| for i := range state.Entries { |
| ent := &state.Entries[i] |
| if ent.Namespace == namespace && ent.Name == name { |
| if ent.Date != date { |
| ent.Date = date |
| ent.Sent = 0 |
| } |
| return ent |
| } |
| } |
| state.Entries = append(state.Entries, ReportingStateEntry{ |
| Namespace: namespace, |
| Name: name, |
| Date: date, |
| Sent: 0, |
| }) |
| return &state.Entries[len(state.Entries)-1] |
| } |
| |
| // bugReportSorter sorts bugs by priority we want to report them. |
| // E.g. we want to report bugs with reproducers before bugs without reproducers. |
| type bugReportSorter []*Bug |
| |
| func (a bugReportSorter) Len() int { return len(a) } |
| func (a bugReportSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] } |
| func (a bugReportSorter) Less(i, j int) bool { |
| if a[i].ReproLevel != a[j].ReproLevel { |
| return a[i].ReproLevel > a[j].ReproLevel |
| } |
| if a[i].HasReport != a[j].HasReport { |
| return a[i].HasReport |
| } |
| if a[i].NumCrashes != a[j].NumCrashes { |
| return a[i].NumCrashes > a[j].NumCrashes |
| } |
| return a[i].FirstTime.Before(a[j].FirstTime) |
| } |
| |
| // kernelArch returns arch as kernel developers know it (rather than Go names). |
| // Currently Linux-specific. |
| func kernelArch(arch string) string { |
| switch arch { |
| case "386": |
| return "i386" |
| case "amd64": |
| return "" // this is kinda the default, so we don't notify about it |
| default: |
| return arch |
| } |
| } |