blob: 83c2af24be80c2d64db41deb2248dd935404ef39 [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"
"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, JobBisectCause)
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, jobType JobType) (*Job, *Crash, *db.Key, *db.Key, error) {
bugKey := bug.key(c)
var jobs []*Job
keys, err := db.NewQuery("Job").
Ancestor(bugKey).
Filter("Type=", jobType).
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 {
jobStr := map[JobType]string{
JobBisectCause: "bisect cause",
JobBisectFix: "bisect fix",
}
return nil, nil, nil, nil, fmt.Errorf("can't find %s job for bug", jobStr[jobType])
}
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 loadAllBugs(c context.Context, ns, manager string) ([]*Bug, error) {
var bugs []*Bug
err := foreachBug(c, ns, manager, func(bug *Bug) error {
bugs = append(bugs, bug)
return nil
})
if err != nil {
return nil, err
}
return bugs, nil
}
func foreachBug(c context.Context, ns, manager string, fn func(bug *Bug) error) error {
const batchSize = 1000
for offset := 0; ; offset += batchSize {
var bugs []*Bug
query := db.NewQuery("Bug")
if ns != "" {
query = query.Filter("Namespace=", ns)
if manager != "" {
query = query.Filter("HappenedOn=", manager)
}
}
_, err := query.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 queryJobsForBug(c context.Context, bugKey *db.Key, jobType JobType) (
[]*Job, []*db.Key, error) {
var jobs []*Job
keys, err := db.NewQuery("Job").
Ancestor(bugKey).
Filter("Type=", jobType).
Filter("Finished>", time.Time{}).
Order("-Finished").
GetAll(c, &jobs)
if err != nil {
return nil, nil, fmt.Errorf("failed to fetch fix bisections: %v", err)
}
return jobs, keys, nil
}
func queryCrashForJob(c context.Context, job *Job, bugKey *db.Key) (*Crash, error) {
// If there was no crash corresponding to the Job, return.
if job.CrashTitle == "" {
return nil, nil
}
// First, fetch the crash who's repro was used to start the bisection
// job.
crash := new(Crash)
crashKey := db.NewKey(c, "Crash", "", job.CrashID, bugKey)
if err := db.Get(c, crashKey, crash); err != nil {
return nil, err
}
// Now, create a crash object with the crash details from the job.
ret := &Crash{
Manager: crash.Manager,
Time: job.Finished,
Log: job.Log,
Report: job.CrashReport,
ReproOpts: crash.ReproOpts,
ReproSyz: crash.ReproSyz,
ReproC: crash.ReproC,
}
return ret, 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
}
}