github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/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  	minVal, maxVal := config.Obsoleting.MinPeriod, config.Obsoleting.MaxPeriod
   370  	if config.Obsoleting.NonFinalMinPeriod != 0 &&
   371  		bug.Reporting[len(bug.Reporting)-1].Reported.IsZero() {
   372  		minVal, maxVal = config.Obsoleting.NonFinalMinPeriod, config.Obsoleting.NonFinalMaxPeriod
   373  	}
   374  	if mgr := bug.managerConfig(c); mgr != nil && mgr.ObsoletingMinPeriod != 0 {
   375  		minVal, maxVal = mgr.ObsoletingMinPeriod, mgr.ObsoletingMaxPeriod
   376  	}
   377  	period = max(period, minVal)
   378  	period = min(period, maxVal)
   379  	return period
   380  }
   381  
   382  func (bug *Bug) managerConfig(c context.Context) *ConfigManager {
   383  	if len(bug.HappenedOn) != 1 {
   384  		return nil
   385  	}
   386  	mgr := getNsConfig(c, bug.Namespace).Managers[bug.HappenedOn[0]]
   387  	return &mgr
   388  }
   389  
   390  func createNotification(c context.Context, typ dashapi.BugNotif, public bool, text string, bug *Bug,
   391  	reporting *Reporting, bugReporting *BugReporting) (*dashapi.BugNotification, error) {
   392  	reportingConfig, err := json.Marshal(reporting.Config)
   393  	if err != nil {
   394  		return nil, err
   395  	}
   396  	crash, _, err := findCrashForBug(c, bug)
   397  	if err != nil {
   398  		return nil, err
   399  	}
   400  	build, err := loadBuild(c, bug.Namespace, crash.BuildID)
   401  	if err != nil {
   402  		return nil, err
   403  	}
   404  	kernelRepo := kernelRepoInfo(c, build)
   405  	notif := &dashapi.BugNotification{
   406  		Type:      typ,
   407  		Namespace: bug.Namespace,
   408  		Config:    reportingConfig,
   409  		ID:        bugReporting.ID,
   410  		ExtID:     bugReporting.ExtID,
   411  		Title:     bug.displayTitle(),
   412  		Text:      text,
   413  		Public:    public,
   414  		Link:      fmt.Sprintf("%v/bug?extid=%v", appURL(c), bugReporting.ID),
   415  		CC:        kernelRepo.CC.Always,
   416  	}
   417  	if public {
   418  		notif.Maintainers = append(crash.Maintainers, kernelRepo.CC.Maintainers...)
   419  	}
   420  	if (public || reporting.moderation) && bugReporting.CC != "" {
   421  		notif.CC = append(notif.CC, strings.Split(bugReporting.CC, "|")...)
   422  	}
   423  	if mgr := bug.managerConfig(c); mgr != nil {
   424  		notif.CC = append(notif.CC, mgr.CC.Always...)
   425  		if public {
   426  			notif.Maintainers = append(notif.Maintainers, mgr.CC.Maintainers...)
   427  		}
   428  	}
   429  	return notif, nil
   430  }
   431  
   432  func currentReporting(c context.Context, bug *Bug) (*Reporting, *BugReporting, int, string, error) {
   433  	if bug.NumCrashes == 0 {
   434  		// This is possible during the short window when we already created a bug,
   435  		// but did not attach the first crash to it yet. We need to avoid reporting this bug yet
   436  		// and wait for the crash. Otherwise reporting filter may mis-classify it as e.g.
   437  		// not having a report or something else.
   438  		return nil, nil, 0, "no crashes yet", nil
   439  	}
   440  	for i := range bug.Reporting {
   441  		bugReporting := &bug.Reporting[i]
   442  		if !bugReporting.Closed.IsZero() {
   443  			continue
   444  		}
   445  		reporting := getNsConfig(c, bug.Namespace).ReportingByName(bugReporting.Name)
   446  		if reporting == nil {
   447  			return nil, nil, 0, "", fmt.Errorf("%v: missing in config", bugReporting.Name)
   448  		}
   449  		if reporting.DailyLimit == 0 {
   450  			return nil, nil, 0, fmt.Sprintf("%v: reporting has daily limit 0", reporting.DisplayTitle), nil
   451  		}
   452  		switch reporting.Filter(bug) {
   453  		case FilterSkip:
   454  			if bugReporting.Reported.IsZero() {
   455  				continue
   456  			}
   457  			fallthrough
   458  		case FilterReport:
   459  			return reporting, bugReporting, i, "", nil
   460  		case FilterHold:
   461  			return nil, nil, 0, fmt.Sprintf("%v: reporting suspended", reporting.DisplayTitle), nil
   462  		}
   463  	}
   464  	return nil, nil, 0, "", fmt.Errorf("no reporting left")
   465  }
   466  
   467  func reproStr(level dashapi.ReproLevel) string {
   468  	switch level {
   469  	case ReproLevelSyz:
   470  		return " syz repro"
   471  	case ReproLevelC:
   472  		return " C repro"
   473  	default:
   474  		return ""
   475  	}
   476  }
   477  
   478  // nolint: gocyclo
   479  func createBugReport(c context.Context, bug *Bug, crash *Crash, crashKey *db.Key,
   480  	bugReporting *BugReporting, reporting *Reporting) (*dashapi.BugReport, error) {
   481  	var job *Job
   482  	if bug.BisectCause == BisectYes || bug.BisectCause == BisectInconclusive || bug.BisectCause == BisectHorizont {
   483  		// If we have bisection results, report the crash/repro used for bisection.
   484  		causeBisect, err := queryBestBisection(c, bug, JobBisectCause)
   485  		if err != nil {
   486  			return nil, err
   487  		}
   488  		if causeBisect != nil {
   489  			err := causeBisect.loadCrash(c)
   490  			if err != nil {
   491  				return nil, err
   492  			}
   493  		}
   494  		// If we didn't check whether the bisect is unreliable, even though it would not be
   495  		// reported anyway, we could still eventually Cc people from those commits later
   496  		// (e.g. when we did bisected with a syz repro and then notified about a C repro).
   497  		if causeBisect != nil && !causeBisect.job.isUnreliableBisect() {
   498  			job = causeBisect.job
   499  			if causeBisect.crash.ReproC != 0 || crash.ReproC == 0 {
   500  				// Don't override the crash in this case,
   501  				// otherwise we will always think that we haven't reported the C repro.
   502  				crash, crashKey = causeBisect.crash, causeBisect.crashKey
   503  			}
   504  		}
   505  	}
   506  	rep, err := crashBugReport(c, bug, crash, crashKey, bugReporting, reporting)
   507  	if err != nil {
   508  		return nil, err
   509  	}
   510  	if job != nil {
   511  		cause, emails := bisectFromJob(c, job)
   512  		rep.BisectCause = cause
   513  		rep.Maintainers = append(rep.Maintainers, emails...)
   514  	}
   515  	return rep, nil
   516  }
   517  
   518  // crashBugReport fills in crash and build related fields into *dashapi.BugReport.
   519  func crashBugReport(c context.Context, bug *Bug, crash *Crash, crashKey *db.Key,
   520  	bugReporting *BugReporting, reporting *Reporting) (*dashapi.BugReport, error) {
   521  	reportingConfig, err := json.Marshal(reporting.Config)
   522  	if err != nil {
   523  		return nil, err
   524  	}
   525  	crashLog, _, err := getText(c, textCrashLog, crash.Log)
   526  	if err != nil {
   527  		return nil, err
   528  	}
   529  	report, _, err := getText(c, textCrashReport, crash.Report)
   530  	if err != nil {
   531  		return nil, err
   532  	}
   533  	if len(report) > maxMailReportLen {
   534  		report = report[:maxMailReportLen]
   535  	}
   536  	machineInfo, _, err := getText(c, textMachineInfo, crash.MachineInfo)
   537  	if err != nil {
   538  		return nil, err
   539  	}
   540  	build, err := loadBuild(c, bug.Namespace, crash.BuildID)
   541  	if err != nil {
   542  		return nil, err
   543  	}
   544  	typ := dashapi.ReportNew
   545  	if !bugReporting.Reported.IsZero() {
   546  		typ = dashapi.ReportRepro
   547  	}
   548  	assetList := createAssetList(c, build, crash, true)
   549  	kernelRepo := kernelRepoInfo(c, build)
   550  	rep := &dashapi.BugReport{
   551  		Type:            typ,
   552  		Config:          reportingConfig,
   553  		ExtID:           bugReporting.ExtID,
   554  		First:           bugReporting.Reported.IsZero(),
   555  		Moderation:      reporting.moderation,
   556  		Log:             crashLog,
   557  		LogLink:         externalLink(c, textCrashLog, crash.Log),
   558  		LogHasStrace:    dashapi.CrashFlags(crash.Flags)&dashapi.CrashUnderStrace > 0,
   559  		Report:          report,
   560  		ReportLink:      externalLink(c, textCrashReport, crash.Report),
   561  		CC:              kernelRepo.CC.Always,
   562  		Maintainers:     append(crash.Maintainers, kernelRepo.CC.Maintainers...),
   563  		ReproOpts:       crash.ReproOpts,
   564  		MachineInfo:     machineInfo,
   565  		MachineInfoLink: externalLink(c, textMachineInfo, crash.MachineInfo),
   566  		CrashID:         crashKey.IntID(),
   567  		CrashTime:       crash.Time,
   568  		NumCrashes:      bug.NumCrashes,
   569  		HappenedOn:      managersToRepos(c, bug.Namespace, bug.HappenedOn),
   570  		Manager:         crash.Manager,
   571  		Assets:          assetList,
   572  		ReportElements:  &dashapi.ReportElements{GuiltyFiles: crash.ReportElements.GuiltyFiles},
   573  		ReproIsRevoked:  crash.ReproIsRevoked,
   574  	}
   575  	rep.ReproCLink = externalLink(c, textReproC, crash.ReproC)
   576  	rep.ReproC, _, err = getText(c, textReproC, crash.ReproC)
   577  	if err != nil {
   578  		return nil, err
   579  	}
   580  	rep.ReproSyzLink = externalLink(c, textReproSyz, crash.ReproSyz)
   581  	rep.ReproSyz, err = loadReproSyz(c, crash)
   582  	if err != nil {
   583  		return nil, err
   584  	}
   585  	if bugReporting.CC != "" {
   586  		rep.CC = append(rep.CC, strings.Split(bugReporting.CC, "|")...)
   587  	}
   588  	if build.Type == BuildFailed {
   589  		rep.Maintainers = append(rep.Maintainers, kernelRepo.CC.BuildMaintainers...)
   590  	}
   591  	if mgr := bug.managerConfig(c); mgr != nil {
   592  		rep.CC = append(rep.CC, mgr.CC.Always...)
   593  		rep.Maintainers = append(rep.Maintainers, mgr.CC.Maintainers...)
   594  		if build.Type == BuildFailed {
   595  			rep.Maintainers = append(rep.Maintainers, mgr.CC.BuildMaintainers...)
   596  		}
   597  	}
   598  	for _, label := range bug.Labels {
   599  		text, ok := reporting.Labels[label.String()]
   600  		if !ok {
   601  			continue
   602  		}
   603  		if rep.LabelMessages == nil {
   604  			rep.LabelMessages = map[string]string{}
   605  		}
   606  		rep.LabelMessages[label.String()] = text
   607  	}
   608  	if err := fillBugReport(c, rep, bug, bugReporting, build); err != nil {
   609  		return nil, err
   610  	}
   611  	return rep, nil
   612  }
   613  
   614  func loadReproSyz(c context.Context, crash *Crash) ([]byte, error) {
   615  	reproSyz, _, err := getText(c, textReproSyz, crash.ReproSyz)
   616  	if err != nil || len(reproSyz) == 0 {
   617  		return nil, err
   618  	}
   619  	buf := new(bytes.Buffer)
   620  	buf.WriteString(syzReproPrefix)
   621  	if len(crash.ReproOpts) != 0 {
   622  		fmt.Fprintf(buf, "#%s\n", crash.ReproOpts)
   623  	}
   624  	buf.Write(reproSyz)
   625  	return buf.Bytes(), nil
   626  }
   627  
   628  // fillBugReport fills common report fields for bug and job reports.
   629  func fillBugReport(c context.Context, rep *dashapi.BugReport, bug *Bug, bugReporting *BugReporting,
   630  	build *Build) error {
   631  	kernelConfig, _, err := getText(c, textKernelConfig, build.KernelConfig)
   632  	if err != nil {
   633  		return err
   634  	}
   635  	creditEmail, err := email.AddAddrContext(ownEmail(c), bugReporting.ID)
   636  	if err != nil {
   637  		return err
   638  	}
   639  	rep.BugStatus, err = bug.dashapiStatus()
   640  	if err != nil {
   641  		return err
   642  	}
   643  	rep.Namespace = bug.Namespace
   644  	rep.ID = bugReporting.ID
   645  	rep.Title = bug.displayTitle()
   646  	rep.Link = fmt.Sprintf("%v/bug?extid=%v", appURL(c), bugReporting.ID)
   647  	rep.CreditEmail = creditEmail
   648  	rep.OS = build.OS
   649  	rep.Arch = build.Arch
   650  	rep.VMArch = build.VMArch
   651  	rep.UserSpaceArch = kernelArch(build.Arch)
   652  	rep.BuildID = build.ID
   653  	rep.BuildTime = build.Time
   654  	rep.CompilerID = build.CompilerID
   655  	rep.KernelRepo = build.KernelRepo
   656  	rep.KernelRepoAlias = kernelRepoInfo(c, build).Alias
   657  	rep.KernelBranch = build.KernelBranch
   658  	rep.KernelCommit = build.KernelCommit
   659  	rep.KernelCommitTitle = build.KernelCommitTitle
   660  	rep.KernelCommitDate = build.KernelCommitDate
   661  	rep.KernelConfig = kernelConfig
   662  	rep.KernelConfigLink = externalLink(c, textKernelConfig, build.KernelConfig)
   663  	rep.SyzkallerCommit = build.SyzkallerCommit
   664  	rep.NoRepro = build.Type == BuildFailed
   665  	for _, item := range bug.LabelValues(SubsystemLabel) {
   666  		rep.Subsystems = append(rep.Subsystems, dashapi.BugSubsystem{
   667  			Name:  item.Value,
   668  			SetBy: item.SetBy,
   669  			Link:  fmt.Sprintf("%v/%s/s/%s", appURL(c), bug.Namespace, item.Value),
   670  		})
   671  		rep.Maintainers = email.MergeEmailLists(rep.Maintainers,
   672  			subsystemMaintainers(c, rep.Namespace, item.Value))
   673  	}
   674  	for _, addr := range bug.UNCC {
   675  		rep.CC = email.RemoveFromEmailList(rep.CC, addr)
   676  		rep.Maintainers = email.RemoveFromEmailList(rep.Maintainers, addr)
   677  	}
   678  	return nil
   679  }
   680  
   681  func managersToRepos(c context.Context, ns string, managers []string) []string {
   682  	var repos []string
   683  	dedup := make(map[string]bool)
   684  	for _, manager := range managers {
   685  		build, err := lastManagerBuild(c, ns, manager)
   686  		if err != nil {
   687  			log.Errorf(c, "failed to get manager %q build: %v", manager, err)
   688  			continue
   689  		}
   690  		repo := kernelRepoInfo(c, build).Alias
   691  		if dedup[repo] {
   692  			continue
   693  		}
   694  		dedup[repo] = true
   695  		repos = append(repos, repo)
   696  	}
   697  	sort.Strings(repos)
   698  	return repos
   699  }
   700  
   701  func queryCrashesForBug(c context.Context, bugKey *db.Key, limit int) (
   702  	[]*Crash, []*db.Key, error) {
   703  	var crashes []*Crash
   704  	keys, err := db.NewQuery("Crash").
   705  		Ancestor(bugKey).
   706  		Order("-ReportLen").
   707  		Order("-Time").
   708  		Limit(limit).
   709  		GetAll(c, &crashes)
   710  	if err != nil {
   711  		return nil, nil, fmt.Errorf("failed to fetch crashes: %w", err)
   712  	}
   713  	return crashes, keys, nil
   714  }
   715  
   716  func loadAllBugs(c context.Context, filter func(*db.Query) *db.Query) ([]*Bug, []*db.Key, error) {
   717  	var bugs []*Bug
   718  	var keys []*db.Key
   719  	err := foreachBug(c, filter, func(bug *Bug, key *db.Key) error {
   720  		bugs = append(bugs, bug)
   721  		keys = append(keys, key)
   722  		return nil
   723  	})
   724  	if err != nil {
   725  		return nil, nil, err
   726  	}
   727  	return bugs, keys, nil
   728  }
   729  
   730  func loadNamespaceBugs(c context.Context, ns string) ([]*Bug, []*db.Key, error) {
   731  	return loadAllBugs(c, func(query *db.Query) *db.Query {
   732  		return query.Filter("Namespace=", ns)
   733  	})
   734  }
   735  
   736  func loadOpenBugs(c context.Context) ([]*Bug, []*db.Key, error) {
   737  	return loadAllBugs(c, func(query *db.Query) *db.Query {
   738  		return query.Filter("Status<", BugStatusFixed)
   739  	})
   740  }
   741  
   742  func foreachBug(c context.Context, filter func(*db.Query) *db.Query, fn func(bug *Bug, key *db.Key) error) error {
   743  	const batchSize = 2000
   744  	var cursor *db.Cursor
   745  	for {
   746  		query := db.NewQuery("Bug").Limit(batchSize)
   747  		if filter != nil {
   748  			query = filter(query)
   749  		}
   750  		if cursor != nil {
   751  			query = query.Start(*cursor)
   752  		}
   753  		iter := query.Run(c)
   754  		for i := 0; ; i++ {
   755  			bug := new(Bug)
   756  			key, err := iter.Next(bug)
   757  			if err == db.Done {
   758  				if i < batchSize {
   759  					return nil
   760  				}
   761  				break
   762  			}
   763  			if err != nil {
   764  				return fmt.Errorf("failed to fetch bugs: %w", err)
   765  			}
   766  			if err := fn(bug, key); err != nil {
   767  				return err
   768  			}
   769  		}
   770  		cur, err := iter.Cursor()
   771  		if err != nil {
   772  			return fmt.Errorf("cursor failed while fetching bugs: %w", err)
   773  		}
   774  		cursor = &cur
   775  	}
   776  }
   777  
   778  func filterBugs(bugs []*Bug, keys []*db.Key, filter func(*Bug) bool) ([]*Bug, []*db.Key) {
   779  	var retBugs []*Bug
   780  	var retKeys []*db.Key
   781  	for i, bug := range bugs {
   782  		if filter(bug) {
   783  			retBugs = append(retBugs, bugs[i])
   784  			retKeys = append(retKeys, keys[i])
   785  		}
   786  	}
   787  	return retBugs, retKeys
   788  }
   789  
   790  // reportingPollClosed is called by backends to get list of closed bugs.
   791  func reportingPollClosed(c context.Context, ids []string) ([]string, error) {
   792  	idMap := make(map[string]bool, len(ids))
   793  	for _, id := range ids {
   794  		idMap[id] = true
   795  	}
   796  	var closed []string
   797  	err := foreachBug(c, nil, func(bug *Bug, _ *db.Key) error {
   798  		for i := range bug.Reporting {
   799  			bugReporting := &bug.Reporting[i]
   800  			if !idMap[bugReporting.ID] {
   801  				continue
   802  			}
   803  			var err error
   804  			bug, err = canonicalBug(c, bug)
   805  			if err != nil {
   806  				log.Errorf(c, "%v", err)
   807  				break
   808  			}
   809  			if bug.Status >= BugStatusFixed || !bugReporting.Closed.IsZero() ||
   810  				getNsConfig(c, bug.Namespace).Decommissioned {
   811  				closed = append(closed, bugReporting.ID)
   812  			}
   813  			break
   814  		}
   815  		return nil
   816  	})
   817  	return closed, err
   818  }
   819  
   820  // incomingCommand is entry point to bug status updates.
   821  func incomingCommand(c context.Context, cmd *dashapi.BugUpdate) (bool, string, error) {
   822  	log.Infof(c, "got command: %+v", cmd)
   823  	ok, reason, err := incomingCommandImpl(c, cmd)
   824  	if err != nil {
   825  		log.Errorf(c, "%v (%v)", reason, err)
   826  	} else if !ok && reason != "" {
   827  		log.Errorf(c, "invalid update: %v", reason)
   828  	}
   829  	return ok, reason, err
   830  }
   831  
   832  func incomingCommandImpl(c context.Context, cmd *dashapi.BugUpdate) (bool, string, error) {
   833  	for i, com := range cmd.FixCommits {
   834  		if len(com) >= 2 && com[0] == '"' && com[len(com)-1] == '"' {
   835  			com = com[1 : len(com)-1]
   836  			cmd.FixCommits[i] = com
   837  		}
   838  		if len(com) < 3 {
   839  			return false, fmt.Sprintf("bad commit title: %q", com), nil
   840  		}
   841  	}
   842  	bug, bugKey, err := findBugByReportingID(c, cmd.ID)
   843  	if err != nil {
   844  		return false, internalError, err
   845  	}
   846  	var dupKey *db.Key
   847  	if cmd.Status == dashapi.BugStatusDup {
   848  		if looksLikeReportingHash(cmd.DupOf) {
   849  			_, dupKey, _ = findBugByReportingID(c, cmd.DupOf)
   850  		}
   851  		if dupKey == nil {
   852  			// Email reporting passes bug title in cmd.DupOf, try to find bug by title.
   853  			var dup *Bug
   854  			dup, dupKey, err = findDupByTitle(c, bug.Namespace, cmd.DupOf)
   855  			if err != nil || dup == nil {
   856  				return false, "can't find the dup bug", err
   857  			}
   858  			dupReporting := lastReportedReporting(dup)
   859  			if dupReporting == nil {
   860  				return false, "can't find the dup bug", fmt.Errorf("dup does not have reporting")
   861  			}
   862  			cmd.DupOf = dupReporting.ID
   863  		}
   864  	}
   865  	now := timeNow(c)
   866  	ok, reply := false, ""
   867  	tx := func(c context.Context) error {
   868  		var err error
   869  		ok, reply, err = incomingCommandTx(c, now, cmd, bugKey, dupKey)
   870  		return err
   871  	}
   872  	err = runInTransaction(c, tx, &db.TransactionOptions{
   873  		XG: true,
   874  		// We don't want incoming bug updates to fail,
   875  		// because for e.g. email we won't have an external retry.
   876  		Attempts: 30,
   877  	})
   878  	if err != nil {
   879  		return false, internalError, err
   880  	}
   881  	return ok, reply, nil
   882  }
   883  
   884  func checkDupBug(c context.Context, cmd *dashapi.BugUpdate, bug *Bug, bugKey, dupKey *db.Key) (
   885  	*Bug, bool, string, error) {
   886  	dup := new(Bug)
   887  	if err := db.Get(c, dupKey, dup); err != nil {
   888  		return nil, false, internalError, fmt.Errorf("can't find the dup by key: %w", err)
   889  	}
   890  	bugReporting, _ := bugReportingByID(bug, cmd.ID)
   891  	dupReporting, _ := bugReportingByID(dup, cmd.DupOf)
   892  	if bugReporting == nil || dupReporting == nil {
   893  		return nil, false, internalError, fmt.Errorf("can't find bug reporting")
   894  	}
   895  	if bugKey.StringID() == dupKey.StringID() {
   896  		if bugReporting.Name == dupReporting.Name {
   897  			return nil, false, "Can't dup bug to itself.", nil
   898  		}
   899  		return nil, false, fmt.Sprintf("Can't dup bug to itself in different reporting (%v->%v).\n"+
   900  			"Please dup syzbot bugs only onto syzbot bugs for the same kernel/reporting.",
   901  			bugReporting.Name, dupReporting.Name), nil
   902  	}
   903  	if bug.Namespace != dup.Namespace {
   904  		return nil, false, fmt.Sprintf("Duplicate bug corresponds to a different kernel (%v->%v).\n"+
   905  			"Please dup syzbot bugs only onto syzbot bugs for the same kernel.",
   906  			bug.Namespace, dup.Namespace), nil
   907  	}
   908  	if !allowCrossReportingDup(c, bug, dup, bugReporting, dupReporting) {
   909  		return nil, false, fmt.Sprintf("Can't dup bug to a bug in different reporting (%v->%v)."+
   910  			"Please dup syzbot bugs only onto syzbot bugs for the same kernel/reporting.",
   911  			bugReporting.Name, dupReporting.Name), nil
   912  	}
   913  	dupCanon, err := canonicalBug(c, dup)
   914  	if err != nil {
   915  		return nil, false, internalError, fmt.Errorf("failed to get canonical bug for dup: %w", err)
   916  	}
   917  	if !dupReporting.Closed.IsZero() && dupCanon.Status == BugStatusOpen {
   918  		return nil, false, "Dup bug is already upstreamed.", nil
   919  	}
   920  	if dupCanon.keyHash(c) == bugKey.StringID() {
   921  		return nil, false, "Setting this dup would lead to a bug cycle, cycles are not allowed.", nil
   922  	}
   923  	return dup, true, "", nil
   924  }
   925  
   926  func allowCrossReportingDup(c context.Context, bug, dup *Bug,
   927  	bugReporting, dupReporting *BugReporting) bool {
   928  	bugIdx := getReportingIdx(c, bug, bugReporting)
   929  	dupIdx := getReportingIdx(c, dup, dupReporting)
   930  	if bugIdx < 0 || dupIdx < 0 {
   931  		return false
   932  	}
   933  	if bugIdx == dupIdx {
   934  		return true
   935  	}
   936  	// We generally allow duping only within the same reporting.
   937  	// But there is one exception: we also allow duping from last but one
   938  	// reporting to the last one (which is stable, final destination)
   939  	// provided that these two reportings have the same access level and type.
   940  	// The rest of the combinations can lead to surprising states and
   941  	// information hiding, so we don't allow them.
   942  	cfg := getNsConfig(c, bug.Namespace)
   943  	bugConfig := &cfg.Reporting[bugIdx]
   944  	dupConfig := &cfg.Reporting[dupIdx]
   945  	lastIdx := len(cfg.Reporting) - 1
   946  	return bugIdx == lastIdx-1 && dupIdx == lastIdx &&
   947  		bugConfig.AccessLevel == dupConfig.AccessLevel &&
   948  		bugConfig.Config.Type() == dupConfig.Config.Type()
   949  }
   950  
   951  func getReportingIdx(c context.Context, bug *Bug, bugReporting *BugReporting) int {
   952  	for i := range bug.Reporting {
   953  		if bug.Reporting[i].Name == bugReporting.Name {
   954  			return i
   955  		}
   956  	}
   957  	log.Errorf(c, "failed to find bug reporting by name: %q/%q", bug.Title, bugReporting.Name)
   958  	return -1
   959  }
   960  
   961  func incomingCommandTx(c context.Context, now time.Time, cmd *dashapi.BugUpdate, bugKey, dupKey *db.Key) (
   962  	bool, string, error) {
   963  	bug := new(Bug)
   964  	if err := db.Get(c, bugKey, bug); err != nil {
   965  		return false, internalError, fmt.Errorf("can't find the corresponding bug: %w", err)
   966  	}
   967  	var dup *Bug
   968  	if cmd.Status == dashapi.BugStatusDup {
   969  		dup1, ok, reason, err := checkDupBug(c, cmd, bug, bugKey, dupKey)
   970  		if !ok || err != nil {
   971  			return ok, reason, err
   972  		}
   973  		dup = dup1
   974  	}
   975  	state, err := loadReportingState(c)
   976  	if err != nil {
   977  		return false, internalError, err
   978  	}
   979  	ok, reason, err := incomingCommandUpdate(c, now, cmd, bugKey, bug, dup, state)
   980  	if !ok || err != nil {
   981  		return ok, reason, err
   982  	}
   983  	if _, err := db.Put(c, bugKey, bug); err != nil {
   984  		return false, internalError, fmt.Errorf("failed to put bug: %w", err)
   985  	}
   986  	if err := saveReportingState(c, state); err != nil {
   987  		return false, internalError, err
   988  	}
   989  	return true, "", nil
   990  }
   991  
   992  func incomingCommandUpdate(c context.Context, now time.Time, cmd *dashapi.BugUpdate, bugKey *db.Key,
   993  	bug, dup *Bug, state *ReportingState) (bool, string, error) {
   994  	bugReporting, final := bugReportingByID(bug, cmd.ID)
   995  	if bugReporting == nil {
   996  		return false, internalError, fmt.Errorf("can't find bug reporting")
   997  	}
   998  	if ok, reply, err := checkBugStatus(c, cmd, bug, bugReporting); !ok {
   999  		return false, reply, err
  1000  	}
  1001  	stateEnt := state.getEntry(now, bug.Namespace, bugReporting.Name)
  1002  	if ok, reply, err := incomingCommandCmd(c, now, cmd, bug, dup, bugReporting, final, stateEnt); !ok {
  1003  		return false, reply, err
  1004  	}
  1005  	if (len(cmd.FixCommits) != 0 || cmd.ResetFixCommits) &&
  1006  		(bug.Status == BugStatusOpen || bug.Status == BugStatusDup) {
  1007  		sort.Strings(cmd.FixCommits)
  1008  		if !reflect.DeepEqual(bug.Commits, cmd.FixCommits) {
  1009  			bug.updateCommits(cmd.FixCommits, now)
  1010  		}
  1011  	}
  1012  	toReport := append([]int64{}, cmd.ReportCrashIDs...)
  1013  	if cmd.CrashID != 0 {
  1014  		bugReporting.CrashID = cmd.CrashID
  1015  		toReport = append(toReport, cmd.CrashID)
  1016  	}
  1017  	newRef := CrashReference{CrashReferenceReporting, bugReporting.Name, now}
  1018  	for _, crashID := range toReport {
  1019  		err := addCrashReference(c, crashID, bugKey, newRef)
  1020  		if err != nil {
  1021  			return false, internalError, err
  1022  		}
  1023  	}
  1024  	for _, crashID := range cmd.UnreportCrashIDs {
  1025  		err := removeCrashReference(c, crashID, bugKey, CrashReferenceReporting, bugReporting.Name)
  1026  		if err != nil {
  1027  			return false, internalError, err
  1028  		}
  1029  	}
  1030  	if bugReporting.ExtID == "" {
  1031  		bugReporting.ExtID = cmd.ExtID
  1032  	}
  1033  	if bugReporting.Link == "" {
  1034  		bugReporting.Link = cmd.Link
  1035  	}
  1036  	if len(cmd.CC) != 0 && cmd.Status != dashapi.BugStatusUnCC {
  1037  		merged := email.MergeEmailLists(strings.Split(bugReporting.CC, "|"), cmd.CC)
  1038  		bugReporting.CC = strings.Join(merged, "|")
  1039  	}
  1040  	bugReporting.ReproLevel = max(bugReporting.ReproLevel, cmd.ReproLevel)
  1041  	if bug.Status != BugStatusDup {
  1042  		bug.DupOf = ""
  1043  	}
  1044  	if cmd.Status != dashapi.BugStatusOpen || !cmd.OnHold {
  1045  		bugReporting.OnHold = time.Time{}
  1046  	}
  1047  	if cmd.Status != dashapi.BugStatusUpdate || cmd.ExtID != "" {
  1048  		// Update LastActivity only on important events.
  1049  		// Otherwise it impedes bug obsoletion.
  1050  		bug.LastActivity = now
  1051  	}
  1052  	return true, "", nil
  1053  }
  1054  
  1055  // nolint:revive
  1056  func incomingCommandCmd(c context.Context, now time.Time, cmd *dashapi.BugUpdate, bug, dup *Bug,
  1057  	bugReporting *BugReporting, final bool, stateEnt *ReportingStateEntry) (bool, string, error) {
  1058  	switch cmd.Status {
  1059  	case dashapi.BugStatusOpen:
  1060  		bug.Status = BugStatusOpen
  1061  		bug.Closed = time.Time{}
  1062  		stateEnt.Sent++
  1063  		if bugReporting.Reported.IsZero() {
  1064  			bugReporting.Reported = now
  1065  		}
  1066  		if bugReporting.OnHold.IsZero() && cmd.OnHold {
  1067  			bugReporting.OnHold = now
  1068  		}
  1069  		// Close all previous reporting if they are not closed yet
  1070  		// (can happen due to Status == ReportingDisabled).
  1071  		for i := range bug.Reporting {
  1072  			if bugReporting == &bug.Reporting[i] {
  1073  				break
  1074  			}
  1075  			if bug.Reporting[i].Closed.IsZero() {
  1076  				bug.Reporting[i].Closed = now
  1077  			}
  1078  		}
  1079  		if bug.ReproLevel < cmd.ReproLevel {
  1080  			return false, internalError,
  1081  				fmt.Errorf("bug update with invalid repro level: %v/%v",
  1082  					bug.ReproLevel, cmd.ReproLevel)
  1083  		}
  1084  	case dashapi.BugStatusUpstream:
  1085  		if final {
  1086  			return false, "Can't upstream, this is final destination.", nil
  1087  		}
  1088  		if len(bug.Commits) != 0 {
  1089  			// We could handle this case, but how/when it will occur
  1090  			// in real life is unclear now.
  1091  			return false, "Can't upstream this bug, the bug has fixing commits.", nil
  1092  		}
  1093  		bug.Status = BugStatusOpen
  1094  		bug.Closed = time.Time{}
  1095  		bugReporting.Closed = now
  1096  		bugReporting.Auto = cmd.Notification
  1097  	case dashapi.BugStatusInvalid:
  1098  		bug.Closed = now
  1099  		bug.Status = BugStatusInvalid
  1100  		bugReporting.Closed = now
  1101  		bugReporting.Auto = cmd.Notification
  1102  	case dashapi.BugStatusDup:
  1103  		bug.Status = BugStatusDup
  1104  		bug.Closed = now
  1105  		bug.DupOf = dup.keyHash(c)
  1106  	case dashapi.BugStatusUpdate:
  1107  		// Just update Link, Commits, etc below.
  1108  	case dashapi.BugStatusUnCC:
  1109  		bug.UNCC = email.MergeEmailLists(bug.UNCC, cmd.CC)
  1110  	default:
  1111  		return false, internalError, fmt.Errorf("unknown bug status %v", cmd.Status)
  1112  	}
  1113  	if cmd.StatusReason != "" {
  1114  		bug.StatusReason = cmd.StatusReason
  1115  	}
  1116  	for _, label := range cmd.Labels {
  1117  		bugReporting.AddLabel(label)
  1118  	}
  1119  	return true, "", nil
  1120  }
  1121  
  1122  func checkBugStatus(c context.Context, cmd *dashapi.BugUpdate, bug *Bug, bugReporting *BugReporting) (
  1123  	bool, string, error) {
  1124  	switch bug.Status {
  1125  	case BugStatusOpen:
  1126  	case BugStatusDup:
  1127  		canon, err := canonicalBug(c, bug)
  1128  		if err != nil {
  1129  			return false, internalError, err
  1130  		}
  1131  		if canon.Status != BugStatusOpen {
  1132  			// We used to reject updates to closed bugs,
  1133  			// but this is confusing and non-actionable for users.
  1134  			// So now we fail the update, but give empty reason,
  1135  			// which means "don't notify user".
  1136  			if cmd.Status == dashapi.BugStatusUpdate {
  1137  				// This happens when people discuss old bugs.
  1138  				log.Infof(c, "Dup bug is already closed")
  1139  			} else {
  1140  				log.Warningf(c, "incoming command %v: dup bug is already closed", cmd.ID)
  1141  			}
  1142  			return false, "", nil
  1143  		}
  1144  	case BugStatusFixed, BugStatusInvalid:
  1145  		if cmd.Status != dashapi.BugStatusUpdate {
  1146  			log.Errorf(c, "incoming command %v: bug is already closed", cmd.ID)
  1147  		}
  1148  		return false, "", nil
  1149  	default:
  1150  		return false, internalError, fmt.Errorf("unknown bug status %v", bug.Status)
  1151  	}
  1152  	if !bugReporting.Closed.IsZero() {
  1153  		if cmd.Status != dashapi.BugStatusUpdate {
  1154  			log.Errorf(c, "incoming command %v: bug reporting is already closed", cmd.ID)
  1155  		}
  1156  		return false, "", nil
  1157  	}
  1158  	return true, "", nil
  1159  }
  1160  
  1161  func findBugByReportingID(c context.Context, id string) (*Bug, *db.Key, error) {
  1162  	var bugs []*Bug
  1163  	keys, err := db.NewQuery("Bug").
  1164  		Filter("Reporting.ID=", id).
  1165  		Limit(2).
  1166  		GetAll(c, &bugs)
  1167  	if err != nil {
  1168  		return nil, nil, fmt.Errorf("failed to fetch bugs: %w", err)
  1169  	}
  1170  	if len(bugs) == 0 {
  1171  		return nil, nil, fmt.Errorf("failed to find bug by reporting id %q", id)
  1172  	}
  1173  	if len(bugs) > 1 {
  1174  		return nil, nil, fmt.Errorf("multiple bugs for reporting id %q", id)
  1175  	}
  1176  	return bugs[0], keys[0], nil
  1177  }
  1178  
  1179  func findDupByTitle(c context.Context, ns, title string) (*Bug, *db.Key, error) {
  1180  	title, seq, err := splitDisplayTitle(title)
  1181  	if err != nil {
  1182  		return nil, nil, err
  1183  	}
  1184  	bugHash := bugKeyHash(c, ns, title, seq)
  1185  	bugKey := db.NewKey(c, "Bug", bugHash, 0, nil)
  1186  	bug := new(Bug)
  1187  	if err := db.Get(c, bugKey, bug); err != nil {
  1188  		if err == db.ErrNoSuchEntity {
  1189  			return nil, nil, nil // This is not really an error, we should notify the user instead.
  1190  		}
  1191  		return nil, nil, fmt.Errorf("failed to get dup: %w", err)
  1192  	}
  1193  	return bug, bugKey, nil
  1194  }
  1195  
  1196  func bugReportingByID(bug *Bug, id string) (*BugReporting, bool) {
  1197  	for i := range bug.Reporting {
  1198  		if bug.Reporting[i].ID == id {
  1199  			return &bug.Reporting[i], i == len(bug.Reporting)-1
  1200  		}
  1201  	}
  1202  	return nil, false
  1203  }
  1204  
  1205  func bugReportingByName(bug *Bug, name string) *BugReporting {
  1206  	for i := range bug.Reporting {
  1207  		if bug.Reporting[i].Name == name {
  1208  			return &bug.Reporting[i]
  1209  		}
  1210  	}
  1211  	return nil
  1212  }
  1213  
  1214  func lastReportedReporting(bug *Bug) *BugReporting {
  1215  	for i := len(bug.Reporting) - 1; i >= 0; i-- {
  1216  		if !bug.Reporting[i].Reported.IsZero() {
  1217  			return &bug.Reporting[i]
  1218  		}
  1219  	}
  1220  	return nil
  1221  }
  1222  
  1223  // The updateReporting method is supposed to be called both to fully initialize a new
  1224  // Bug object and also to adjust it to the updated namespace configuration.
  1225  func (bug *Bug) updateReportings(c context.Context, cfg *Config, now time.Time) error {
  1226  	oldReportings := map[string]BugReporting{}
  1227  	oldPositions := map[string]int{}
  1228  	for i, rep := range bug.Reporting {
  1229  		oldReportings[rep.Name] = rep
  1230  		oldPositions[rep.Name] = i
  1231  	}
  1232  	maxPos := 0
  1233  	bug.Reporting = nil
  1234  	for _, rep := range cfg.Reporting {
  1235  		if oldRep, ok := oldReportings[rep.Name]; ok {
  1236  			oldPos := oldPositions[rep.Name]
  1237  			if oldPos < maxPos {
  1238  				// At the moment we only support insertions and deletions of reportings.
  1239  				// TODO: figure out what exactly can go wrong if we also allow reordering.
  1240  				return fmt.Errorf("the order of reportings is changed, before: %v", oldPositions)
  1241  			}
  1242  			maxPos = oldPos
  1243  			bug.Reporting = append(bug.Reporting, oldRep)
  1244  		} else {
  1245  			bug.Reporting = append(bug.Reporting, BugReporting{
  1246  				Name: rep.Name,
  1247  				ID:   bugReportingHash(bug.keyHash(c), rep.Name),
  1248  			})
  1249  		}
  1250  	}
  1251  	// We might have added new BugReporting objects between/before the ones that were
  1252  	// already reported. To let syzbot continue from the same reporting stage where it
  1253  	// stopped, close such outliers.
  1254  	seenProcessed := false
  1255  	minTime := now
  1256  	for i := len(bug.Reporting) - 1; i >= 0; i-- {
  1257  		rep := &bug.Reporting[i]
  1258  		if !rep.Reported.IsZero() || !rep.Closed.IsZero() || !rep.OnHold.IsZero() {
  1259  			if !rep.Reported.IsZero() && rep.Reported.Before(minTime) {
  1260  				minTime = rep.Reported
  1261  			}
  1262  			seenProcessed = true
  1263  		} else if seenProcessed {
  1264  			rep.Dummy = true
  1265  		}
  1266  	}
  1267  	// Yet another stage -- we set Closed and Reported to dummy objects in non-decreasing order.
  1268  	currTime := minTime
  1269  	for i := range bug.Reporting {
  1270  		rep := &bug.Reporting[i]
  1271  		if rep.Dummy {
  1272  			rep.Closed = currTime
  1273  			rep.Reported = currTime
  1274  		} else if rep.Reported.After(currTime) {
  1275  			currTime = rep.Reported
  1276  		}
  1277  	}
  1278  	return nil
  1279  }
  1280  
  1281  func findCrashForBug(c context.Context, bug *Bug) (*Crash, *db.Key, error) {
  1282  	bugKey := bug.key(c)
  1283  	crashes, keys, err := queryCrashesForBug(c, bugKey, 1)
  1284  	if err != nil {
  1285  		return nil, nil, err
  1286  	}
  1287  	if len(crashes) < 1 {
  1288  		return nil, nil, fmt.Errorf("no crashes")
  1289  	}
  1290  	crash, key := crashes[0], keys[0]
  1291  	switch bug.HeadReproLevel {
  1292  	case ReproLevelC:
  1293  		if crash.ReproC == 0 {
  1294  			log.Errorf(c, "bug '%v': has C repro, but crash without C repro", bug.Title)
  1295  		}
  1296  	case ReproLevelSyz:
  1297  		if crash.ReproSyz == 0 {
  1298  			log.Errorf(c, "bug '%v': has syz repro, but crash without syz repro", bug.Title)
  1299  		}
  1300  	}
  1301  	return crash, key, nil
  1302  }
  1303  
  1304  func loadReportingState(c context.Context) (*ReportingState, error) {
  1305  	state := new(ReportingState)
  1306  	key := db.NewKey(c, "ReportingState", "", 1, nil)
  1307  	if err := db.Get(c, key, state); err != nil && err != db.ErrNoSuchEntity {
  1308  		return nil, fmt.Errorf("failed to get reporting state: %w", err)
  1309  	}
  1310  	return state, nil
  1311  }
  1312  
  1313  func saveReportingState(c context.Context, state *ReportingState) error {
  1314  	key := db.NewKey(c, "ReportingState", "", 1, nil)
  1315  	if _, err := db.Put(c, key, state); err != nil {
  1316  		return fmt.Errorf("failed to put reporting state: %w", err)
  1317  	}
  1318  	return nil
  1319  }
  1320  
  1321  func (state *ReportingState) getEntry(now time.Time, namespace, name string) *ReportingStateEntry {
  1322  	if namespace == "" || name == "" {
  1323  		panic(fmt.Sprintf("requesting reporting state for %v/%v", namespace, name))
  1324  	}
  1325  	// Convert time to date of the form 20170125.
  1326  	date := timeDate(now)
  1327  	for i := range state.Entries {
  1328  		ent := &state.Entries[i]
  1329  		if ent.Namespace == namespace && ent.Name == name {
  1330  			if ent.Date != date {
  1331  				ent.Date = date
  1332  				ent.Sent = 0
  1333  			}
  1334  			return ent
  1335  		}
  1336  	}
  1337  	state.Entries = append(state.Entries, ReportingStateEntry{
  1338  		Namespace: namespace,
  1339  		Name:      name,
  1340  		Date:      date,
  1341  		Sent:      0,
  1342  	})
  1343  	return &state.Entries[len(state.Entries)-1]
  1344  }
  1345  
  1346  func loadFullBugInfo(c context.Context, bug *Bug, bugKey *db.Key,
  1347  	bugReporting *BugReporting) (*dashapi.FullBugInfo, error) {
  1348  	reporting := getNsConfig(c, bug.Namespace).ReportingByName(bugReporting.Name)
  1349  	if reporting == nil {
  1350  		return nil, fmt.Errorf("failed to find the reporting object")
  1351  	}
  1352  	ret := &dashapi.FullBugInfo{}
  1353  	// Query bisections.
  1354  	var err error
  1355  	if bug.BisectCause > BisectPending {
  1356  		ret.BisectCause, err = prepareBisectionReport(c, bug, JobBisectCause, reporting)
  1357  		if err != nil {
  1358  			return nil, err
  1359  		}
  1360  	}
  1361  	if bug.BisectFix > BisectPending {
  1362  		ret.BisectFix, err = prepareBisectionReport(c, bug, JobBisectFix, reporting)
  1363  		if err != nil {
  1364  			return nil, err
  1365  		}
  1366  	}
  1367  	// Query the fix candidate.
  1368  	if bug.FixCandidateJob != "" {
  1369  		ret.FixCandidate, err = prepareFixCandidateReport(c, bug, reporting)
  1370  		if err != nil {
  1371  			return nil, err
  1372  		}
  1373  	}
  1374  	// Query similar bugs.
  1375  	similar, err := loadSimilarBugs(c, bug)
  1376  	if err != nil {
  1377  		return nil, err
  1378  	}
  1379  	for _, similarBug := range similar {
  1380  		_, bugReporting, _, _, _ := currentReporting(c, similarBug)
  1381  		if bugReporting == nil {
  1382  			continue
  1383  		}
  1384  		status, err := similarBug.dashapiStatus()
  1385  		if err != nil {
  1386  			return nil, err
  1387  		}
  1388  		ret.SimilarBugs = append(ret.SimilarBugs, &dashapi.SimilarBugInfo{
  1389  			Title:      similarBug.displayTitle(),
  1390  			Status:     status,
  1391  			Namespace:  similarBug.Namespace,
  1392  			ReproLevel: similarBug.ReproLevel,
  1393  			Link:       fmt.Sprintf("%v/bug?extid=%v", appURL(c), bugReporting.ID),
  1394  			ReportLink: bugReporting.Link,
  1395  			Closed:     similarBug.Closed,
  1396  		})
  1397  	}
  1398  	// Query crashes.
  1399  	crashes, err := representativeCrashes(c, bugKey)
  1400  	if err != nil {
  1401  		return nil, err
  1402  	}
  1403  	for _, crash := range crashes {
  1404  		rep, err := crashBugReport(c, bug, crash.crash, crash.key, bugReporting, reporting)
  1405  		if err != nil {
  1406  			return nil, fmt.Errorf("crash %d: %w", crash.key.IntID(), err)
  1407  		}
  1408  		ret.Crashes = append(ret.Crashes, rep)
  1409  	}
  1410  	// Query tree testing jobs.
  1411  	ret.TreeJobs, err = treeTestJobs(c, bug)
  1412  	if err != nil {
  1413  		return nil, err
  1414  	}
  1415  	return ret, nil
  1416  }
  1417  
  1418  func prepareFixCandidateReport(c context.Context, bug *Bug, reporting *Reporting) (*dashapi.BugReport, error) {
  1419  	job, jobKey, err := fetchJob(c, bug.FixCandidateJob)
  1420  	if err != nil {
  1421  		return nil, err
  1422  	}
  1423  	ret, err := createBugReportForJob(c, job, jobKey, reporting.Config)
  1424  	if err != nil {
  1425  		return nil, fmt.Errorf("failed to create the job bug report: %w", err)
  1426  	}
  1427  	return ret, nil
  1428  }
  1429  
  1430  func prepareBisectionReport(c context.Context, bug *Bug, jobType JobType,
  1431  	reporting *Reporting) (*dashapi.BugReport, error) {
  1432  	bisectionJob, err := queryBestBisection(c, bug, jobType)
  1433  	if bisectionJob == nil || err != nil {
  1434  		return nil, err
  1435  	}
  1436  	job, jobKey := bisectionJob.job, bisectionJob.key
  1437  	if job.Reporting != "" {
  1438  		ret, err := createBugReportForJob(c, job, jobKey, reporting.Config)
  1439  		if err != nil {
  1440  			return nil, fmt.Errorf("failed to create the job bug report: %w", err)
  1441  		}
  1442  		return ret, nil
  1443  	}
  1444  	return nil, nil
  1445  }
  1446  
  1447  type crashWithKey struct {
  1448  	crash *Crash
  1449  	key   *db.Key
  1450  }
  1451  
  1452  func representativeCrashes(c context.Context, bugKey *db.Key) ([]*crashWithKey, error) {
  1453  	// There should generally be no need to query lots of crashes.
  1454  	const fetchCrashes = 50
  1455  	allCrashes, allCrashKeys, err := queryCrashesForBug(c, bugKey, fetchCrashes)
  1456  	if err != nil {
  1457  		return nil, err
  1458  	}
  1459  	// Let's consider a crash fresh if it happened within the last week.
  1460  	const recentDuration = time.Hour * 24 * 7
  1461  	now := timeNow(c)
  1462  
  1463  	var bestCrash, recentCrash, straceCrash *crashWithKey
  1464  	for id, crash := range allCrashes {
  1465  		key := allCrashKeys[id]
  1466  		if bestCrash == nil {
  1467  			bestCrash = &crashWithKey{crash, key}
  1468  		}
  1469  		if now.Sub(crash.Time) < recentDuration && recentCrash == nil {
  1470  			recentCrash = &crashWithKey{crash, key}
  1471  		}
  1472  		if dashapi.CrashFlags(crash.Flags)&dashapi.CrashUnderStrace > 0 &&
  1473  			straceCrash == nil {
  1474  			straceCrash = &crashWithKey{crash, key}
  1475  		}
  1476  	}
  1477  	// It's possible that there are so many crashes with reproducers that
  1478  	// we do not get to see the recent crashes without them.
  1479  	// Give it one more try.
  1480  	if recentCrash == nil {
  1481  		var crashes []*Crash
  1482  		keys, err := db.NewQuery("Crash").
  1483  			Ancestor(bugKey).
  1484  			Order("-Time").
  1485  			Limit(1).
  1486  			GetAll(c, &crashes)
  1487  		if err != nil {
  1488  			return nil, fmt.Errorf("failed to fetch the latest crash: %w", err)
  1489  		}
  1490  		if len(crashes) > 0 {
  1491  			recentCrash = &crashWithKey{crashes[0], keys[0]}
  1492  		}
  1493  	}
  1494  	dedup := map[int64]bool{}
  1495  	crashes := []*crashWithKey{}
  1496  	for _, item := range []*crashWithKey{recentCrash, bestCrash, straceCrash} {
  1497  		if item == nil || dedup[item.key.IntID()] {
  1498  			continue
  1499  		}
  1500  		crashes = append(crashes, item)
  1501  		dedup[item.key.IntID()] = true
  1502  	}
  1503  	// Sort by Time in desc order.
  1504  	sort.Slice(crashes, func(i, j int) bool {
  1505  		return crashes[i].crash.Time.After(crashes[j].crash.Time)
  1506  	})
  1507  	return crashes, nil
  1508  }
  1509  
  1510  // bugReportSorter sorts bugs by priority we want to report them.
  1511  // E.g. we want to report bugs with reproducers before bugs without reproducers.
  1512  type bugReportSorter []*Bug
  1513  
  1514  func (a bugReportSorter) Len() int      { return len(a) }
  1515  func (a bugReportSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
  1516  func (a bugReportSorter) Less(i, j int) bool {
  1517  	if a[i].ReproLevel != a[j].ReproLevel {
  1518  		return a[i].ReproLevel > a[j].ReproLevel
  1519  	}
  1520  	if a[i].HasReport != a[j].HasReport {
  1521  		return a[i].HasReport
  1522  	}
  1523  	if a[i].NumCrashes != a[j].NumCrashes {
  1524  		return a[i].NumCrashes > a[j].NumCrashes
  1525  	}
  1526  	return a[i].FirstTime.Before(a[j].FirstTime)
  1527  }
  1528  
  1529  // kernelArch returns arch as kernel developers know it (rather than Go names).
  1530  // Currently Linux-specific.
  1531  func kernelArch(arch string) string {
  1532  	switch arch {
  1533  	case targets.I386:
  1534  		return "i386"
  1535  	default:
  1536  		return arch
  1537  	}
  1538  }