blob: db0df9e96478f5006804eb862cfd10dfbcdc99c9 [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 (
"encoding/json"
"fmt"
"sort"
"strings"
"time"
"github.com/google/syzkaller/dashboard/dashapi"
"github.com/google/syzkaller/pkg/email"
"golang.org/x/net/context"
"google.golang.org/appengine/datastore"
"google.golang.org/appengine/log"
)
// 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
internalError = "internal error"
)
// reportingPoll is called by backends to get list of bugs that need to be reported.
func reportingPoll(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 = datastore.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, _, _, _, err := needReport(c, typ, state, bug)
if err != nil || reporting == nil {
return nil, err
}
rep, err := createBugReport(c, bug, crash, bugReporting, reporting.Config)
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, 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.Name, reproStr(bugReporting.ReproLevel),
formatTime(bugReporting.Reported))
reporting, bugReporting = nil, nil
return
}
ent := state.getEntry(timeNow(c), bug.Namespace, reporting.Name)
cfg := config.Namespaces[bug.Namespace]
if bug.ReproLevel < ReproLevelC && timeSince(c, bug.FirstTime) < cfg.WaitForRepro {
status = fmt.Sprintf("%v: waiting for C repro", reporting.Name)
reporting, bugReporting = nil, nil
return
}
if !cfg.MailWithoutReport && !bug.HasReport {
status = fmt.Sprintf("%v: no report", reporting.Name)
reporting, bugReporting = nil, nil
return
}
crash, _, err = findCrashForBug(c, bug)
if err != nil {
status = fmt.Sprintf("%v: no crashes!", reporting.Name)
reporting, bugReporting = nil, nil
return
}
if reporting.Config.NeedMaintainers() && len(crash.Maintainers) == 0 {
status = fmt.Sprintf("%v: no maintainers", reporting.Name)
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.Name)
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.Name)
if !bugReporting.Reported.IsZero() {
status += fmt.Sprintf(" (reported%v on %v)",
reproStr(bugReporting.ReproLevel), formatTime(bugReporting.Reported))
}
return
}
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.Status {
case ReportingActive:
break
case ReportingSuspended:
return nil, nil, 0, fmt.Sprintf("%v: reporting suspended", bugReporting.Name), nil
case ReportingDisabled:
continue
case ReportingPassThrough:
if !isSpecialBug(bug) {
continue
}
}
return reporting, bugReporting, i, "", nil
}
return nil, nil, 0, "", fmt.Errorf("no reporting left")
}
func isSpecialBug(bug *Bug) bool {
// We may consider introducing a bug type, but for now we just look at some fields.
return !bug.HasReport ||
bug.Title == corruptedReportTitle ||
strings.Contains(bug.Title, "build error") ||
strings.Contains(bug.Title, "boot error:") ||
strings.Contains(bug.Title, "test error:")
}
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, bugReporting *BugReporting, config interface{}) (*dashapi.BugReport, error) {
reportingConfig, err := json.Marshal(config)
if err != nil {
return nil, err
}
crashLog, err := getText(c, "CrashLog", crash.Log)
if err != nil {
return nil, err
}
if len(crashLog) > maxMailLogLen {
crashLog = crashLog[len(crashLog)-maxMailLogLen:]
}
report, err := getText(c, "CrashReport", crash.Report)
if err != nil {
return nil, err
}
if len(report) > maxMailReportLen {
report = report[:maxMailReportLen]
}
reproC, err := getText(c, "ReproC", crash.ReproC)
if err != nil {
return nil, err
}
reproSyz, err := getText(c, "ReproSyz", crash.ReproSyz)
if err != nil {
return nil, err
}
if len(reproSyz) != 0 && len(crash.ReproOpts) != 0 {
tmp := append([]byte{'#'}, crash.ReproOpts...)
tmp = append(tmp, '\n')
tmp = append(tmp, reproSyz...)
reproSyz = tmp
}
build, err := loadBuild(c, bug.Namespace, crash.BuildID)
if err != nil {
return nil, err
}
kernelConfig, err := getText(c, "KernelConfig", build.KernelConfig)
if err != nil {
return nil, err
}
rep := &dashapi.BugReport{
Namespace: bug.Namespace,
Config: reportingConfig,
ID: bugReporting.ID,
ExtID: bugReporting.ExtID,
First: bugReporting.Reported.IsZero(),
Title: bug.displayTitle(),
Log: crashLog,
Report: report,
Maintainers: crash.Maintainers,
OS: build.OS,
Arch: build.Arch,
VMArch: build.VMArch,
CompilerID: build.CompilerID,
KernelRepo: build.KernelRepo,
KernelBranch: build.KernelBranch,
KernelCommit: build.KernelCommit,
KernelConfig: kernelConfig,
ReproC: reproC,
ReproSyz: reproSyz,
}
if bugReporting.CC != "" {
rep.CC = strings.Split(bugReporting.CC, "|")
}
return rep, nil
}
// incomingCommand is entry point to bug status updates.
func incomingCommand(c context.Context, cmd *dashapi.BugUpdate) (bool, string, error) {
log.Infof(c, "got command: %+q", cmd)
ok, reason, err := incomingCommandImpl(c, cmd)
if err != nil {
log.Errorf(c, "%v (%v)", reason, err)
} else if !ok {
log.Warningf(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 {
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 := bugReportingByName(dup, bugReporting.Name)
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 bugReporting.Name != dupReporting.Name {
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 = bugKeyHash(dup.Namespace, dup.Title, dup.Seq)
}
ok, reply := false, ""
tx := func(c context.Context) error {
var err error
ok, reply, err = incomingCommandTx(c, now, cmd, bugKey, dupHash)
return err
}
err = datastore.RunInTransaction(c, tx, &datastore.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 incomingCommandTx(c context.Context, now time.Time, cmd *dashapi.BugUpdate, bugKey *datastore.Key, dupHash string) (bool, string, error) {
bug := new(Bug)
if err := datastore.Get(c, bugKey, bug); err != nil {
return false, internalError, fmt.Errorf("can't find the corresponding bug: %v", err)
}
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".
log.Warningf(c, "Dup bug is already closed")
return false, "", nil
}
case BugStatusFixed, BugStatusInvalid:
log.Warningf(c, "This bug is already closed")
return false, "", nil
default:
return false, internalError, fmt.Errorf("unknown bug status %v", bug.Status)
}
bugReporting, final := bugReportingByID(bug, cmd.ID)
if bugReporting == nil {
return false, internalError, fmt.Errorf("can't find bug reporting")
}
if !bugReporting.Closed.IsZero() {
log.Warningf(c, "This bug reporting is already closed")
return false, "", nil
}
state, err := loadReportingState(c)
if err != nil {
return false, internalError, err
}
stateEnt := state.getEntry(now, bug.Namespace, bugReporting.Name)
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
}
// 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
case dashapi.BugStatusInvalid:
bugReporting.Closed = now
bug.Closed = now
bug.Status = BugStatusInvalid
case dashapi.BugStatusDup:
bug.Status = BugStatusDup
bug.Closed = now
bug.DupOf = dupHash
case dashapi.BugStatusUpdate:
// Just update Link, Commits, etc below.
default:
return false, internalError, fmt.Errorf("unknown bug status %v", cmd.Status)
}
if len(cmd.FixCommits) != 0 && (bug.Status == BugStatusOpen || bug.Status == BugStatusDup) {
m := make(map[string]bool)
for _, com := range cmd.FixCommits {
m[com] = true
}
same := false
if len(bug.Commits) == len(m) {
same = true
for _, com := range bug.Commits {
if !m[com] {
same = false
break
}
}
}
if !same {
commits := make([]string, 0, len(m))
for com := range m {
commits = append(commits, com)
}
sort.Strings(commits)
bug.Commits = commits
bug.PatchedOn = nil
}
}
if bugReporting.ExtID == "" {
bugReporting.ExtID = cmd.ExtID
}
if bugReporting.Link == "" {
bugReporting.Link = cmd.Link
}
if len(cmd.CC) != 0 {
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 _, err := datastore.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 findBugByReportingID(c context.Context, id string) (*Bug, *datastore.Key, error) {
var bugs []*Bug
keys, err := datastore.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, *datastore.Key, error) {
title, seq, err := splitDisplayTitle(title)
if err != nil {
return nil, nil, err
}
bugHash := bugKeyHash(ns, title, seq)
bugKey := datastore.NewKey(c, "Bug", bugHash, 0, nil)
bug := new(Bug)
if err := datastore.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 queryCrashesForBug(c context.Context, bugKey *datastore.Key, limit int) (
[]*Crash, []*datastore.Key, error) {
var crashes []*Crash
keys, err := datastore.NewQuery("Crash").
Ancestor(bugKey).
Order("-ReproC").
Order("-ReproSyz").
Order("-ReportLen").
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, *datastore.Key, error) {
bugKey := datastore.NewKey(c, "Bug", bugKeyHash(bug.Namespace, bug.Title, bug.Seq), 0, nil)
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 := datastore.NewKey(c, "ReportingState", "", 1, nil)
if err := datastore.Get(c, key, state); err != nil && err != datastore.ErrNoSuchEntity {
return nil, fmt.Errorf("failed to get reporting state: %v", err)
}
return state, nil
}
func saveReportingState(c context.Context, state *ReportingState) error {
key := datastore.NewKey(c, "ReportingState", "", 1, nil)
if _, err := datastore.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)
}