github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/dashboard/app/reporting_lists.go (about) 1 // Copyright 2023 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 "context" 8 "encoding/json" 9 "fmt" 10 "net/http" 11 "sort" 12 "strings" 13 "time" 14 15 "github.com/google/syzkaller/dashboard/dashapi" 16 "github.com/google/syzkaller/pkg/hash" 17 "google.golang.org/appengine/v2" 18 db "google.golang.org/appengine/v2/datastore" 19 "google.golang.org/appengine/v2/log" 20 ) 21 22 // reportingPollBugLists is called by backends to get bug lists that need to be reported. 23 func reportingPollBugLists(c context.Context, typ string) []*dashapi.BugListReport { 24 state, err := loadReportingState(c) 25 if err != nil { 26 log.Errorf(c, "%v", err) 27 return nil 28 } 29 registry, err := makeSubsystemReportRegistry(c) 30 if err != nil { 31 log.Errorf(c, "%v", err) 32 return nil 33 } 34 ret := []*dashapi.BugListReport{} 35 for ns, nsConfig := range getConfig(c).Namespaces { 36 rConfig := nsConfig.Subsystems.Reminder 37 if rConfig == nil { 38 continue 39 } 40 reporting := nsConfig.ReportingByName(rConfig.SourceReporting) 41 stateEntry := state.getEntry(timeNow(c), ns, reporting.Name) 42 // The DB might well contain info about stale entities, but by querying the latest 43 // list of subsystems from the configuration, we make sure we only consider what's 44 // currently relevant. 45 rawSubsystems := nsConfig.Subsystems.Service.List() 46 // Sort to keep output stable. 47 sort.Slice(rawSubsystems, func(i, j int) bool { 48 return rawSubsystems[i].Name < rawSubsystems[j].Name 49 }) 50 for _, entry := range rawSubsystems { 51 if entry.NoReminders { 52 continue 53 } 54 for _, dbReport := range registry.get(ns, entry.Name) { 55 if stateEntry.Sent >= reporting.DailyLimit { 56 break 57 } 58 report, err := reportingBugListReport(c, dbReport, ns, entry.Name, typ) 59 if err != nil { 60 log.Errorf(c, "%v", err) 61 return nil 62 } 63 if report != nil { 64 ret = append(ret, report) 65 stateEntry.Sent++ 66 } 67 } 68 } 69 } 70 return ret 71 } 72 73 const maxNewListsPerNs = 5 74 75 // handleSubsystemReports is periodically invoked to construct fresh SubsystemReport objects. 76 func handleSubsystemReports(w http.ResponseWriter, r *http.Request) { 77 c := appengine.NewContext(r) 78 registry, err := makeSubsystemRegistry(c) 79 if err != nil { 80 log.Errorf(c, "failed to load subsystems: %v", err) 81 return 82 } 83 for ns, nsConfig := range getConfig(c).Namespaces { 84 rConfig := nsConfig.Subsystems.Reminder 85 if rConfig == nil { 86 continue 87 } 88 minPeriod := 24 * time.Hour * time.Duration(rConfig.PeriodDays) 89 reporting := nsConfig.ReportingByName(rConfig.SourceReporting) 90 var subsystems []*Subsystem 91 for _, entry := range nsConfig.Subsystems.Service.List() { 92 if entry.NoReminders { 93 continue 94 } 95 subsystems = append(subsystems, registry.get(ns, entry.Name)) 96 } 97 // Poll subsystems in a round-robin manner. 98 sort.Slice(subsystems, func(i, j int) bool { 99 return subsystems[i].ListsQueried.Before(subsystems[j].ListsQueried) 100 }) 101 updateLimit := maxNewListsPerNs 102 for _, subsystem := range subsystems { 103 if updateLimit == 0 { 104 break 105 } 106 if timeNow(c).Before(subsystem.LastBugList.Add(minPeriod)) { 107 continue 108 } 109 report, err := querySubsystemReport(c, subsystem, reporting, rConfig) 110 if err != nil { 111 log.Errorf(c, "failed to query bug lists: %v", err) 112 return 113 } 114 if err := registry.updatePoll(c, subsystem, report != nil); err != nil { 115 log.Errorf(c, "failed to update subsystem: %v", err) 116 return 117 } 118 if report == nil { 119 continue 120 } 121 updateLimit-- 122 if err := storeSubsystemReport(c, subsystem, report); err != nil { 123 log.Errorf(c, "failed to save subsystem: %v", err) 124 return 125 } 126 } 127 } 128 } 129 130 func reportingBugListCommand(c context.Context, cmd *dashapi.BugListUpdate) (string, error) { 131 // We have to execute it outside of the transacation, otherwise we get the 132 // "Only ancestor queries are allowed inside transactions." error. 133 subsystem, rawReport, _, err := findSubsystemReportByID(c, cmd.ID) 134 if err != nil { 135 return "", err 136 } 137 if subsystem == nil { 138 return "", fmt.Errorf("the bug list was not found") 139 } 140 reply := "" 141 tx := func(c context.Context) error { 142 subsystemKey := subsystemKey(c, subsystem) 143 reportKey := subsystemReportKey(c, subsystemKey, rawReport) 144 report := new(SubsystemReport) 145 if err := db.Get(c, reportKey, report); err != nil { 146 return fmt.Errorf("failed to query SubsystemReport (%v): %w", reportKey, err) 147 } 148 stage := report.findStage(cmd.ID) 149 if stage.ExtID == "" { 150 stage.ExtID = cmd.ExtID 151 } 152 if stage.Link == "" { 153 stage.Link = cmd.Link 154 } 155 // It might e.g. happen that we skipped a stage in reportingBugListReport. 156 // Make sure all skipped stages have non-nil Closed. 157 for i := range report.Stages { 158 item := &report.Stages[i] 159 if cmd.Command != dashapi.BugListRegenerateCmd && item == stage { 160 break 161 } 162 item.Closed = timeNow(c) 163 } 164 switch cmd.Command { 165 case dashapi.BugListSentCmd: 166 if !stage.Reported.IsZero() { 167 return fmt.Errorf("the reporting stage was already reported") 168 } 169 stage.Reported = timeNow(c) 170 171 state, err := loadReportingState(c) 172 if err != nil { 173 return fmt.Errorf("failed to query state: %w", err) 174 } 175 stateEnt := state.getEntry(timeNow(c), subsystem.Namespace, 176 getConfig(c).Namespaces[subsystem.Namespace].Subsystems.Reminder.SourceReporting) 177 stateEnt.Sent++ 178 if err := saveReportingState(c, state); err != nil { 179 return fmt.Errorf("failed to save state: %w", err) 180 } 181 case dashapi.BugListUpstreamCmd: 182 if !stage.Moderation { 183 reply = `The report cannot be sent further upstream. 184 It's already at the last reporting stage.` 185 return nil 186 } 187 if !stage.Closed.IsZero() { 188 reply = `The bug list was already upstreamed. 189 Please visit the new discussion thread.` 190 return nil 191 } 192 stage.Closed = timeNow(c) 193 case dashapi.BugListRegenerateCmd: 194 dbSubsystem := new(Subsystem) 195 err := db.Get(c, subsystemKey, dbSubsystem) 196 if err != nil { 197 return fmt.Errorf("failed to get subsystem: %w", err) 198 } 199 dbSubsystem.LastBugList = time.Time{} 200 _, err = db.Put(c, subsystemKey, dbSubsystem) 201 if err != nil { 202 return fmt.Errorf("failed to save subsystem: %w", err) 203 } 204 } 205 _, err = db.Put(c, reportKey, report) 206 if err != nil { 207 return fmt.Errorf("failed to save the object: %w", err) 208 } 209 return nil 210 } 211 return reply, runInTransaction(c, tx, &db.TransactionOptions{XG: true}) 212 } 213 214 func findSubsystemReportByID(c context.Context, ID string) (*Subsystem, 215 *SubsystemReport, *SubsystemReportStage, error) { 216 var subsystemReports []*SubsystemReport 217 reportKeys, err := db.NewQuery("SubsystemReport"). 218 Filter("Stages.ID=", ID). 219 Limit(1). 220 GetAll(c, &subsystemReports) 221 if err != nil { 222 return nil, nil, nil, fmt.Errorf("failed to query subsystem reports: %w", err) 223 } 224 if len(subsystemReports) == 0 { 225 return nil, nil, nil, nil 226 } 227 stage := subsystemReports[0].findStage(ID) 228 if stage == nil { 229 // This should never happen (provided that all the code is correct). 230 return nil, nil, nil, fmt.Errorf("bug list is found, but the stage is missing") 231 } 232 subsystem := new(Subsystem) 233 if err := db.Get(c, reportKeys[0].Parent(), subsystem); err != nil { 234 return nil, nil, nil, fmt.Errorf("failed to query subsystem: %w", err) 235 } 236 return subsystem, subsystemReports[0], stage, nil 237 } 238 239 // querySubsystemReport queries the open bugs and constructs a new SubsystemReport object. 240 func querySubsystemReport(c context.Context, subsystem *Subsystem, reporting *Reporting, 241 config *BugListReportingConfig) (*SubsystemReport, error) { 242 rawOpenBugs, fixedBugs, err := queryMatchingBugs(c, subsystem.Namespace, 243 subsystem.Name, reporting) 244 if err != nil { 245 return nil, err 246 } 247 withRepro, noRepro := []*Bug{}, []*Bug{} 248 for _, bug := range rawOpenBugs { 249 const possiblyFixedTimespan = 24 * time.Hour * 14 250 if bug.LastTime.Before(timeNow(c).Add(-possiblyFixedTimespan)) { 251 // The bug didn't happen recently, possibly it was already fixed. 252 // Let's not display such bugs in reminders. 253 continue 254 } 255 if bug.FirstTime.After(timeNow(c).Add(-config.MinBugAge)) { 256 // Don't take bugs which are too new -- they're still fresh in memory. 257 continue 258 } 259 if bug.prio() == LowPrioBug { 260 // Don't include low priority bugs in reports because the community 261 // actually perceives them as non-actionable. 262 continue 263 } 264 discussions := bug.discussionSummary() 265 if discussions.ExternalMessages > 0 && 266 discussions.LastMessage.After(timeNow(c).Add(-config.UserReplyFrist)) { 267 // Don't take bugs with recent user replies. 268 // As we don't keep exactly the date of the last user message, approximate it. 269 continue 270 } 271 if bug.HasLabel(NoRemindersLabel, "") { 272 // The bug was intentionally excluded from monthly reminders. 273 continue 274 } 275 if bug.ReproLevel == dashapi.ReproLevelNone { 276 noRepro = append(noRepro, bug) 277 } else { 278 withRepro = append(withRepro, bug) 279 } 280 } 281 // Let's reduce noise and don't remind about just one bug. 282 if len(noRepro)+len(withRepro) < config.MinBugsCount { 283 return nil, nil 284 } 285 // Even if we have enough bugs with a reproducer, there might still be bugs 286 // without a reproducer that have a lot of crashes. So let's take a small number 287 // of such bugs and give them a chance to be present in the final list. 288 takeNoRepro := 2 289 if takeNoRepro+len(withRepro) < config.BugsInReport { 290 takeNoRepro = config.BugsInReport - len(withRepro) 291 } 292 takeNoRepro = min(takeNoRepro, len(noRepro)) 293 sort.Slice(noRepro, func(i, j int) bool { 294 return noRepro[i].NumCrashes > noRepro[j].NumCrashes 295 }) 296 takeBugs := append(withRepro, noRepro[:takeNoRepro]...) 297 sort.Slice(takeBugs, func(i, j int) bool { 298 firstPrio, secondPrio := takeBugs[i].prio(), takeBugs[j].prio() 299 if firstPrio != secondPrio { 300 return !firstPrio.LessThan(secondPrio) 301 } 302 if takeBugs[i].NumCrashes != takeBugs[j].NumCrashes { 303 return takeBugs[i].NumCrashes > takeBugs[j].NumCrashes 304 } 305 return takeBugs[i].Title < takeBugs[j].Title 306 }) 307 keys := []*db.Key{} 308 for _, bug := range takeBugs { 309 keys = append(keys, bug.key(c)) 310 } 311 if len(keys) > config.BugsInReport { 312 keys = keys[:config.BugsInReport] 313 } 314 report := makeSubsystemReport(c, config, keys) 315 report.TotalStats = makeSubsystemReportStats(c, rawOpenBugs, fixedBugs, 0) 316 report.PeriodStats = makeSubsystemReportStats(c, rawOpenBugs, fixedBugs, config.PeriodDays) 317 return report, nil 318 } 319 320 func makeSubsystemReportStats(c context.Context, open, fixed []*Bug, days int) SubsystemReportStats { 321 after := timeNow(c).Add(-time.Hour * 24 * time.Duration(days)) 322 ret := SubsystemReportStats{} 323 for _, bug := range open { 324 if days > 0 && bug.FirstTime.Before(after) { 325 continue 326 } 327 if bug.prio() == LowPrioBug { 328 ret.LowPrio++ 329 } else { 330 ret.Reported++ 331 } 332 } 333 for _, bug := range fixed { 334 if len(bug.CommitInfo) == 0 { 335 continue 336 } 337 if days > 0 && bug.CommitInfo[0].Date.Before(after) { 338 continue 339 } 340 ret.Fixed++ 341 } 342 return ret 343 } 344 345 func queryMatchingBugs(c context.Context, ns, name string, reporting *Reporting) ([]*Bug, []*Bug, error) { 346 allOpenBugs, _, err := loadAllBugs(c, func(query *db.Query) *db.Query { 347 return query.Filter("Namespace=", ns). 348 Filter("Status=", BugStatusOpen). 349 Filter("Labels.Label=", SubsystemLabel). 350 Filter("Labels.Value=", name) 351 }) 352 if err != nil { 353 return nil, nil, fmt.Errorf("failed to query open bugs for subsystem: %w", err) 354 } 355 allFixedBugs, _, err := loadAllBugs(c, func(query *db.Query) *db.Query { 356 return query.Filter("Namespace=", ns). 357 Filter("Status=", BugStatusFixed). 358 Filter("Labels.Label=", SubsystemLabel). 359 Filter("Labels.Value=", name) 360 }) 361 if err != nil { 362 return nil, nil, fmt.Errorf("failed to query fixed bugs for subsystem: %w", err) 363 } 364 open, fixed := []*Bug{}, []*Bug{} 365 for _, bug := range append(allOpenBugs, allFixedBugs...) { 366 if len(bug.Commits) != 0 || bug.Status == BugStatusFixed { 367 // This bug is no longer really open. 368 fixed = append(fixed, bug) 369 continue 370 } 371 currReporting, _, _, _, err := currentReporting(c, bug) 372 if err != nil { 373 continue 374 } 375 if reporting.Name != currReporting.Name { 376 // The bug is not at the expected reporting stage. 377 continue 378 } 379 if currReporting.AccessLevel > reporting.AccessLevel { 380 continue 381 } 382 open = append(open, bug) 383 } 384 return open, fixed, nil 385 } 386 387 // makeSubsystemReport creates a new SubsystemReminder object. 388 func makeSubsystemReport(c context.Context, config *BugListReportingConfig, 389 keys []*db.Key) *SubsystemReport { 390 ret := &SubsystemReport{ 391 Created: timeNow(c), 392 } 393 for _, key := range keys { 394 ret.BugKeys = append(ret.BugKeys, key.Encode()) 395 } 396 baseID := hash.String([]byte(fmt.Sprintf("%v-%v", timeNow(c), ret.BugKeys))) 397 if config.ModerationConfig != nil { 398 ret.Stages = append(ret.Stages, SubsystemReportStage{ 399 ID: bugListReportingHash(baseID, "moderation"), 400 Moderation: true, 401 }) 402 } 403 ret.Stages = append(ret.Stages, SubsystemReportStage{ 404 ID: bugListReportingHash(baseID, "public"), 405 }) 406 return ret 407 } 408 409 const bugListHashPrefix = "list" 410 411 func bugListReportingHash(base, name string) string { 412 return bugListHashPrefix + bugReportingHash(base, name) 413 } 414 415 func isBugListHash(hash string) bool { 416 return strings.HasPrefix(hash, bugListHashPrefix) 417 } 418 419 func reportingBugListReport(c context.Context, subsystemReport *SubsystemReport, 420 ns, name, targetReportingType string) (*dashapi.BugListReport, error) { 421 for _, stage := range subsystemReport.Stages { 422 if !stage.Closed.IsZero() { 423 continue 424 } 425 repConfig := bugListReportingConfig(c, ns, &stage) 426 if repConfig == nil { 427 // It might happen if e.g. Moderation was set to nil. 428 // Just skip the stage then. 429 continue 430 } 431 if !stage.Reported.IsZero() || repConfig.Type() != targetReportingType { 432 break 433 } 434 configJSON, err := json.Marshal(repConfig) 435 if err != nil { 436 return nil, err 437 } 438 ret := &dashapi.BugListReport{ 439 ID: stage.ID, 440 Created: subsystemReport.Created, 441 Config: configJSON, 442 Link: fmt.Sprintf("%v/%s/s/%s", appURL(c), ns, name), 443 Subsystem: name, 444 Maintainers: subsystemMaintainers(c, ns, name), 445 Moderation: stage.Moderation, 446 TotalStats: subsystemReport.TotalStats.toDashapi(), 447 PeriodStats: subsystemReport.PeriodStats.toDashapi(), 448 PeriodDays: getNsConfig(c, ns).Subsystems.Reminder.PeriodDays, 449 } 450 bugKeys, err := subsystemReport.getBugKeys() 451 if err != nil { 452 return nil, fmt.Errorf("failed to get bug keys: %w", err) 453 } 454 bugs := make([]*Bug, len(bugKeys)) 455 err = db.GetMulti(c, bugKeys, bugs) 456 if err != nil { 457 return nil, fmt.Errorf("failed to get bugs: %w", err) 458 } 459 for _, bug := range bugs { 460 bugReporting := bugReportingByName(bug, 461 getNsConfig(c, ns).Subsystems.Reminder.SourceReporting) 462 ret.Bugs = append(ret.Bugs, dashapi.BugListItem{ 463 Title: bug.displayTitle(), 464 Link: fmt.Sprintf("%v/bug?extid=%v", appURL(c), bugReporting.ID), 465 ReproLevel: bug.ReproLevel, 466 Hits: bug.NumCrashes, 467 }) 468 } 469 return ret, nil 470 } 471 return nil, nil 472 } 473 474 func bugListReportingConfig(c context.Context, ns string, stage *SubsystemReportStage) ReportingType { 475 cfg := getNsConfig(c, ns).Subsystems.Reminder 476 if stage.Moderation { 477 return cfg.ModerationConfig 478 } 479 return cfg.Config 480 } 481 482 func makeSubsystem(ns, name string) *Subsystem { 483 return &Subsystem{ 484 Namespace: ns, 485 Name: name, 486 } 487 } 488 489 func subsystemKey(c context.Context, s *Subsystem) *db.Key { 490 return db.NewKey(c, "Subsystem", fmt.Sprintf("%v-%v", s.Namespace, s.Name), 0, nil) 491 } 492 493 func subsystemReportKey(c context.Context, subsystemKey *db.Key, r *SubsystemReport) *db.Key { 494 return db.NewKey(c, "SubsystemReport", r.Created.UTC().Format(time.RFC822), 0, subsystemKey) 495 } 496 497 type subsystemsRegistry struct { 498 entities map[string]map[string]*Subsystem 499 } 500 501 func makeSubsystemRegistry(c context.Context) (*subsystemsRegistry, error) { 502 var subsystems []*Subsystem 503 if _, err := db.NewQuery("Subsystem").GetAll(c, &subsystems); err != nil { 504 return nil, err 505 } 506 ret := &subsystemsRegistry{ 507 entities: map[string]map[string]*Subsystem{}, 508 } 509 for _, item := range subsystems { 510 ret.store(item) 511 } 512 return ret, nil 513 } 514 515 func (sr *subsystemsRegistry) get(ns, name string) *Subsystem { 516 ret := sr.entities[ns][name] 517 if ret == nil { 518 ret = makeSubsystem(ns, name) 519 } 520 return ret 521 } 522 523 func (sr *subsystemsRegistry) store(item *Subsystem) { 524 if sr.entities[item.Namespace] == nil { 525 sr.entities[item.Namespace] = map[string]*Subsystem{} 526 } 527 sr.entities[item.Namespace][item.Name] = item 528 } 529 530 func (sr *subsystemsRegistry) updatePoll(c context.Context, s *Subsystem, success bool) error { 531 key := subsystemKey(c, s) 532 return runInTransaction(c, func(c context.Context) error { 533 dbSubsystem := new(Subsystem) 534 err := db.Get(c, key, dbSubsystem) 535 if err == db.ErrNoSuchEntity { 536 dbSubsystem = s 537 } else if err != nil { 538 return fmt.Errorf("failed to get Subsystem '%v': %w", key, err) 539 } 540 dbSubsystem.ListsQueried = timeNow(c) 541 if success { 542 dbSubsystem.LastBugList = timeNow(c) 543 } 544 if _, err := db.Put(c, key, dbSubsystem); err != nil { 545 return fmt.Errorf("failed to save Subsystem: %w", err) 546 } 547 sr.store(dbSubsystem) 548 return nil 549 }, nil) 550 } 551 552 type subsystemReportRegistry struct { 553 entities map[string]map[string][]*SubsystemReport 554 } 555 556 func makeSubsystemReportRegistry(c context.Context) (*subsystemReportRegistry, error) { 557 var reports []*SubsystemReport 558 reportKeys, err := db.NewQuery("SubsystemReport").GetAll(c, &reports) 559 if err != nil { 560 return nil, err 561 } 562 ret := &subsystemReportRegistry{ 563 entities: map[string]map[string][]*SubsystemReport{}, 564 } 565 loader := &dependencyLoader[Subsystem]{} 566 for i, key := range reportKeys { 567 report := reports[i] 568 loader.add(key.Parent(), func(subsystem *Subsystem) { 569 ret.store(subsystem.Namespace, subsystem.Name, report) 570 }) 571 } 572 if err := loader.load(c); err != nil { 573 return nil, err 574 } 575 return ret, nil 576 } 577 578 func (srr *subsystemReportRegistry) get(ns, name string) []*SubsystemReport { 579 return srr.entities[ns][name] 580 } 581 582 func (srr *subsystemReportRegistry) store(ns, name string, item *SubsystemReport) { 583 if srr.entities[ns] == nil { 584 srr.entities[ns] = map[string][]*SubsystemReport{} 585 } 586 srr.entities[ns][name] = append(srr.entities[ns][name], item) 587 } 588 589 func storeSubsystemReport(c context.Context, s *Subsystem, report *SubsystemReport) error { 590 key := subsystemKey(c, s) 591 return runInTransaction(c, func(c context.Context) error { 592 // First close all previouly active per-subsystem reports. 593 var previous []*SubsystemReport 594 prevKeys, err := db.NewQuery("SubsystemReport"). 595 Ancestor(key). 596 Filter("Stages.Closed=", time.Time{}). 597 GetAll(c, &previous) 598 if err != nil { 599 return fmt.Errorf("failed to query old subsystem reports: %w", err) 600 } 601 for i, subsystem := range previous { 602 for i := range subsystem.Stages { 603 subsystem.Stages[i].Closed = timeNow(c) 604 } 605 if _, err := db.Put(c, prevKeys[i], subsystem); err != nil { 606 return fmt.Errorf("failed to save SubsystemReport: %w", err) 607 } 608 } 609 // Now save a new one. 610 reportKey := subsystemReportKey(c, key, report) 611 if _, err := db.Put(c, reportKey, report); err != nil { 612 return fmt.Errorf("failed to store new SubsystemReport: %w", err) 613 } 614 return nil 615 }, nil) 616 }