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 }