github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/dashboard/app/reporting.go (about)

     1  // Copyright 2017 syzkaller project authors. All rights reserved.
     2  // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
     3  
     4  package main
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"encoding/json"
    10  	"fmt"
    11  	"math"
    12  	"reflect"
    13  	"sort"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/google/syzkaller/dashboard/dashapi"
    18  	"github.com/google/syzkaller/pkg/email"
    19  	"github.com/google/syzkaller/pkg/html"
    20  	"github.com/google/syzkaller/sys/targets"
    21  	db "google.golang.org/appengine/v2/datastore"
    22  	"google.golang.org/appengine/v2/log"
    23  )
    24  
    25  // Backend-independent reporting logic.
    26  // Two main entry points:
    27  //  - reportingPoll is called by backends to get list of bugs that need to be reported.
    28  //  - incomingCommand is called by backends to update bug statuses.
    29  
    30  const (
    31  	maxMailReportLen           = 64 << 10
    32  	maxInlineError             = 16 << 10
    33  	notifyResendPeriod         = 14 * 24 * time.Hour
    34  	notifyAboutBadCommitPeriod = 90 * 24 * time.Hour
    35  	never                      = 100 * 365 * 24 * time.Hour
    36  	internalError              = "internal error"
    37  	// This is embedded as first line of syzkaller reproducer files.
    38  	syzReproPrefix = "# See https://goo.gl/kgGztJ for information about syzkaller reproducers.\n"
    39  )
    40  
    41  // reportingPoll is called by backends to get list of bugs that need to be reported.
    42  func reportingPollBugs(c context.Context, typ string) []*dashapi.BugReport {
    43  	state, err := loadReportingState(c)
    44  	if err != nil {
    45  		log.Errorf(c, "%v", err)
    46  		return nil
    47  	}
    48  	bugs, _, err := loadOpenBugs(c)
    49  	if err != nil {
    50  		log.Errorf(c, "%v", err)
    51  		return nil
    52  	}
    53  	log.Infof(c, "fetched %v bugs", len(bugs))
    54  	sort.Sort(bugReportSorter(bugs))
    55  	var reports []*dashapi.BugReport
    56  	for _, bug := range bugs {
    57  		rep, err := handleReportBug(c, typ, state, bug)
    58  		if err != nil {
    59  			log.Errorf(c, "%v: failed to report bug %v: %v", bug.Namespace, bug.Title, err)
    60  			continue
    61  		}
    62  		if rep == nil {
    63  			continue
    64  		}
    65  		reports = append(reports, rep)
    66  		// Trying to report too many at once is known to cause OOMs.
    67  		// But new bugs appear incrementally and polling is frequent enough,
    68  		// so reporting lots of bugs at once is also not necessary.
    69  		if len(reports) == 3 {
    70  			break
    71  		}
    72  	}
    73  	return reports
    74  }
    75  
    76  func handleReportBug(c context.Context, typ string, state *ReportingState, bug *Bug) (
    77  	*dashapi.BugReport, error) {
    78  	reporting, bugReporting, _, _, _, err := needReport(c, typ, state, bug)
    79  	if err != nil || reporting == nil {
    80  		return nil, err
    81  	}
    82  	crash, crashKey, err := findCrashForBug(c, bug)
    83  	if err != nil {
    84  		return nil, err
    85  	} else if crash == nil {
    86  		return nil, fmt.Errorf("no crashes")
    87  	}
    88  	rep, err := createBugReport(c, bug, crash, crashKey, bugReporting, reporting)
    89  	if err != nil {
    90  		return nil, err
    91  	}
    92  	log.Infof(c, "bug %q: reporting to %v", bug.Title, reporting.Name)
    93  	return rep, nil
    94  }
    95  
    96  func needReport(c context.Context, typ string, state *ReportingState, bug *Bug) (
    97  	reporting *Reporting, bugReporting *BugReporting, reportingIdx int,
    98  	status, link string, err error) {
    99  	reporting, bugReporting, reportingIdx, status, err = currentReporting(c, bug)
   100  	if err != nil || reporting == nil {
   101  		return
   102  	}
   103  	if typ != "" && typ != reporting.Config.Type() {
   104  		status = "on a different reporting"
   105  		reporting, bugReporting = nil, nil
   106  		return
   107  	}
   108  	link = bugReporting.Link
   109  	if !bugReporting.Reported.IsZero() && bugReporting.ReproLevel >= bug.HeadReproLevel {
   110  		status = fmt.Sprintf("%v: reported%v on %v",
   111  			reporting.DisplayTitle, reproStr(bugReporting.ReproLevel),
   112  			html.FormatTime(bugReporting.Reported))
   113  		reporting, bugReporting = nil, nil
   114  		return
   115  	}
   116  	ent := state.getEntry(timeNow(c), bug.Namespace, reporting.Name)
   117  	cfg := getNsConfig(c, bug.Namespace)
   118  	if timeSince(c, bug.FirstTime) < cfg.ReportingDelay {
   119  		status = fmt.Sprintf("%v: initial reporting delay", reporting.DisplayTitle)
   120  		reporting, bugReporting = nil, nil
   121  		return
   122  	}
   123  	if crashNeedsRepro(bug.Title) && bug.ReproLevel < ReproLevelC &&
   124  		timeSince(c, bug.FirstTime) < cfg.WaitForRepro {
   125  		status = fmt.Sprintf("%v: waiting for C repro", reporting.DisplayTitle)
   126  		reporting, bugReporting = nil, nil
   127  		return
   128  	}
   129  	if !cfg.MailWithoutReport && !bug.HasReport {
   130  		status = fmt.Sprintf("%v: no report", reporting.DisplayTitle)
   131  		reporting, bugReporting = nil, nil
   132  		return
   133  	}
   134  	if bug.NumCrashes == 0 {
   135  		status = fmt.Sprintf("%v: no crashes!", reporting.DisplayTitle)
   136  		reporting, bugReporting = nil, nil
   137  		return
   138  	}
   139  
   140  	// Limit number of reports sent per day.
   141  	if ent.Sent >= reporting.DailyLimit {
   142  		status = fmt.Sprintf("%v: out of quota for today", reporting.DisplayTitle)
   143  		reporting, bugReporting = nil, nil
   144  		return
   145  	}
   146  
   147  	// Ready to be reported.
   148  	// This update won't be committed, but it is useful as a best effort measure
   149  	// so that we don't overflow the limit in a single poll.
   150  	ent.Sent++
   151  
   152  	status = fmt.Sprintf("%v: ready to report", reporting.DisplayTitle)
   153  	if !bugReporting.Reported.IsZero() {
   154  		status += fmt.Sprintf(" (reported%v on %v)",
   155  			reproStr(bugReporting.ReproLevel), html.FormatTime(bugReporting.Reported))
   156  	}
   157  	return
   158  }
   159  
   160  func reportingPollNotifications(c context.Context, typ string) []*dashapi.BugNotification {
   161  	bugs, _, err := loadOpenBugs(c)
   162  	if err != nil {
   163  		log.Errorf(c, "%v", err)
   164  		return nil
   165  	}
   166  	log.Infof(c, "fetched %v bugs", len(bugs))
   167  	var notifs []*dashapi.BugNotification
   168  	for _, bug := range bugs {
   169  		if getNsConfig(c, bug.Namespace).Decommissioned {
   170  			continue
   171  		}
   172  		notif, err := handleReportNotif(c, typ, bug)
   173  		if err != nil {
   174  			log.Errorf(c, "%v: failed to create bug notif %v: %v", bug.Namespace, bug.Title, err)
   175  			continue
   176  		}
   177  		if notif == nil {
   178  			continue
   179  		}
   180  		notifs = append(notifs, notif)
   181  		if len(notifs) >= 10 {
   182  			break // don't send too many at once just in case
   183  		}
   184  	}
   185  	return notifs
   186  }
   187  
   188  func handleReportNotif(c context.Context, typ string, bug *Bug) (*dashapi.BugNotification, error) {
   189  	reporting, bugReporting, _, _, err := currentReporting(c, bug)
   190  	if err != nil || reporting == nil {
   191  		return nil, nil
   192  	}
   193  	if typ != "" && typ != reporting.Config.Type() {
   194  		return nil, nil
   195  	}
   196  	if bug.Status != BugStatusOpen || bugReporting.Reported.IsZero() {
   197  		return nil, nil
   198  	}
   199  	for _, f := range notificationGenerators {
   200  		notif, err := f(c, bug, reporting, bugReporting)
   201  		if notif != nil || err != nil {
   202  			return notif, err
   203  		}
   204  	}
   205  	return nil, nil
   206  }
   207  
   208  var notificationGenerators = []func(context.Context, *Bug, *Reporting,
   209  	*BugReporting) (*dashapi.BugNotification, error){
   210  	// Embargo upstreaming.
   211  	func(c context.Context, bug *Bug, reporting *Reporting,
   212  		bugReporting *BugReporting) (*dashapi.BugNotification, error) {
   213  		if reporting.moderation &&
   214  			reporting.Embargo != 0 &&
   215  			len(bug.Commits) == 0 &&
   216  			bugReporting.OnHold.IsZero() &&
   217  			timeSince(c, bugReporting.Reported) > reporting.Embargo {
   218  			log.Infof(c, "%v: upstreaming (embargo): %v", bug.Namespace, bug.Title)
   219  			return createNotification(c, dashapi.BugNotifUpstream, true, "", bug, reporting, bugReporting)
   220  		}
   221  		return nil, nil
   222  	},
   223  	// Upstreaming.
   224  	func(c context.Context, bug *Bug, reporting *Reporting,
   225  		bugReporting *BugReporting) (*dashapi.BugNotification, error) {
   226  		if reporting.moderation &&
   227  			len(bug.Commits) == 0 &&
   228  			bugReporting.OnHold.IsZero() &&
   229  			reporting.Filter(bug) == FilterSkip {
   230  			log.Infof(c, "%v: upstreaming (skip): %v", bug.Namespace, bug.Title)
   231  			return createNotification(c, dashapi.BugNotifUpstream, true, "", bug, reporting, bugReporting)
   232  		}
   233  		return nil, nil
   234  	},
   235  	// Obsoleting.
   236  	func(c context.Context, bug *Bug, reporting *Reporting,
   237  		bugReporting *BugReporting) (*dashapi.BugNotification, error) {
   238  		if len(bug.Commits) == 0 &&
   239  			bug.canBeObsoleted(c) &&
   240  			timeSince(c, bug.LastActivity) > notifyResendPeriod &&
   241  			timeSince(c, bug.LastTime) > bug.obsoletePeriod(c) {
   242  			log.Infof(c, "%v: obsoleting: %v", bug.Namespace, bug.Title)
   243  			why := bugObsoletionReason(bug)
   244  			return createNotification(c, dashapi.BugNotifObsoleted, false, string(why), bug, reporting, bugReporting)
   245  		}
   246  		return nil, nil
   247  	},
   248  	// Bad commit.
   249  	func(c context.Context, bug *Bug, reporting *Reporting,
   250  		bugReporting *BugReporting) (*dashapi.BugNotification, error) {
   251  		if len(bug.Commits) > 0 &&
   252  			len(bug.PatchedOn) == 0 &&
   253  			timeSince(c, bug.LastActivity) > notifyResendPeriod &&
   254  			timeSince(c, bug.FixTime) > notifyAboutBadCommitPeriod {
   255  			log.Infof(c, "%v: bad fix commit: %v", bug.Namespace, bug.Title)
   256  			commits := strings.Join(bug.Commits, "\n")
   257  			return createNotification(c, dashapi.BugNotifBadCommit, true, commits, bug, reporting, bugReporting)
   258  		}
   259  		return nil, nil
   260  	},
   261  	// Label notifications.
   262  	func(c context.Context, bug *Bug, reporting *Reporting,
   263  		bugReporting *BugReporting) (*dashapi.BugNotification, error) {
   264  		for _, label := range bug.Labels {
   265  			if label.SetBy != "" {
   266  				continue
   267  			}
   268  			str := label.String()
   269  			if reporting.Labels[str] == "" {
   270  				continue
   271  			}
   272  			if stringInList(bugReporting.GetLabels(), str) {
   273  				continue
   274  			}
   275  			return createLabelNotification(c, label, bug, reporting, bugReporting)
   276  		}
   277  		return nil, nil
   278  	},
   279  }
   280  
   281  func createLabelNotification(c context.Context, label BugLabel, bug *Bug, reporting *Reporting,
   282  	bugReporting *BugReporting) (*dashapi.BugNotification, error) {
   283  	labelStr := label.String()
   284  	notif, err := createNotification(c, dashapi.BugNotifLabel, true, reporting.Labels[labelStr],
   285  		bug, reporting, bugReporting)
   286  	if err != nil {
   287  		return nil, err
   288  	}
   289  	notif.Label = labelStr
   290  	// For some labels also attach job results.
   291  	if label.Label == OriginLabel {
   292  		var err error
   293  		notif.TreeJobs, err = treeTestJobs(c, bug)
   294  		if err != nil {
   295  			log.Errorf(c, "failed to extract jobs for %s: %v", bug.keyHash(c), err)
   296  			return nil, fmt.Errorf("failed to fetch jobs: %w", err)
   297  		}
   298  	}
   299  	return notif, nil
   300  }
   301  
   302  func bugObsoletionReason(bug *Bug) dashapi.BugStatusReason {
   303  	if bug.HeadReproLevel == ReproLevelNone && bug.ReproLevel != ReproLevelNone {
   304  		return dashapi.InvalidatedByRevokedRepro
   305  	}
   306  	return dashapi.InvalidatedByNoActivity
   307  }
   308  
   309  var noObsoletionsKey = "Temporarily disable bug obsoletions"
   310  
   311  func contextWithNoObsoletions(c context.Context) context.Context {
   312  	return context.WithValue(c, &noObsoletionsKey, struct{}{})
   313  }
   314  
   315  func getNoObsoletions(c context.Context) bool {
   316  	return c.Value(&noObsoletionsKey) != nil
   317  }
   318  
   319  // TODO: this is what we would like to do, but we need to figure out
   320  // KMSAN story: we don't do fix bisection on it (rebased),
   321  // do we want to close all old KMSAN bugs with repros?
   322  // For now we only enable this in tests.
   323  var obsoleteWhatWontBeFixBisected = false
   324  
   325  func (bug *Bug) canBeObsoleted(c context.Context) bool {
   326  	if getNoObsoletions(c) {
   327  		return false
   328  	}
   329  	if bug.HeadReproLevel == ReproLevelNone {
   330  		return true
   331  	}
   332  	if obsoleteWhatWontBeFixBisected {
   333  		cfg := getNsConfig(c, bug.Namespace)
   334  		for _, mgr := range bug.HappenedOn {
   335  			if !cfg.Managers[mgr].FixBisectionDisabled {
   336  				return false
   337  			}
   338  		}
   339  		return true
   340  	}
   341  	return false
   342  }
   343  
   344  func (bug *Bug) obsoletePeriod(c context.Context) time.Duration {
   345  	period := never
   346  	config := getConfig(c)
   347  	if config.Obsoleting.MinPeriod == 0 {
   348  		return period
   349  	}
   350  
   351  	// Let's assume that crashes follow the Possion distribution with rate r=crashes/days.
   352  	// Then, the chance of seeing a crash within t days is p=1-e^(-r*t).
   353  	// Solving it for t, we get t=log(1/(1-p))/r.
   354  	// Since our rate is also only an estimate, let's require p=0.99.
   355  
   356  	// Before we have at least 10 crashes, any estimation of frequency is too imprecise.
   357  	// In such case we conservatively assume it still happens.
   358  	if bug.NumCrashes >= 10 {
   359  		bugDays := bug.LastTime.Sub(bug.FirstTime).Hours() / 24.0
   360  		rate := float64(bug.NumCrashes-1) / bugDays
   361  
   362  		const probability = 0.99
   363  		// For 1 crash/day, this will be ~4.6 days.
   364  		days := math.Log(1.0/(1.0-probability)) / rate
   365  		// Let's be conservative and multiply it by 3.
   366  		days = days * 3
   367  		period = time.Hour * time.Duration(24*days)
   368  	}
   369  	min, max := config.Obsoleting.MinPeriod, config.Obsoleting.MaxPeriod
   370  	if config.Obsoleting.NonFinalMinPeriod != 0 &&
   371  		bug.Reporting[len(bug.Reporting)-1].Reported.IsZero() {
   372  		min, max = config.Obsoleting.NonFinalMinPeriod, config.Obsoleting.NonFinalMaxPeriod
   373  	}
   374  	if mgr := bug.managerConfig(c); mgr != nil && mgr.ObsoletingMinPeriod != 0 {
   375  		min, max = mgr.ObsoletingMinPeriod, mgr.ObsoletingMaxPeriod
   376  	}
   377  	if period < min {
   378  		period = min
   379  	}
   380  	if period > max {
   381  		period = max
   382  	}
   383  	return period
   384  }
   385  
   386  func (bug *Bug) managerConfig(c context.Context) *ConfigManager {
   387  	if len(bug.HappenedOn) != 1 {
   388  		return nil
   389  	}
   390  	mgr := getNsConfig(c, bug.Namespace).Managers[bug.HappenedOn[0]]
   391  	return &mgr
   392  }
   393  
   394  func createNotification(c context.Context, typ dashapi.BugNotif, public bool, text string, bug *Bug,
   395  	reporting *Reporting, bugReporting *BugReporting) (*dashapi.BugNotification, error) {
   396  	reportingConfig, err := json.Marshal(reporting.Config)
   397  	if err != nil {
   398  		return nil, err
   399  	}
   400  	crash, _, err := findCrashForBug(c, bug)
   401  	if err != nil {
   402  		return nil, err
   403  	}
   404  	build, err := loadBuild(c, bug.Namespace, crash.BuildID)
   405  	if err != nil {
   406  		return nil, err
   407  	}
   408  	kernelRepo := kernelRepoInfo(c, build)
   409  	notif := &dashapi.BugNotification{
   410  		Type:      typ,
   411  		Namespace: bug.Namespace,
   412  		Config:    reportingConfig,
   413  		ID:        bugReporting.ID,
   414  		ExtID:     bugReporting.ExtID,
   415  		Title:     bug.displayTitle(),
   416  		Text:      text,
   417  		Public:    public,
   418  		Link:      fmt.Sprintf("%v/bug?extid=%v", appURL(c), bugReporting.ID),
   419  		CC:        kernelRepo.CC.Always,
   420  	}
   421  	if public {
   422  		notif.Maintainers = append(crash.Maintainers, kernelRepo.CC.Maintainers...)
   423  	}
   424  	if (public || reporting.moderation) && bugReporting.CC != "" {
   425  		notif.CC = append(notif.CC, strings.Split(bugReporting.CC, "|")...)
   426  	}
   427  	if mgr := bug.managerConfig(c); mgr != nil {
   428  		notif.CC = append(notif.CC, mgr.CC.Always...)
   429  		if public {
   430  			notif.Maintainers = append(notif.Maintainers, mgr.CC.Maintainers...)
   431  		}
   432  	}
   433  	return notif, nil
   434  }
   435  
   436  func currentReporting(c context.Context, bug *Bug) (*Reporting, *BugReporting, int, string, error) {
   437  	if bug.NumCrashes == 0 {
   438  		// This is possible during the short window when we already created a bug,
   439  		// but did not attach the first crash to it yet. We need to avoid reporting this bug yet
   440  		// and wait for the crash. Otherwise reporting filter may mis-classify it as e.g.
   441  		// not having a report or something else.
   442  		return nil, nil, 0, "no crashes yet", nil
   443  	}
   444  	for i := range bug.Reporting {
   445  		bugReporting := &bug.Reporting[i]
   446  		if !bugReporting.Closed.IsZero() {
   447  			continue
   448  		}
   449  		reporting := getNsConfig(c, bug.Namespace).ReportingByName(bugReporting.Name)
   450  		if reporting == nil {
   451  			return nil, nil, 0, "", fmt.Errorf("%v: missing in config", bugReporting.Name)
   452  		}
   453  		if reporting.DailyLimit == 0 {
   454  			return nil, nil, 0, fmt.Sprintf("%v: reporting has daily limit 0", reporting.DisplayTitle), nil
   455  		}
   456  		switch reporting.Filter(bug) {
   457  		case FilterSkip:
   458  			if bugReporting.Reported.IsZero() {
   459  				continue
   460  			}
   461  			fallthrough
   462  		case FilterReport:
   463  			return reporting, bugReporting, i, "", nil
   464  		case FilterHold:
   465  			return nil, nil, 0, fmt.Sprintf("%v: reporting suspended", reporting.DisplayTitle), nil
   466  		}
   467  	}
   468  	return nil, nil, 0, "", fmt.Errorf("no reporting left")
   469  }
   470  
   471  func reproStr(level dashapi.ReproLevel) string {
   472  	switch level {
   473  	case ReproLevelSyz:
   474  		return " syz repro"
   475  	case ReproLevelC:
   476  		return " C repro"
   477  	default:
   478  		return ""
   479  	}
   480  }
   481  
   482  // nolint: gocyclo
   483  func createBugReport(c context.Context, bug *Bug, crash *Crash, crashKey *db.Key,
   484  	bugReporting *BugReporting, reporting *Reporting) (*dashapi.BugReport, error) {
   485  	var job *Job
   486  	if bug.BisectCause == BisectYes || bug.BisectCause == BisectInconclusive || bug.BisectCause == BisectHorizont {
   487  		// If we have bisection results, report the crash/repro used for bisection.
   488  		causeBisect, err := queryBestBisection(c, bug, JobBisectCause)
   489  		if err != nil {
   490  			return nil, err
   491  		}
   492  		if causeBisect != nil {
   493  			err := causeBisect.loadCrash(c)
   494  			if err != nil {
   495  				return nil, err
   496  			}
   497  		}
   498  		// If we didn't check whether the bisect is unreliable, even though it would not be
   499  		// reported anyway, we could still eventually Cc people from those commits later
   500  		// (e.g. when we did bisected with a syz repro and then notified about a C repro).
   501  		if causeBisect != nil && !causeBisect.job.isUnreliableBisect() {
   502  			job = causeBisect.job
   503  			if causeBisect.crash.ReproC != 0 || crash.ReproC == 0 {
   504  				// Don't override the crash in this case,
   505  				// otherwise we will always think that we haven't reported the C repro.
   506  				crash, crashKey = causeBisect.crash, causeBisect.crashKey
   507  			}
   508  		}
   509  	}
   510  	rep, err := crashBugReport(c, bug, crash, crashKey, bugReporting, reporting)
   511  	if err != nil {
   512  		return nil, err
   513  	}
   514  	if job != nil {
   515  		cause, emails := bisectFromJob(c, job)
   516  		rep.BisectCause = cause
   517  		rep.Maintainers = append(rep.Maintainers, emails...)
   518  	}
   519  	return rep, nil
   520  }
   521  
   522  // crashBugReport fills in crash and build related fields into *dashapi.BugReport.
   523  func crashBugReport(c context.Context, bug *Bug, crash *Crash, crashKey *db.Key,
   524  	bugReporting *BugReporting, reporting *Reporting) (*dashapi.BugReport, error) {
   525  	reportingConfig, err := json.Marshal(reporting.Config)
   526  	if err != nil {
   527  		return nil, err
   528  	}
   529  	crashLog, _, err := getText(c, textCrashLog, crash.Log)
   530  	if err != nil {
   531  		return nil, err
   532  	}
   533  	report, _, err := getText(c, textCrashReport, crash.Report)
   534  	if err != nil {
   535  		return nil, err
   536  	}
   537  	if len(report) > maxMailReportLen {
   538  		report = report[:maxMailReportLen]
   539  	}
   540  	machineInfo, _, err := getText(c, textMachineInfo, crash.MachineInfo)
   541  	if err != nil {
   542  		return nil, err
   543  	}
   544  	build, err := loadBuild(c, bug.Namespace, crash.BuildID)
   545  	if err != nil {
   546  		return nil, err
   547  	}
   548  	typ := dashapi.ReportNew
   549  	if !bugReporting.Reported.IsZero() {
   550  		typ = dashapi.ReportRepro
   551  	}
   552  	assetList := createAssetList(build, crash, true)
   553  	kernelRepo := kernelRepoInfo(c, build)
   554  	rep := &dashapi.BugReport{
   555  		Type:            typ,
   556  		Config:          reportingConfig,
   557  		ExtID:           bugReporting.ExtID,
   558  		First:           bugReporting.Reported.IsZero(),
   559  		Moderation:      reporting.moderation,
   560  		Log:             crashLog,
   561  		LogLink:         externalLink(c, textCrashLog, crash.Log),
   562  		LogHasStrace:    dashapi.CrashFlags(crash.Flags)&dashapi.CrashUnderStrace > 0,
   563  		Report:          report,
   564  		ReportLink:      externalLink(c, textCrashReport, crash.Report),
   565  		CC:              kernelRepo.CC.Always,
   566  		Maintainers:     append(crash.Maintainers, kernelRepo.CC.Maintainers...),
   567  		ReproOpts:       crash.ReproOpts,
   568  		MachineInfo:     machineInfo,
   569  		MachineInfoLink: externalLink(c, textMachineInfo, crash.MachineInfo),
   570  		CrashID:         crashKey.IntID(),
   571  		CrashTime:       crash.Time,
   572  		NumCrashes:      bug.NumCrashes,
   573  		HappenedOn:      managersToRepos(c, bug.Namespace, bug.HappenedOn),
   574  		Manager:         crash.Manager,
   575  		Assets:          assetList,
   576  		ReportElements:  &dashapi.ReportElements{GuiltyFiles: crash.ReportElements.GuiltyFiles},
   577  	}
   578  	if !crash.ReproIsRevoked {
   579  		rep.ReproCLink = externalLink(c, textReproC, crash.ReproC)
   580  		rep.ReproC, _, err = getText(c, textReproC, crash.ReproC)
   581  		if err != nil {
   582  			return nil, err
   583  		}
   584  		rep.ReproSyzLink = externalLink(c, textReproSyz, crash.ReproSyz)
   585  		rep.ReproSyz, err = loadReproSyz(c, crash)
   586  		if err != nil {
   587  			return nil, err
   588  		}
   589  	}
   590  	if bugReporting.CC != "" {
   591  		rep.CC = append(rep.CC, strings.Split(bugReporting.CC, "|")...)
   592  	}
   593  	if build.Type == BuildFailed {
   594  		rep.Maintainers = append(rep.Maintainers, kernelRepo.CC.BuildMaintainers...)
   595  	}
   596  	if mgr := bug.managerConfig(c); mgr != nil {
   597  		rep.CC = append(rep.CC, mgr.CC.Always...)
   598  		rep.Maintainers = append(rep.Maintainers, mgr.CC.Maintainers...)
   599  		if build.Type == BuildFailed {
   600  			rep.Maintainers = append(rep.Maintainers, mgr.CC.BuildMaintainers...)
   601  		}
   602  	}
   603  	for _, label := range bug.Labels {
   604  		text, ok := reporting.Labels[label.String()]
   605  		if !ok {
   606  			continue
   607  		}
   608  		if rep.LabelMessages == nil {
   609  			rep.LabelMessages = map[string]string{}
   610  		}
   611  		rep.LabelMessages[label.String()] = text
   612  	}
   613  	if err := fillBugReport(c, rep, bug, bugReporting, build); err != nil {
   614  		return nil, err
   615  	}
   616  	return rep, nil
   617  }
   618  
   619  func loadReproSyz(c context.Context, crash *Crash) ([]byte, error) {
   620  	reproSyz, _, err := getText(c, textReproSyz, crash.ReproSyz)
   621  	if err != nil || len(reproSyz) == 0 {
   622  		return nil, err
   623  	}
   624  	buf := new(bytes.Buffer)
   625  	buf.WriteString(syzReproPrefix)
   626  	if len(crash.ReproOpts) != 0 {
   627  		fmt.Fprintf(buf, "#%s\n", crash.ReproOpts)
   628  	}
   629  	buf.Write(reproSyz)
   630  	return buf.Bytes(), nil
   631  }
   632  
   633  // fillBugReport fills common report fields for bug and job reports.
   634  func fillBugReport(c context.Context, rep *dashapi.BugReport, bug *Bug, bugReporting *BugReporting,
   635  	build *Build) error {
   636  	kernelConfig, _, err := getText(c, textKernelConfig, build.KernelConfig)
   637  	if err != nil {
   638  		return err
   639  	}
   640  	creditEmail, err := email.AddAddrContext(ownEmail(c), bugReporting.ID)
   641  	if err != nil {
   642  		return err
   643  	}
   644  	rep.BugStatus, err = bug.dashapiStatus()
   645  	if err != nil {
   646  		return err
   647  	}
   648  	rep.Namespace = bug.Namespace
   649  	rep.ID = bugReporting.ID
   650  	rep.Title = bug.displayTitle()
   651  	rep.Link = fmt.Sprintf("%v/bug?extid=%v", appURL(c), bugReporting.ID)
   652  	rep.CreditEmail = creditEmail
   653  	rep.OS = build.OS
   654  	rep.Arch = build.Arch
   655  	rep.VMArch = build.VMArch
   656  	rep.UserSpaceArch = kernelArch(build.Arch)
   657  	rep.BuildID = build.ID
   658  	rep.BuildTime = build.Time
   659  	rep.CompilerID = build.CompilerID
   660  	rep.KernelRepo = build.KernelRepo
   661  	rep.KernelRepoAlias = kernelRepoInfo(c, build).Alias
   662  	rep.KernelBranch = build.KernelBranch
   663  	rep.KernelCommit = build.KernelCommit
   664  	rep.KernelCommitTitle = build.KernelCommitTitle
   665  	rep.KernelCommitDate = build.KernelCommitDate
   666  	rep.KernelConfig = kernelConfig
   667  	rep.KernelConfigLink = externalLink(c, textKernelConfig, build.KernelConfig)
   668  	rep.SyzkallerCommit = build.SyzkallerCommit
   669  	rep.NoRepro = build.Type == BuildFailed
   670  	for _, item := range bug.LabelValues(SubsystemLabel) {
   671  		rep.Subsystems = append(rep.Subsystems, dashapi.BugSubsystem{
   672  			Name:  item.Value,
   673  			SetBy: item.SetBy,
   674  			Link:  fmt.Sprintf("%v/%s/s/%s", appURL(c), bug.Namespace, item.Value),
   675  		})
   676  		rep.Maintainers = email.MergeEmailLists(rep.Maintainers,
   677  			subsystemMaintainers(c, rep.Namespace, item.Value))
   678  	}
   679  	for _, addr := range bug.UNCC {
   680  		rep.CC = email.RemoveFromEmailList(rep.CC, addr)
   681  		rep.Maintainers = email.RemoveFromEmailList(rep.Maintainers, addr)
   682  	}
   683  	return nil
   684  }
   685  
   686  func managersToRepos(c context.Context, ns string, managers []string) []string {
   687  	var repos []string
   688  	dedup := make(map[string]bool)
   689  	for _, manager := range managers {
   690  		build, err := lastManagerBuild(c, ns, manager)
   691  		if err != nil {
   692  			log.Errorf(c, "failed to get manager %q build: %v", manager, err)
   693  			continue
   694  		}
   695  		repo := kernelRepoInfo(c, build).Alias
   696  		if dedup[repo] {
   697  			continue
   698  		}
   699  		dedup[repo] = true
   700  		repos = append(repos, repo)
   701  	}
   702  	sort.Strings(repos)
   703  	return repos
   704  }
   705  
   706  func queryCrashesForBug(c context.Context, bugKey *db.Key, limit int) (
   707  	[]*Crash, []*db.Key, error) {
   708  	var crashes []*Crash
   709  	keys, err := db.NewQuery("Crash").
   710  		Ancestor(bugKey).
   711  		Order("-ReportLen").
   712  		Order("-Time").
   713  		Limit(limit).
   714  		GetAll(c, &crashes)
   715  	if err != nil {
   716  		return nil, nil, fmt.Errorf("failed to fetch crashes: %w", err)
   717  	}
   718  	return crashes, keys, nil
   719  }
   720  
   721  func loadAllBugs(c context.Context, filter func(*db.Query) *db.Query) ([]*Bug, []*db.Key, error) {
   722  	var bugs []*Bug
   723  	var keys []*db.Key
   724  	err := foreachBug(c, filter, func(bug *Bug, key *db.Key) error {
   725  		bugs = append(bugs, bug)
   726  		keys = append(keys, key)
   727  		return nil
   728  	})
   729  	if err != nil {
   730  		return nil, nil, err
   731  	}
   732  	return bugs, keys, nil
   733  }
   734  
   735  func loadNamespaceBugs(c context.Context, ns string) ([]*Bug, []*db.Key, error) {
   736  	return loadAllBugs(c, func(query *db.Query) *db.Query {
   737  		return query.Filter("Namespace=", ns)
   738  	})
   739  }
   740  
   741  func loadOpenBugs(c context.Context) ([]*Bug, []*db.Key, error) {
   742  	return loadAllBugs(c, func(query *db.Query) *db.Query {
   743  		return query.Filter("Status<", BugStatusFixed)
   744  	})
   745  }
   746  
   747  func foreachBug(c context.Context, filter func(*db.Query) *db.Query, fn func(bug *Bug, key *db.Key) error) error {
   748  	const batchSize = 2000
   749  	var cursor *db.Cursor
   750  	for {
   751  		query := db.NewQuery("Bug").Limit(batchSize)
   752  		if filter != nil {
   753  			query = filter(query)
   754  		}
   755  		if cursor != nil {
   756  			query = query.Start(*cursor)
   757  		}
   758  		iter := query.Run(c)
   759  		for i := 0; ; i++ {
   760  			bug := new(Bug)
   761  			key, err := iter.Next(bug)
   762  			if err == db.Done {
   763  				if i < batchSize {
   764  					return nil
   765  				}
   766  				break
   767  			}
   768  			if err != nil {
   769  				return fmt.Errorf("failed to fetch bugs: %w", err)
   770  			}
   771  			if err := fn(bug, key); err != nil {
   772  				return err
   773  			}
   774  		}
   775  		cur, err := iter.Cursor()
   776  		if err != nil {
   777  			return fmt.Errorf("cursor failed while fetching bugs: %w", err)
   778  		}
   779  		cursor = &cur
   780  	}
   781  }
   782  
   783  func filterBugs(bugs []*Bug, keys []*db.Key, filter func(*Bug) bool) ([]*Bug, []*db.Key) {
   784  	var retBugs []*Bug
   785  	var retKeys []*db.Key
   786  	for i, bug := range bugs {
   787  		if filter(bug) {
   788  			retBugs = append(retBugs, bugs[i])
   789  			retKeys = append(retKeys, keys[i])
   790  		}
   791  	}
   792  	return retBugs, retKeys
   793  }
   794  
   795  // reportingPollClosed is called by backends to get list of closed bugs.
   796  func reportingPollClosed(c context.Context, ids []string) ([]string, error) {
   797  	idMap := make(map[string]bool, len(ids))
   798  	for _, id := range ids {
   799  		idMap[id] = true
   800  	}
   801  	var closed []string
   802  	err := foreachBug(c, nil, func(bug *Bug, _ *db.Key) error {
   803  		for i := range bug.Reporting {
   804  			bugReporting := &bug.Reporting[i]
   805  			if !idMap[bugReporting.ID] {
   806  				continue
   807  			}
   808  			var err error
   809  			bug, err = canonicalBug(c, bug)
   810  			if err != nil {
   811  				log.Errorf(c, "%v", err)
   812  				break
   813  			}
   814  			if bug.Status >= BugStatusFixed || !bugReporting.Closed.IsZero() ||
   815  				getNsConfig(c, bug.Namespace).Decommissioned {
   816  				closed = append(closed, bugReporting.ID)
   817  			}
   818  			break
   819  		}
   820  		return nil
   821  	})
   822  	return closed, err
   823  }
   824  
   825  // incomingCommand is entry point to bug status updates.
   826  func incomingCommand(c context.Context, cmd *dashapi.BugUpdate) (bool, string, error) {
   827  	log.Infof(c, "got command: %+v", cmd)
   828  	ok, reason, err := incomingCommandImpl(c, cmd)
   829  	if err != nil {
   830  		log.Errorf(c, "%v (%v)", reason, err)
   831  	} else if !ok && reason != "" {
   832  		log.Errorf(c, "invalid update: %v", reason)
   833  	}
   834  	return ok, reason, err
   835  }
   836  
   837  func incomingCommandImpl(c context.Context, cmd *dashapi.BugUpdate) (bool, string, error) {
   838  	for i, com := range cmd.FixCommits {
   839  		if len(com) >= 2 && com[0] == '"' && com[len(com)-1] == '"' {
   840  			com = com[1 : len(com)-1]
   841  			cmd.FixCommits[i] = com
   842  		}
   843  		if len(com) < 3 {
   844  			return false, fmt.Sprintf("bad commit title: %q", com), nil
   845  		}
   846  	}
   847  	bug, bugKey, err := findBugByReportingID(c, cmd.ID)
   848  	if err != nil {
   849  		return false, internalError, err
   850  	}
   851  	var dupKey *db.Key
   852  	if cmd.Status == dashapi.BugStatusDup {
   853  		if looksLikeReportingHash(cmd.DupOf) {
   854  			_, dupKey, _ = findBugByReportingID(c, cmd.DupOf)
   855  		}
   856  		if dupKey == nil {
   857  			// Email reporting passes bug title in cmd.DupOf, try to find bug by title.
   858  			var dup *Bug
   859  			dup, dupKey, err = findDupByTitle(c, bug.Namespace, cmd.DupOf)
   860  			if err != nil || dup == nil {
   861  				return false, "can't find the dup bug", err
   862  			}
   863  			dupReporting := lastReportedReporting(dup)
   864  			if dupReporting == nil {
   865  				return false, "can't find the dup bug", fmt.Errorf("dup does not have reporting")
   866  			}
   867  			cmd.DupOf = dupReporting.ID
   868  		}
   869  	}
   870  	now := timeNow(c)
   871  	ok, reply := false, ""
   872  	tx := func(c context.Context) error {
   873  		var err error
   874  		ok, reply, err = incomingCommandTx(c, now, cmd, bugKey, dupKey)
   875  		return err
   876  	}
   877  	err = db.RunInTransaction(c, tx, &db.TransactionOptions{
   878  		XG: true,
   879  		// Default is 3 which fails sometimes.
   880  		// We don't want incoming bug updates to fail,
   881  		// because for e.g. email we won't have an external retry.
   882  		Attempts: 30,
   883  	})
   884  	if err != nil {
   885  		return false, internalError, err
   886  	}
   887  	return ok, reply, nil
   888  }
   889  
   890  func checkDupBug(c context.Context, cmd *dashapi.BugUpdate, bug *Bug, bugKey, dupKey *db.Key) (
   891  	*Bug, bool, string, error) {
   892  	dup := new(Bug)
   893  	if err := db.Get(c, dupKey, dup); err != nil {
   894  		return nil, false, internalError, fmt.Errorf("can't find the dup by key: %w", err)
   895  	}
   896  	bugReporting, _ := bugReportingByID(bug, cmd.ID)
   897  	dupReporting, _ := bugReportingByID(dup, cmd.DupOf)
   898  	if bugReporting == nil || dupReporting == nil {
   899  		return nil, false, internalError, fmt.Errorf("can't find bug reporting")
   900  	}
   901  	if bugKey.StringID() == dupKey.StringID() {
   902  		if bugReporting.Name == dupReporting.Name {
   903  			return nil, false, "Can't dup bug to itself.", nil
   904  		}
   905  		return nil, false, fmt.Sprintf("Can't dup bug to itself in different reporting (%v->%v).\n"+
   906  			"Please dup syzbot bugs only onto syzbot bugs for the same kernel/reporting.",
   907  			bugReporting.Name, dupReporting.Name), nil
   908  	}
   909  	if bug.Namespace != dup.Namespace {
   910  		return nil, false, fmt.Sprintf("Duplicate bug corresponds to a different kernel (%v->%v).\n"+
   911  			"Please dup syzbot bugs only onto syzbot bugs for the same kernel.",
   912  			bug.Namespace, dup.Namespace), nil
   913  	}
   914  	if !allowCrossReportingDup(c, bug, dup, bugReporting, dupReporting) {
   915  		return nil, false, fmt.Sprintf("Can't dup bug to a bug in different reporting (%v->%v)."+
   916  			"Please dup syzbot bugs only onto syzbot bugs for the same kernel/reporting.",
   917  			bugReporting.Name, dupReporting.Name), nil
   918  	}
   919  	dupCanon, err := canonicalBug(c, dup)
   920  	if err != nil {
   921  		return nil, false, internalError, fmt.Errorf("failed to get canonical bug for dup: %w", err)
   922  	}
   923  	if !dupReporting.Closed.IsZero() && dupCanon.Status == BugStatusOpen {
   924  		return nil, false, "Dup bug is already upstreamed.", nil
   925  	}
   926  	if dupCanon.keyHash(c) == bugKey.StringID() {
   927  		return nil, false, "Setting this dup would lead to a bug cycle, cycles are not allowed.", nil
   928  	}
   929  	return dup, true, "", nil
   930  }
   931  
   932  func allowCrossReportingDup(c context.Context, bug, dup *Bug,
   933  	bugReporting, dupReporting *BugReporting) bool {
   934  	bugIdx := getReportingIdx(c, bug, bugReporting)
   935  	dupIdx := getReportingIdx(c, dup, dupReporting)
   936  	if bugIdx < 0 || dupIdx < 0 {
   937  		return false
   938  	}
   939  	if bugIdx == dupIdx {
   940  		return true
   941  	}
   942  	// We generally allow duping only within the same reporting.
   943  	// But there is one exception: we also allow duping from last but one
   944  	// reporting to the last one (which is stable, final destination)
   945  	// provided that these two reportings have the same access level and type.
   946  	// The rest of the combinations can lead to surprising states and
   947  	// information hiding, so we don't allow them.
   948  	cfg := getNsConfig(c, bug.Namespace)
   949  	bugConfig := &cfg.Reporting[bugIdx]
   950  	dupConfig := &cfg.Reporting[dupIdx]
   951  	lastIdx := len(cfg.Reporting) - 1
   952  	return bugIdx == lastIdx-1 && dupIdx == lastIdx &&
   953  		bugConfig.AccessLevel == dupConfig.AccessLevel &&
   954  		bugConfig.Config.Type() == dupConfig.Config.Type()
   955  }
   956  
   957  func getReportingIdx(c context.Context, bug *Bug, bugReporting *BugReporting) int {
   958  	for i := range bug.Reporting {
   959  		if bug.Reporting[i].Name == bugReporting.Name {
   960  			return i
   961  		}
   962  	}
   963  	log.Errorf(c, "failed to find bug reporting by name: %q/%q", bug.Title, bugReporting.Name)
   964  	return -1
   965  }
   966  
   967  func incomingCommandTx(c context.Context, now time.Time, cmd *dashapi.BugUpdate, bugKey, dupKey *db.Key) (
   968  	bool, string, error) {
   969  	bug := new(Bug)
   970  	if err := db.Get(c, bugKey, bug); err != nil {
   971  		return false, internalError, fmt.Errorf("can't find the corresponding bug: %w", err)
   972  	}
   973  	var dup *Bug
   974  	if cmd.Status == dashapi.BugStatusDup {
   975  		dup1, ok, reason, err := checkDupBug(c, cmd, bug, bugKey, dupKey)
   976  		if !ok || err != nil {
   977  			return ok, reason, err
   978  		}
   979  		dup = dup1
   980  	}
   981  	state, err := loadReportingState(c)
   982  	if err != nil {
   983  		return false, internalError, err
   984  	}
   985  	ok, reason, err := incomingCommandUpdate(c, now, cmd, bugKey, bug, dup, state)
   986  	if !ok || err != nil {
   987  		return ok, reason, err
   988  	}
   989  	if _, err := db.Put(c, bugKey, bug); err != nil {
   990  		return false, internalError, fmt.Errorf("failed to put bug: %w", err)
   991  	}
   992  	if err := saveReportingState(c, state); err != nil {
   993  		return false, internalError, err
   994  	}
   995  	return true, "", nil
   996  }
   997  
   998  func incomingCommandUpdate(c context.Context, now time.Time, cmd *dashapi.BugUpdate, bugKey *db.Key,
   999  	bug, dup *Bug, state *ReportingState) (bool, string, error) {
  1000  	bugReporting, final := bugReportingByID(bug, cmd.ID)
  1001  	if bugReporting == nil {
  1002  		return false, internalError, fmt.Errorf("can't find bug reporting")
  1003  	}
  1004  	if ok, reply, err := checkBugStatus(c, cmd, bug, bugReporting); !ok {
  1005  		return false, reply, err
  1006  	}
  1007  	stateEnt := state.getEntry(now, bug.Namespace, bugReporting.Name)
  1008  	if ok, reply, err := incomingCommandCmd(c, now, cmd, bug, dup, bugReporting, final, stateEnt); !ok {
  1009  		return false, reply, err
  1010  	}
  1011  	if (len(cmd.FixCommits) != 0 || cmd.ResetFixCommits) &&
  1012  		(bug.Status == BugStatusOpen || bug.Status == BugStatusDup) {
  1013  		sort.Strings(cmd.FixCommits)
  1014  		if !reflect.DeepEqual(bug.Commits, cmd.FixCommits) {
  1015  			bug.updateCommits(cmd.FixCommits, now)
  1016  		}
  1017  	}
  1018  	toReport := append([]int64{}, cmd.ReportCrashIDs...)
  1019  	if cmd.CrashID != 0 {
  1020  		bugReporting.CrashID = cmd.CrashID
  1021  		toReport = append(toReport, cmd.CrashID)
  1022  	}
  1023  	newRef := CrashReference{CrashReferenceReporting, bugReporting.Name, now}
  1024  	for _, crashID := range toReport {
  1025  		err := addCrashReference(c, crashID, bugKey, newRef)
  1026  		if err != nil {
  1027  			return false, internalError, err
  1028  		}
  1029  	}
  1030  	for _, crashID := range cmd.UnreportCrashIDs {
  1031  		err := removeCrashReference(c, crashID, bugKey, CrashReferenceReporting, bugReporting.Name)
  1032  		if err != nil {
  1033  			return false, internalError, err
  1034  		}
  1035  	}
  1036  	if bugReporting.ExtID == "" {
  1037  		bugReporting.ExtID = cmd.ExtID
  1038  	}
  1039  	if bugReporting.Link == "" {
  1040  		bugReporting.Link = cmd.Link
  1041  	}
  1042  	if len(cmd.CC) != 0 && cmd.Status != dashapi.BugStatusUnCC {
  1043  		merged := email.MergeEmailLists(strings.Split(bugReporting.CC, "|"), cmd.CC)
  1044  		bugReporting.CC = strings.Join(merged, "|")
  1045  	}
  1046  	if bugReporting.ReproLevel < cmd.ReproLevel {
  1047  		bugReporting.ReproLevel = cmd.ReproLevel
  1048  	}
  1049  	if bug.Status != BugStatusDup {
  1050  		bug.DupOf = ""
  1051  	}
  1052  	if cmd.Status != dashapi.BugStatusOpen || !cmd.OnHold {
  1053  		bugReporting.OnHold = time.Time{}
  1054  	}
  1055  	if cmd.Status != dashapi.BugStatusUpdate || cmd.ExtID != "" {
  1056  		// Update LastActivity only on important events.
  1057  		// Otherwise it impedes bug obsoletion.
  1058  		bug.LastActivity = now
  1059  	}
  1060  	return true, "", nil
  1061  }
  1062  
  1063  func incomingCommandCmd(c context.Context, now time.Time, cmd *dashapi.BugUpdate, bug, dup *Bug,
  1064  	bugReporting *BugReporting, final bool, stateEnt *ReportingStateEntry) (bool, string, error) {
  1065  	switch cmd.Status {
  1066  	case dashapi.BugStatusOpen:
  1067  		bug.Status = BugStatusOpen
  1068  		bug.Closed = time.Time{}
  1069  		stateEnt.Sent++
  1070  		if bugReporting.Reported.IsZero() {
  1071  			bugReporting.Reported = now
  1072  		}
  1073  		if bugReporting.OnHold.IsZero() && cmd.OnHold {
  1074  			bugReporting.OnHold = now
  1075  		}
  1076  		// Close all previous reporting if they are not closed yet
  1077  		// (can happen due to Status == ReportingDisabled).
  1078  		for i := range bug.Reporting {
  1079  			if bugReporting == &bug.Reporting[i] {
  1080  				break
  1081  			}
  1082  			if bug.Reporting[i].Closed.IsZero() {
  1083  				bug.Reporting[i].Closed = now
  1084  			}
  1085  		}
  1086  		if bug.ReproLevel < cmd.ReproLevel {
  1087  			return false, internalError,
  1088  				fmt.Errorf("bug update with invalid repro level: %v/%v",
  1089  					bug.ReproLevel, cmd.ReproLevel)
  1090  		}
  1091  	case dashapi.BugStatusUpstream:
  1092  		if final {
  1093  			return false, "Can't upstream, this is final destination.", nil
  1094  		}
  1095  		if len(bug.Commits) != 0 {
  1096  			// We could handle this case, but how/when it will occur
  1097  			// in real life is unclear now.
  1098  			return false, "Can't upstream this bug, the bug has fixing commits.", nil
  1099  		}
  1100  		bug.Status = BugStatusOpen
  1101  		bug.Closed = time.Time{}
  1102  		bugReporting.Closed = now
  1103  		bugReporting.Auto = cmd.Notification
  1104  	case dashapi.BugStatusInvalid:
  1105  		bug.Closed = now
  1106  		bug.Status = BugStatusInvalid
  1107  		bugReporting.Closed = now
  1108  		bugReporting.Auto = cmd.Notification
  1109  	case dashapi.BugStatusDup:
  1110  		bug.Status = BugStatusDup
  1111  		bug.Closed = now
  1112  		bug.DupOf = dup.keyHash(c)
  1113  	case dashapi.BugStatusUpdate:
  1114  		// Just update Link, Commits, etc below.
  1115  	case dashapi.BugStatusUnCC:
  1116  		bug.UNCC = email.MergeEmailLists(bug.UNCC, cmd.CC)
  1117  	default:
  1118  		return false, internalError, fmt.Errorf("unknown bug status %v", cmd.Status)
  1119  	}
  1120  	if cmd.StatusReason != "" {
  1121  		bug.StatusReason = cmd.StatusReason
  1122  	}
  1123  	for _, label := range cmd.Labels {
  1124  		bugReporting.AddLabel(label)
  1125  	}
  1126  	return true, "", nil
  1127  }
  1128  
  1129  func checkBugStatus(c context.Context, cmd *dashapi.BugUpdate, bug *Bug, bugReporting *BugReporting) (
  1130  	bool, string, error) {
  1131  	switch bug.Status {
  1132  	case BugStatusOpen:
  1133  	case BugStatusDup:
  1134  		canon, err := canonicalBug(c, bug)
  1135  		if err != nil {
  1136  			return false, internalError, err
  1137  		}
  1138  		if canon.Status != BugStatusOpen {
  1139  			// We used to reject updates to closed bugs,
  1140  			// but this is confusing and non-actionable for users.
  1141  			// So now we fail the update, but give empty reason,
  1142  			// which means "don't notify user".
  1143  			if cmd.Status == dashapi.BugStatusUpdate {
  1144  				// This happens when people discuss old bugs.
  1145  				log.Infof(c, "Dup bug is already closed")
  1146  			} else {
  1147  				log.Warningf(c, "incoming command %v: dup bug is already closed", cmd.ID)
  1148  			}
  1149  			return false, "", nil
  1150  		}
  1151  	case BugStatusFixed, BugStatusInvalid:
  1152  		if cmd.Status != dashapi.BugStatusUpdate {
  1153  			log.Errorf(c, "incoming command %v: bug is already closed", cmd.ID)
  1154  		}
  1155  		return false, "", nil
  1156  	default:
  1157  		return false, internalError, fmt.Errorf("unknown bug status %v", bug.Status)
  1158  	}
  1159  	if !bugReporting.Closed.IsZero() {
  1160  		if cmd.Status != dashapi.BugStatusUpdate {
  1161  			log.Errorf(c, "incoming command %v: bug reporting is already closed", cmd.ID)
  1162  		}
  1163  		return false, "", nil
  1164  	}
  1165  	return true, "", nil
  1166  }
  1167  
  1168  func findBugByReportingID(c context.Context, id string) (*Bug, *db.Key, error) {
  1169  	var bugs []*Bug
  1170  	keys, err := db.NewQuery("Bug").
  1171  		Filter("Reporting.ID=", id).
  1172  		Limit(2).
  1173  		GetAll(c, &bugs)
  1174  	if err != nil {
  1175  		return nil, nil, fmt.Errorf("failed to fetch bugs: %w", err)
  1176  	}
  1177  	if len(bugs) == 0 {
  1178  		return nil, nil, fmt.Errorf("failed to find bug by reporting id %q", id)
  1179  	}
  1180  	if len(bugs) > 1 {
  1181  		return nil, nil, fmt.Errorf("multiple bugs for reporting id %q", id)
  1182  	}
  1183  	return bugs[0], keys[0], nil
  1184  }
  1185  
  1186  func findDupByTitle(c context.Context, ns, title string) (*Bug, *db.Key, error) {
  1187  	title, seq, err := splitDisplayTitle(title)
  1188  	if err != nil {
  1189  		return nil, nil, err
  1190  	}
  1191  	bugHash := bugKeyHash(c, ns, title, seq)
  1192  	bugKey := db.NewKey(c, "Bug", bugHash, 0, nil)
  1193  	bug := new(Bug)
  1194  	if err := db.Get(c, bugKey, bug); err != nil {
  1195  		if err == db.ErrNoSuchEntity {
  1196  			return nil, nil, nil // This is not really an error, we should notify the user instead.
  1197  		}
  1198  		return nil, nil, fmt.Errorf("failed to get dup: %w", err)
  1199  	}
  1200  	return bug, bugKey, nil
  1201  }
  1202  
  1203  func bugReportingByID(bug *Bug, id string) (*BugReporting, bool) {
  1204  	for i := range bug.Reporting {
  1205  		if bug.Reporting[i].ID == id {
  1206  			return &bug.Reporting[i], i == len(bug.Reporting)-1
  1207  		}
  1208  	}
  1209  	return nil, false
  1210  }
  1211  
  1212  func bugReportingByName(bug *Bug, name string) *BugReporting {
  1213  	for i := range bug.Reporting {
  1214  		if bug.Reporting[i].Name == name {
  1215  			return &bug.Reporting[i]
  1216  		}
  1217  	}
  1218  	return nil
  1219  }
  1220  
  1221  func lastReportedReporting(bug *Bug) *BugReporting {
  1222  	for i := len(bug.Reporting) - 1; i >= 0; i-- {
  1223  		if !bug.Reporting[i].Reported.IsZero() {
  1224  			return &bug.Reporting[i]
  1225  		}
  1226  	}
  1227  	return nil
  1228  }
  1229  
  1230  // The updateReporting method is supposed to be called both to fully initialize a new
  1231  // Bug object and also to adjust it to the updated namespace configuration.
  1232  func (bug *Bug) updateReportings(c context.Context, cfg *Config, now time.Time) error {
  1233  	oldReportings := map[string]BugReporting{}
  1234  	oldPositions := map[string]int{}
  1235  	for i, rep := range bug.Reporting {
  1236  		oldReportings[rep.Name] = rep
  1237  		oldPositions[rep.Name] = i
  1238  	}
  1239  	maxPos := 0
  1240  	bug.Reporting = nil
  1241  	for _, rep := range cfg.Reporting {
  1242  		if oldRep, ok := oldReportings[rep.Name]; ok {
  1243  			oldPos := oldPositions[rep.Name]
  1244  			if oldPos < maxPos {
  1245  				// At the moment we only support insertions and deletions of reportings.
  1246  				// TODO: figure out what exactly can go wrong if we also allow reordering.
  1247  				return fmt.Errorf("the order of reportings is changed, before: %v", oldPositions)
  1248  			}
  1249  			maxPos = oldPos
  1250  			bug.Reporting = append(bug.Reporting, oldRep)
  1251  		} else {
  1252  			bug.Reporting = append(bug.Reporting, BugReporting{
  1253  				Name: rep.Name,
  1254  				ID:   bugReportingHash(bug.keyHash(c), rep.Name),
  1255  			})
  1256  		}
  1257  	}
  1258  	// We might have added new BugReporting objects between/before the ones that were
  1259  	// already reported. To let syzbot continue from the same reporting stage where it
  1260  	// stopped, close such outliers.
  1261  	seenProcessed := false
  1262  	minTime := now
  1263  	for i := len(bug.Reporting) - 1; i >= 0; i-- {
  1264  		rep := &bug.Reporting[i]
  1265  		if !rep.Reported.IsZero() || !rep.Closed.IsZero() || !rep.OnHold.IsZero() {
  1266  			if !rep.Reported.IsZero() && rep.Reported.Before(minTime) {
  1267  				minTime = rep.Reported
  1268  			}
  1269  			seenProcessed = true
  1270  		} else if seenProcessed {
  1271  			rep.Dummy = true
  1272  		}
  1273  	}
  1274  	// Yet another stage -- we set Closed and Reported to dummy objects in non-decreasing order.
  1275  	currTime := minTime
  1276  	for i := range bug.Reporting {
  1277  		rep := &bug.Reporting[i]
  1278  		if rep.Dummy {
  1279  			rep.Closed = currTime
  1280  			rep.Reported = currTime
  1281  		} else if rep.Reported.After(currTime) {
  1282  			currTime = rep.Reported
  1283  		}
  1284  	}
  1285  	return nil
  1286  }
  1287  
  1288  func findCrashForBug(c context.Context, bug *Bug) (*Crash, *db.Key, error) {
  1289  	bugKey := bug.key(c)
  1290  	crashes, keys, err := queryCrashesForBug(c, bugKey, 1)
  1291  	if err != nil {
  1292  		return nil, nil, err
  1293  	}
  1294  	if len(crashes) < 1 {
  1295  		return nil, nil, fmt.Errorf("no crashes")
  1296  	}
  1297  	crash, key := crashes[0], keys[0]
  1298  	if bug.HeadReproLevel == ReproLevelC {
  1299  		if crash.ReproC == 0 {
  1300  			log.Errorf(c, "bug '%v': has C repro, but crash without C repro", bug.Title)
  1301  		}
  1302  	} else if bug.HeadReproLevel == ReproLevelSyz {
  1303  		if crash.ReproSyz == 0 {
  1304  			log.Errorf(c, "bug '%v': has syz repro, but crash without syz repro", bug.Title)
  1305  		}
  1306  	}
  1307  	return crash, key, nil
  1308  }
  1309  
  1310  func loadReportingState(c context.Context) (*ReportingState, error) {
  1311  	state := new(ReportingState)
  1312  	key := db.NewKey(c, "ReportingState", "", 1, nil)
  1313  	if err := db.Get(c, key, state); err != nil && err != db.ErrNoSuchEntity {
  1314  		return nil, fmt.Errorf("failed to get reporting state: %w", err)
  1315  	}
  1316  	return state, nil
  1317  }
  1318  
  1319  func saveReportingState(c context.Context, state *ReportingState) error {
  1320  	key := db.NewKey(c, "ReportingState", "", 1, nil)
  1321  	if _, err := db.Put(c, key, state); err != nil {
  1322  		return fmt.Errorf("failed to put reporting state: %w", err)
  1323  	}
  1324  	return nil
  1325  }
  1326  
  1327  func (state *ReportingState) getEntry(now time.Time, namespace, name string) *ReportingStateEntry {
  1328  	if namespace == "" || name == "" {
  1329  		panic(fmt.Sprintf("requesting reporting state for %v/%v", namespace, name))
  1330  	}
  1331  	// Convert time to date of the form 20170125.
  1332  	date := timeDate(now)
  1333  	for i := range state.Entries {
  1334  		ent := &state.Entries[i]
  1335  		if ent.Namespace == namespace && ent.Name == name {
  1336  			if ent.Date != date {
  1337  				ent.Date = date
  1338  				ent.Sent = 0
  1339  			}
  1340  			return ent
  1341  		}
  1342  	}
  1343  	state.Entries = append(state.Entries, ReportingStateEntry{
  1344  		Namespace: namespace,
  1345  		Name:      name,
  1346  		Date:      date,
  1347  		Sent:      0,
  1348  	})
  1349  	return &state.Entries[len(state.Entries)-1]
  1350  }
  1351  
  1352  func loadFullBugInfo(c context.Context, bug *Bug, bugKey *db.Key,
  1353  	bugReporting *BugReporting) (*dashapi.FullBugInfo, error) {
  1354  	reporting := getNsConfig(c, bug.Namespace).ReportingByName(bugReporting.Name)
  1355  	if reporting == nil {
  1356  		return nil, fmt.Errorf("failed to find the reporting object")
  1357  	}
  1358  	ret := &dashapi.FullBugInfo{}
  1359  	// Query bisections.
  1360  	var err error
  1361  	if bug.BisectCause > BisectPending {
  1362  		ret.BisectCause, err = prepareBisectionReport(c, bug, JobBisectCause, reporting)
  1363  		if err != nil {
  1364  			return nil, err
  1365  		}
  1366  	}
  1367  	if bug.BisectFix > BisectPending {
  1368  		ret.BisectFix, err = prepareBisectionReport(c, bug, JobBisectFix, reporting)
  1369  		if err != nil {
  1370  			return nil, err
  1371  		}
  1372  	}
  1373  	// Query the fix candidate.
  1374  	if bug.FixCandidateJob != "" {
  1375  		ret.FixCandidate, err = prepareFixCandidateReport(c, bug, reporting)
  1376  		if err != nil {
  1377  			return nil, err
  1378  		}
  1379  	}
  1380  	// Query similar bugs.
  1381  	similar, err := loadSimilarBugs(c, bug)
  1382  	if err != nil {
  1383  		return nil, err
  1384  	}
  1385  	for _, similarBug := range similar {
  1386  		_, bugReporting, _, _, _ := currentReporting(c, similarBug)
  1387  		if bugReporting == nil {
  1388  			continue
  1389  		}
  1390  		status, err := similarBug.dashapiStatus()
  1391  		if err != nil {
  1392  			return nil, err
  1393  		}
  1394  		ret.SimilarBugs = append(ret.SimilarBugs, &dashapi.SimilarBugInfo{
  1395  			Title:      similarBug.displayTitle(),
  1396  			Status:     status,
  1397  			Namespace:  similarBug.Namespace,
  1398  			ReproLevel: similarBug.ReproLevel,
  1399  			Link:       fmt.Sprintf("%v/bug?extid=%v", appURL(c), bugReporting.ID),
  1400  			ReportLink: bugReporting.Link,
  1401  			Closed:     similarBug.Closed,
  1402  		})
  1403  	}
  1404  	// Query crashes.
  1405  	crashes, err := representativeCrashes(c, bugKey)
  1406  	if err != nil {
  1407  		return nil, err
  1408  	}
  1409  	for _, crash := range crashes {
  1410  		rep, err := crashBugReport(c, bug, crash.crash, crash.key, bugReporting, reporting)
  1411  		if err != nil {
  1412  			return nil, fmt.Errorf("crash %d: %w", crash.key.IntID(), err)
  1413  		}
  1414  		ret.Crashes = append(ret.Crashes, rep)
  1415  	}
  1416  	// Query tree testing jobs.
  1417  	ret.TreeJobs, err = treeTestJobs(c, bug)
  1418  	if err != nil {
  1419  		return nil, err
  1420  	}
  1421  	return ret, nil
  1422  }
  1423  
  1424  func prepareFixCandidateReport(c context.Context, bug *Bug, reporting *Reporting) (*dashapi.BugReport, error) {
  1425  	job, jobKey, err := fetchJob(c, bug.FixCandidateJob)
  1426  	if err != nil {
  1427  		return nil, err
  1428  	}
  1429  	ret, err := createBugReportForJob(c, job, jobKey, reporting.Config)
  1430  	if err != nil {
  1431  		return nil, fmt.Errorf("failed to create the job bug report: %w", err)
  1432  	}
  1433  	return ret, nil
  1434  }
  1435  
  1436  func prepareBisectionReport(c context.Context, bug *Bug, jobType JobType,
  1437  	reporting *Reporting) (*dashapi.BugReport, error) {
  1438  	bisectionJob, err := queryBestBisection(c, bug, jobType)
  1439  	if bisectionJob == nil || err != nil {
  1440  		return nil, err
  1441  	}
  1442  	job, jobKey := bisectionJob.job, bisectionJob.key
  1443  	if job.Reporting != "" {
  1444  		ret, err := createBugReportForJob(c, job, jobKey, reporting.Config)
  1445  		if err != nil {
  1446  			return nil, fmt.Errorf("failed to create the job bug report: %w", err)
  1447  		}
  1448  		return ret, nil
  1449  	}
  1450  	return nil, nil
  1451  }
  1452  
  1453  type crashWithKey struct {
  1454  	crash *Crash
  1455  	key   *db.Key
  1456  }
  1457  
  1458  func representativeCrashes(c context.Context, bugKey *db.Key) ([]*crashWithKey, error) {
  1459  	// There should generally be no need to query lots of crashes.
  1460  	const fetchCrashes = 50
  1461  	allCrashes, allCrashKeys, err := queryCrashesForBug(c, bugKey, fetchCrashes)
  1462  	if err != nil {
  1463  		return nil, err
  1464  	}
  1465  	// Let's consider a crash fresh if it happened within the last week.
  1466  	const recentDuration = time.Hour * 24 * 7
  1467  	now := timeNow(c)
  1468  
  1469  	var bestCrash, recentCrash, straceCrash *crashWithKey
  1470  	for id, crash := range allCrashes {
  1471  		key := allCrashKeys[id]
  1472  		if bestCrash == nil {
  1473  			bestCrash = &crashWithKey{crash, key}
  1474  		}
  1475  		if now.Sub(crash.Time) < recentDuration && recentCrash == nil {
  1476  			recentCrash = &crashWithKey{crash, key}
  1477  		}
  1478  		if dashapi.CrashFlags(crash.Flags)&dashapi.CrashUnderStrace > 0 &&
  1479  			straceCrash == nil {
  1480  			straceCrash = &crashWithKey{crash, key}
  1481  		}
  1482  	}
  1483  	// It's possible that there are so many crashes with reproducers that
  1484  	// we do not get to see the recent crashes without them.
  1485  	// Give it one more try.
  1486  	if recentCrash == nil {
  1487  		var crashes []*Crash
  1488  		keys, err := db.NewQuery("Crash").
  1489  			Ancestor(bugKey).
  1490  			Order("-Time").
  1491  			Limit(1).
  1492  			GetAll(c, &crashes)
  1493  		if err != nil {
  1494  			return nil, fmt.Errorf("failed to fetch the latest crash: %w", err)
  1495  		}
  1496  		if len(crashes) > 0 {
  1497  			recentCrash = &crashWithKey{crashes[0], keys[0]}
  1498  		}
  1499  	}
  1500  	dedup := map[int64]bool{}
  1501  	crashes := []*crashWithKey{}
  1502  	for _, item := range []*crashWithKey{recentCrash, bestCrash, straceCrash} {
  1503  		if item == nil || dedup[item.key.IntID()] {
  1504  			continue
  1505  		}
  1506  		crashes = append(crashes, item)
  1507  		dedup[item.key.IntID()] = true
  1508  	}
  1509  	// Sort by Time in desc order.
  1510  	sort.Slice(crashes, func(i, j int) bool {
  1511  		return crashes[i].crash.Time.After(crashes[j].crash.Time)
  1512  	})
  1513  	return crashes, nil
  1514  }
  1515  
  1516  // bugReportSorter sorts bugs by priority we want to report them.
  1517  // E.g. we want to report bugs with reproducers before bugs without reproducers.
  1518  type bugReportSorter []*Bug
  1519  
  1520  func (a bugReportSorter) Len() int      { return len(a) }
  1521  func (a bugReportSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
  1522  func (a bugReportSorter) Less(i, j int) bool {
  1523  	if a[i].ReproLevel != a[j].ReproLevel {
  1524  		return a[i].ReproLevel > a[j].ReproLevel
  1525  	}
  1526  	if a[i].HasReport != a[j].HasReport {
  1527  		return a[i].HasReport
  1528  	}
  1529  	if a[i].NumCrashes != a[j].NumCrashes {
  1530  		return a[i].NumCrashes > a[j].NumCrashes
  1531  	}
  1532  	return a[i].FirstTime.Before(a[j].FirstTime)
  1533  }
  1534  
  1535  // kernelArch returns arch as kernel developers know it (rather than Go names).
  1536  // Currently Linux-specific.
  1537  func kernelArch(arch string) string {
  1538  	switch arch {
  1539  	case targets.I386:
  1540  		return "i386"
  1541  	default:
  1542  		return arch
  1543  	}
  1544  }