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