github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/dashboard/app/api.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 "compress/gzip" 9 "context" 10 "encoding/json" 11 "fmt" 12 "io" 13 "math/rand" 14 "net/http" 15 "net/url" 16 "reflect" 17 "regexp" 18 "sort" 19 "strings" 20 "time" 21 "unicode/utf8" 22 23 "github.com/google/syzkaller/dashboard/dashapi" 24 "github.com/google/syzkaller/pkg/asset" 25 "github.com/google/syzkaller/pkg/auth" 26 "github.com/google/syzkaller/pkg/debugtracer" 27 "github.com/google/syzkaller/pkg/email" 28 "github.com/google/syzkaller/pkg/hash" 29 "github.com/google/syzkaller/pkg/subsystem" 30 "github.com/google/syzkaller/sys/targets" 31 "google.golang.org/appengine/v2" 32 db "google.golang.org/appengine/v2/datastore" 33 "google.golang.org/appengine/v2/log" 34 "google.golang.org/appengine/v2/user" 35 ) 36 37 func initAPIHandlers() { 38 http.Handle("/api", handleJSON(handleAPI)) 39 } 40 41 var apiHandlers = map[string]APIHandler{ 42 "log_error": apiLogError, 43 "job_poll": apiJobPoll, 44 "job_reset": apiJobReset, 45 "job_done": apiJobDone, 46 "reporting_poll_bugs": apiReportingPollBugs, 47 "reporting_poll_notifs": apiReportingPollNotifications, 48 "reporting_poll_closed": apiReportingPollClosed, 49 "reporting_update": apiReportingUpdate, 50 "new_test_job": apiNewTestJob, 51 "needed_assets": apiNeededAssetsList, 52 "load_full_bug": apiLoadFullBug, 53 "save_discussion": apiSaveDiscussion, 54 } 55 56 var apiNamespaceHandlers = map[string]APINamespaceHandler{ 57 "upload_build": apiUploadBuild, 58 "builder_poll": apiBuilderPoll, 59 "report_build_error": apiReportBuildError, 60 "report_crash": apiReportCrash, 61 "report_failed_repro": apiReportFailedRepro, 62 "need_repro": apiNeedRepro, 63 "manager_stats": apiManagerStats, 64 "commit_poll": apiCommitPoll, 65 "upload_commits": apiUploadCommits, 66 "bug_list": apiBugList, 67 "load_bug": apiLoadBug, 68 "update_report": apiUpdateReport, 69 "add_build_assets": apiAddBuildAssets, 70 "log_to_repro": apiLogToReproduce, 71 } 72 73 type JSONHandler func(c context.Context, r *http.Request) (interface{}, error) 74 type APIHandler func(c context.Context, r *http.Request, payload []byte) (interface{}, error) 75 type APINamespaceHandler func(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) 76 77 const ( 78 maxReproPerBug = 10 79 reproRetryPeriod = 24 * time.Hour // try 1 repro per day until we have at least syz repro 80 // Attempt a new repro every ~ 3 months, even if we have already found it for the bug. This should: 81 // 1) Improve old repros over time (as we update descriptions / change syntax / repro algorithms). 82 // 2) Constrain the impact of bugs in syzkaller's backward compatibility. Fewer old repros, fewer problems. 83 reproStalePeriod = 100 * 24 * time.Hour 84 ) 85 86 // Overridable for testing. 87 var timeNow = func(c context.Context) time.Time { 88 return time.Now() 89 } 90 91 func timeSince(c context.Context, t time.Time) time.Duration { 92 return timeNow(c).Sub(t) 93 } 94 95 var maxCrashes = func() int { 96 const maxCrashesPerBug = 40 97 return maxCrashesPerBug 98 } 99 100 func handleJSON(fn JSONHandler) http.Handler { 101 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 102 c := appengine.NewContext(r) 103 reply, err := fn(c, r) 104 if err != nil { 105 // ErrAccess is logged earlier. 106 if err != ErrAccess { 107 log.Errorf(c, "%v", err) 108 } 109 http.Error(w, err.Error(), http.StatusInternalServerError) 110 return 111 } 112 w.Header().Set("Content-Type", "application/json") 113 if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { 114 w.Header().Set("Content-Encoding", "gzip") 115 gz := gzip.NewWriter(w) 116 if err := json.NewEncoder(gz).Encode(reply); err != nil { 117 log.Errorf(c, "failed to encode reply: %v", err) 118 } 119 gz.Close() 120 } else { 121 if err := json.NewEncoder(w).Encode(reply); err != nil { 122 log.Errorf(c, "failed to encode reply: %v", err) 123 } 124 } 125 }) 126 } 127 128 func handleAPI(c context.Context, r *http.Request) (reply interface{}, err error) { 129 client := r.PostFormValue("client") 130 method := r.PostFormValue("method") 131 log.Infof(c, "api %q from %q", method, client) 132 auth := auth.MakeEndpoint(auth.GoogleTokenInfoEndpoint) 133 subj, err := auth.DetermineAuthSubj(timeNow(c), r.Header["Authorization"]) 134 if err != nil { 135 return nil, err 136 } 137 // Somewhat confusingly the "key" parameter is the password. 138 ns, err := checkClient(getConfig(c), client, r.PostFormValue("key"), subj) 139 if err != nil { 140 if client != "" { 141 log.Errorf(c, "%v", err) 142 } else { 143 // Don't log as error if somebody just invokes /api. 144 log.Infof(c, "%v", err) 145 } 146 return nil, err 147 } 148 var payload []byte 149 if str := r.PostFormValue("payload"); str != "" { 150 gr, err := gzip.NewReader(strings.NewReader(str)) 151 if err != nil { 152 return nil, fmt.Errorf("failed to ungzip payload: %w", err) 153 } 154 payload, err = io.ReadAll(gr) 155 if err != nil { 156 return nil, fmt.Errorf("failed to ungzip payload: %w", err) 157 } 158 if err := gr.Close(); err != nil { 159 return nil, fmt.Errorf("failed to ungzip payload: %w", err) 160 } 161 } 162 handler := apiHandlers[method] 163 if handler != nil { 164 return handler(c, r, payload) 165 } 166 nsHandler := apiNamespaceHandlers[method] 167 if nsHandler == nil { 168 return nil, fmt.Errorf("unknown api method %q", method) 169 } 170 if ns == "" { 171 return nil, fmt.Errorf("method %q must be called within a namespace", method) 172 } 173 return nsHandler(c, ns, r, payload) 174 } 175 176 func apiLogError(c context.Context, r *http.Request, payload []byte) (interface{}, error) { 177 req := new(dashapi.LogEntry) 178 if err := json.Unmarshal(payload, req); err != nil { 179 return nil, fmt.Errorf("failed to unmarshal request: %w", err) 180 } 181 log.Errorf(c, "%v: %v", req.Name, req.Text) 182 return nil, nil 183 } 184 185 func apiBuilderPoll(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) { 186 req := new(dashapi.BuilderPollReq) 187 if err := json.Unmarshal(payload, req); err != nil { 188 return nil, fmt.Errorf("failed to unmarshal request: %w", err) 189 } 190 bugs, _, err := loadAllBugs(c, func(query *db.Query) *db.Query { 191 return query.Filter("Namespace=", ns). 192 Filter("Status<", BugStatusFixed) 193 }) 194 if err != nil { 195 return nil, err 196 } 197 m := make(map[string]bool) 198 loop: 199 for _, bug := range bugs { 200 // TODO(dvyukov): include this condition into the query if possible. 201 if len(bug.Commits) == 0 { 202 continue 203 } 204 for _, mgr := range bug.PatchedOn { 205 if mgr == req.Manager { 206 continue loop 207 } 208 } 209 for _, com := range bug.Commits { 210 m[com] = true 211 } 212 } 213 commits := make([]string, 0, len(m)) 214 for com := range m { 215 commits = append(commits, com) 216 } 217 sort.Strings(commits) 218 resp := &dashapi.BuilderPollResp{ 219 PendingCommits: commits, 220 ReportEmail: reportEmail(c, ns), 221 } 222 return resp, nil 223 } 224 225 func reportEmail(c context.Context, ns string) string { 226 for _, reporting := range getNsConfig(c, ns).Reporting { 227 if _, ok := reporting.Config.(*EmailConfig); ok { 228 return ownEmail(c) 229 } 230 } 231 return "" 232 } 233 234 func apiCommitPoll(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) { 235 resp := &dashapi.CommitPollResp{ 236 ReportEmail: reportEmail(c, ns), 237 } 238 for _, repo := range getNsConfig(c, ns).Repos { 239 if repo.NoPoll { 240 continue 241 } 242 resp.Repos = append(resp.Repos, dashapi.Repo{ 243 URL: repo.URL, 244 Branch: repo.Branch, 245 }) 246 } 247 var bugs []*Bug 248 _, err := db.NewQuery("Bug"). 249 Filter("Namespace=", ns). 250 Filter("NeedCommitInfo=", true). 251 Project("Commits"). 252 Limit(100). 253 GetAll(c, &bugs) 254 if err != nil { 255 return nil, fmt.Errorf("failed to query bugs: %w", err) 256 } 257 commits := make(map[string]bool) 258 for _, bug := range bugs { 259 for _, com := range bug.Commits { 260 commits[com] = true 261 } 262 } 263 for com := range commits { 264 resp.Commits = append(resp.Commits, com) 265 } 266 if getNsConfig(c, ns).RetestMissingBackports { 267 const takeBackportTitles = 5 268 backportCommits, err := pollBackportCommits(c, ns, takeBackportTitles) 269 if err != nil { 270 return nil, err 271 } 272 resp.Commits = append(resp.Commits, backportCommits...) 273 } 274 return resp, nil 275 } 276 277 func pollBackportCommits(c context.Context, ns string, count int) ([]string, error) { 278 // Let's assume that there won't be too many pending backports. 279 bugs, jobs, _, err := relevantBackportJobs(c) 280 if err != nil { 281 return nil, fmt.Errorf("failed to query backport: %w", err) 282 } 283 var backportTitles []string 284 for i, bug := range bugs { 285 if bug.Namespace != ns { 286 continue 287 } 288 backportTitles = append(backportTitles, jobs[i].Commits[0].Title) 289 } 290 randomizer := rand.New(rand.NewSource(timeNow(c).UnixNano())) 291 randomizer.Shuffle(len(backportTitles), func(i, j int) { 292 backportTitles[i], backportTitles[j] = backportTitles[j], backportTitles[i] 293 }) 294 if len(backportTitles) > count { 295 backportTitles = backportTitles[:count] 296 } 297 return backportTitles, nil 298 } 299 300 func apiUploadCommits(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) { 301 req := new(dashapi.CommitPollResultReq) 302 if err := json.Unmarshal(payload, req); err != nil { 303 return nil, fmt.Errorf("failed to unmarshal request: %w", err) 304 } 305 // This adds fixing commits to bugs. 306 err := addCommitsToBugs(c, ns, "", nil, req.Commits) 307 if err != nil { 308 return nil, err 309 } 310 // Now add commit info to commits. 311 for _, com := range req.Commits { 312 if com.Hash == "" { 313 continue 314 } 315 if err := addCommitInfo(c, ns, com); err != nil { 316 return nil, err 317 } 318 } 319 if getNsConfig(c, ns).RetestMissingBackports { 320 err = updateBackportCommits(c, ns, req.Commits) 321 if err != nil { 322 return nil, fmt.Errorf("failed to update backport commits: %w", err) 323 } 324 } 325 return nil, nil 326 } 327 328 func addCommitInfo(c context.Context, ns string, com dashapi.Commit) error { 329 var bugs []*Bug 330 keys, err := db.NewQuery("Bug"). 331 Filter("Namespace=", ns). 332 Filter("Commits=", com.Title). 333 GetAll(c, &bugs) 334 if err != nil { 335 return fmt.Errorf("failed to query bugs: %w", err) 336 } 337 for i, bug := range bugs { 338 if err := addCommitInfoToBug(c, bug, keys[i], com); err != nil { 339 return err 340 } 341 } 342 return nil 343 } 344 345 func addCommitInfoToBug(c context.Context, bug *Bug, bugKey *db.Key, com dashapi.Commit) error { 346 if needUpdate, err := addCommitInfoToBugImpl(c, bug, com); err != nil { 347 return err 348 } else if !needUpdate { 349 return nil 350 } 351 tx := func(c context.Context) error { 352 bug := new(Bug) 353 if err := db.Get(c, bugKey, bug); err != nil { 354 return fmt.Errorf("failed to get bug %v: %w", bugKey.StringID(), err) 355 } 356 if needUpdate, err := addCommitInfoToBugImpl(c, bug, com); err != nil { 357 return err 358 } else if !needUpdate { 359 return nil 360 } 361 if _, err := db.Put(c, bugKey, bug); err != nil { 362 return fmt.Errorf("failed to put bug: %w", err) 363 } 364 return nil 365 } 366 return db.RunInTransaction(c, tx, nil) 367 } 368 369 func addCommitInfoToBugImpl(c context.Context, bug *Bug, com dashapi.Commit) (bool, error) { 370 ci := -1 371 for i, title := range bug.Commits { 372 if title == com.Title { 373 ci = i 374 break 375 } 376 } 377 if ci < 0 { 378 return false, nil 379 } 380 for len(bug.CommitInfo) < len(bug.Commits) { 381 bug.CommitInfo = append(bug.CommitInfo, Commit{}) 382 } 383 hash0 := bug.CommitInfo[ci].Hash 384 date0 := bug.CommitInfo[ci].Date 385 author0 := bug.CommitInfo[ci].Author 386 needCommitInfo0 := bug.NeedCommitInfo 387 388 bug.CommitInfo[ci].Hash = com.Hash 389 bug.CommitInfo[ci].Date = com.Date 390 bug.CommitInfo[ci].Author = com.Author 391 bug.NeedCommitInfo = false 392 for i := range bug.CommitInfo { 393 if bug.CommitInfo[i].Hash == "" { 394 bug.NeedCommitInfo = true 395 break 396 } 397 } 398 changed := hash0 != bug.CommitInfo[ci].Hash || 399 date0 != bug.CommitInfo[ci].Date || 400 author0 != bug.CommitInfo[ci].Author || 401 needCommitInfo0 != bug.NeedCommitInfo 402 return changed, nil 403 } 404 405 func apiJobPoll(c context.Context, r *http.Request, payload []byte) (interface{}, error) { 406 if stop, err := emergentlyStopped(c); err != nil || stop { 407 // The bot's operation was aborted. Don't accept new crash reports. 408 return &dashapi.JobPollResp{}, err 409 } 410 req := new(dashapi.JobPollReq) 411 if err := json.Unmarshal(payload, req); err != nil { 412 return nil, fmt.Errorf("failed to unmarshal request: %w", err) 413 } 414 if len(req.Managers) == 0 { 415 return nil, fmt.Errorf("no managers") 416 } 417 return pollPendingJobs(c, req.Managers) 418 } 419 420 // nolint: dupl 421 func apiJobDone(c context.Context, r *http.Request, payload []byte) (interface{}, error) { 422 req := new(dashapi.JobDoneReq) 423 if err := json.Unmarshal(payload, req); err != nil { 424 return nil, fmt.Errorf("failed to unmarshal request: %w", err) 425 } 426 err := doneJob(c, req) 427 return nil, err 428 } 429 430 // nolint: dupl 431 func apiJobReset(c context.Context, r *http.Request, payload []byte) (interface{}, error) { 432 req := new(dashapi.JobResetReq) 433 if err := json.Unmarshal(payload, req); err != nil { 434 return nil, fmt.Errorf("failed to unmarshal request: %w", err) 435 } 436 err := resetJobs(c, req) 437 return nil, err 438 } 439 440 func apiUploadBuild(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) { 441 req := new(dashapi.Build) 442 if err := json.Unmarshal(payload, req); err != nil { 443 return nil, fmt.Errorf("failed to unmarshal request: %w", err) 444 } 445 now := timeNow(c) 446 _, isNewBuild, err := uploadBuild(c, now, ns, req, BuildNormal) 447 if err != nil { 448 return nil, err 449 } 450 if isNewBuild { 451 err := updateManager(c, ns, req.Manager, func(mgr *Manager, stats *ManagerStats) error { 452 prevKernel, prevSyzkaller := "", "" 453 if mgr.CurrentBuild != "" { 454 prevBuild, err := loadBuild(c, ns, mgr.CurrentBuild) 455 if err != nil { 456 return err 457 } 458 prevKernel = prevBuild.KernelCommit 459 prevSyzkaller = prevBuild.SyzkallerCommit 460 } 461 log.Infof(c, "new build on %v: kernel %v->%v syzkaller %v->%v", 462 req.Manager, prevKernel, req.KernelCommit, prevSyzkaller, req.SyzkallerCommit) 463 mgr.CurrentBuild = req.ID 464 if req.KernelCommit != prevKernel { 465 mgr.FailedBuildBug = "" 466 } 467 if req.SyzkallerCommit != prevSyzkaller { 468 mgr.FailedSyzBuildBug = "" 469 } 470 return nil 471 }) 472 if err != nil { 473 return nil, err 474 } 475 } 476 if len(req.Commits) != 0 || len(req.FixCommits) != 0 { 477 for i := range req.FixCommits { 478 // Reset hashes just to make sure, 479 // the build does not necessary come from the master repo, so we must not remember hashes. 480 req.FixCommits[i].Hash = "" 481 } 482 if err := addCommitsToBugs(c, ns, req.Manager, req.Commits, req.FixCommits); err != nil { 483 // We've already uploaded the build successfully and manager can use it. 484 // Moreover, addCommitsToBugs scans all bugs and can take long time. 485 // So just log the error. 486 log.Errorf(c, "failed to add commits to bugs: %v", err) 487 } 488 } 489 return nil, nil 490 } 491 492 func uploadBuild(c context.Context, now time.Time, ns string, req *dashapi.Build, typ BuildType) ( 493 *Build, bool, error) { 494 newAssets := []Asset{} 495 for i, toAdd := range req.Assets { 496 newAsset, err := parseIncomingAsset(c, toAdd) 497 if err != nil { 498 return nil, false, fmt.Errorf("failed to parse asset #%d: %w", i, err) 499 } 500 newAssets = append(newAssets, newAsset) 501 } 502 if build, err := loadBuild(c, ns, req.ID); err == nil { 503 return build, false, nil 504 } 505 checkStrLen := func(str, name string, maxLen int) error { 506 if str == "" { 507 return fmt.Errorf("%v is empty", name) 508 } 509 if len(str) > maxLen { 510 return fmt.Errorf("%v is too long (%v)", name, len(str)) 511 } 512 return nil 513 } 514 if err := checkStrLen(req.Manager, "Build.Manager", MaxStringLen); err != nil { 515 return nil, false, err 516 } 517 if err := checkStrLen(req.ID, "Build.ID", MaxStringLen); err != nil { 518 return nil, false, err 519 } 520 if err := checkStrLen(req.KernelRepo, "Build.KernelRepo", MaxStringLen); err != nil { 521 return nil, false, err 522 } 523 if len(req.KernelBranch) > MaxStringLen { 524 return nil, false, fmt.Errorf("Build.KernelBranch is too long (%v)", len(req.KernelBranch)) 525 } 526 if err := checkStrLen(req.SyzkallerCommit, "Build.SyzkallerCommit", MaxStringLen); err != nil { 527 return nil, false, err 528 } 529 if len(req.CompilerID) > MaxStringLen { 530 return nil, false, fmt.Errorf("Build.CompilerID is too long (%v)", len(req.CompilerID)) 531 } 532 if len(req.KernelCommit) > MaxStringLen { 533 return nil, false, fmt.Errorf("Build.KernelCommit is too long (%v)", len(req.KernelCommit)) 534 } 535 configID, err := putText(c, ns, textKernelConfig, req.KernelConfig, true) 536 if err != nil { 537 return nil, false, err 538 } 539 build := &Build{ 540 Namespace: ns, 541 Manager: req.Manager, 542 ID: req.ID, 543 Type: typ, 544 Time: now, 545 OS: req.OS, 546 Arch: req.Arch, 547 VMArch: req.VMArch, 548 SyzkallerCommit: req.SyzkallerCommit, 549 SyzkallerCommitDate: req.SyzkallerCommitDate, 550 CompilerID: req.CompilerID, 551 KernelRepo: req.KernelRepo, 552 KernelBranch: req.KernelBranch, 553 KernelCommit: req.KernelCommit, 554 KernelCommitTitle: req.KernelCommitTitle, 555 KernelCommitDate: req.KernelCommitDate, 556 KernelConfig: configID, 557 Assets: newAssets, 558 } 559 if _, err := db.Put(c, buildKey(c, ns, req.ID), build); err != nil { 560 return nil, false, err 561 } 562 return build, true, nil 563 } 564 565 func addCommitsToBugs(c context.Context, ns, manager string, titles []string, fixCommits []dashapi.Commit) error { 566 presentCommits := make(map[string]bool) 567 bugFixedBy := make(map[string][]string) 568 for _, com := range titles { 569 presentCommits[com] = true 570 } 571 for _, com := range fixCommits { 572 presentCommits[com.Title] = true 573 for _, bugID := range com.BugIDs { 574 bugFixedBy[bugID] = append(bugFixedBy[bugID], com.Title) 575 } 576 } 577 managers, err := managerList(c, ns) 578 if err != nil { 579 return err 580 } 581 // Fetching all bugs in a namespace can be slow, and there is no way to filter only Open/Dup statuses. 582 // So we run a separate query for each status, this both avoids fetching unnecessary data 583 // and splits a long query into two (two smaller queries have lower chances of trigerring 584 // timeouts than one huge). 585 for _, status := range []int{BugStatusOpen, BugStatusDup} { 586 err := addCommitsToBugsInStatus(c, status, ns, manager, managers, presentCommits, bugFixedBy) 587 if err != nil { 588 return err 589 } 590 } 591 return nil 592 } 593 594 func addCommitsToBugsInStatus(c context.Context, status int, ns, manager string, managers []string, 595 presentCommits map[string]bool, bugFixedBy map[string][]string) error { 596 bugs, _, err := loadAllBugs(c, func(query *db.Query) *db.Query { 597 return query.Filter("Namespace=", ns). 598 Filter("Status=", status) 599 }) 600 if err != nil { 601 return err 602 } 603 for _, bug := range bugs { 604 var fixCommits []string 605 for i := range bug.Reporting { 606 fixCommits = append(fixCommits, bugFixedBy[bug.Reporting[i].ID]...) 607 } 608 sort.Strings(fixCommits) 609 if err := addCommitsToBug(c, bug, manager, managers, fixCommits, presentCommits); err != nil { 610 return err 611 } 612 if bug.Status == BugStatusDup { 613 canon, err := canonicalBug(c, bug) 614 if err != nil { 615 return err 616 } 617 if canon.Status == BugStatusOpen && len(bug.Commits) == 0 { 618 if err := addCommitsToBug(c, canon, manager, managers, 619 fixCommits, presentCommits); err != nil { 620 return err 621 } 622 } 623 } 624 } 625 return nil 626 } 627 628 func addCommitsToBug(c context.Context, bug *Bug, manager string, managers, fixCommits []string, 629 presentCommits map[string]bool) error { 630 if !bugNeedsCommitUpdate(c, bug, manager, fixCommits, presentCommits, true) { 631 return nil 632 } 633 now := timeNow(c) 634 bugKey := bug.key(c) 635 tx := func(c context.Context) error { 636 bug := new(Bug) 637 if err := db.Get(c, bugKey, bug); err != nil { 638 return fmt.Errorf("failed to get bug %v: %w", bugKey.StringID(), err) 639 } 640 if !bugNeedsCommitUpdate(c, bug, manager, fixCommits, presentCommits, false) { 641 return nil 642 } 643 if len(fixCommits) != 0 && !reflect.DeepEqual(bug.Commits, fixCommits) { 644 bug.updateCommits(fixCommits, now) 645 } 646 if manager != "" { 647 bug.PatchedOn = append(bug.PatchedOn, manager) 648 if bug.Status == BugStatusOpen { 649 fixed := true 650 for _, mgr := range managers { 651 if !stringInList(bug.PatchedOn, mgr) { 652 fixed = false 653 break 654 } 655 } 656 if fixed { 657 bug.Status = BugStatusFixed 658 bug.Closed = now 659 } 660 } 661 } 662 if _, err := db.Put(c, bugKey, bug); err != nil { 663 return fmt.Errorf("failed to put bug: %w", err) 664 } 665 return nil 666 } 667 return db.RunInTransaction(c, tx, nil) 668 } 669 670 func bugNeedsCommitUpdate(c context.Context, bug *Bug, manager string, fixCommits []string, 671 presentCommits map[string]bool, dolog bool) bool { 672 if len(fixCommits) != 0 && !reflect.DeepEqual(bug.Commits, fixCommits) { 673 if dolog { 674 log.Infof(c, "bug %q is fixed with %q", bug.Title, fixCommits) 675 } 676 return true 677 } 678 if len(bug.Commits) == 0 || manager == "" || stringInList(bug.PatchedOn, manager) { 679 return false 680 } 681 for _, com := range bug.Commits { 682 if !presentCommits[com] { 683 return false 684 } 685 } 686 return true 687 } 688 689 // Note: if you do not need the latest data, prefer CachedManagersList(). 690 func managerList(c context.Context, ns string) ([]string, error) { 691 var builds []*Build 692 _, err := db.NewQuery("Build"). 693 Filter("Namespace=", ns). 694 Project("Manager"). 695 Distinct(). 696 GetAll(c, &builds) 697 if err != nil { 698 return nil, fmt.Errorf("failed to query builds: %w", err) 699 } 700 configManagers := getNsConfig(c, ns).Managers 701 var managers []string 702 for _, build := range builds { 703 if configManagers[build.Manager].Decommissioned { 704 continue 705 } 706 managers = append(managers, build.Manager) 707 } 708 return managers, nil 709 } 710 711 func apiReportBuildError(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) { 712 req := new(dashapi.BuildErrorReq) 713 if err := json.Unmarshal(payload, req); err != nil { 714 return nil, fmt.Errorf("failed to unmarshal request: %w", err) 715 } 716 now := timeNow(c) 717 build, _, err := uploadBuild(c, now, ns, &req.Build, BuildFailed) 718 if err != nil { 719 return nil, fmt.Errorf("failed to store build: %w", err) 720 } 721 req.Crash.BuildID = req.Build.ID 722 bug, err := reportCrash(c, build, &req.Crash) 723 if err != nil { 724 return nil, fmt.Errorf("failed to store crash: %w", err) 725 } 726 if err := updateManager(c, ns, req.Build.Manager, func(mgr *Manager, stats *ManagerStats) error { 727 log.Infof(c, "failed build on %v: kernel=%v", req.Build.Manager, req.Build.KernelCommit) 728 if req.Build.KernelCommit != "" { 729 mgr.FailedBuildBug = bug.keyHash(c) 730 } else { 731 mgr.FailedSyzBuildBug = bug.keyHash(c) 732 } 733 return nil 734 }); err != nil { 735 return nil, fmt.Errorf("failed to update manager: %w", err) 736 } 737 return nil, nil 738 } 739 740 const ( 741 corruptedReportTitle = "corrupted report" 742 suppressedReportTitle = "suppressed report" 743 ) 744 745 func apiReportCrash(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) { 746 if stop, err := emergentlyStopped(c); err != nil || stop { 747 // The bot's operation was aborted. Don't accept new crash reports. 748 return &dashapi.ReportCrashResp{}, err 749 } 750 req := new(dashapi.Crash) 751 if err := json.Unmarshal(payload, req); err != nil { 752 return nil, fmt.Errorf("failed to unmarshal request: %w", err) 753 } 754 build, err := loadBuild(c, ns, req.BuildID) 755 if err != nil { 756 return nil, err 757 } 758 if !getNsConfig(c, ns).TransformCrash(build, req) { 759 return new(dashapi.ReportCrashResp), nil 760 } 761 var bug2 *Bug 762 if req.OriginalTitle != "" { 763 bug2, err = findExistingBugForCrash(c, ns, []string{req.OriginalTitle}) 764 if err != nil { 765 return nil, fmt.Errorf("original bug query failed: %w", err) 766 } 767 } 768 bug, err := reportCrash(c, build, req) 769 if err != nil { 770 return nil, err 771 } 772 if bug2 != nil && bug2.Title != bug.Title && len(req.ReproLog) > 0 { 773 // During bug reproduction, we have diverted to another bug. 774 // Let's remember this. 775 err = saveFailedReproLog(c, bug2, build, req.ReproLog) 776 if err != nil { 777 return nil, fmt.Errorf("failed to save failed repro log: %w", err) 778 } 779 } 780 resp := &dashapi.ReportCrashResp{ 781 NeedRepro: needRepro(c, bug), 782 } 783 return resp, nil 784 } 785 786 // nolint: gocyclo 787 func reportCrash(c context.Context, build *Build, req *dashapi.Crash) (*Bug, error) { 788 assets, err := parseCrashAssets(c, req) 789 if err != nil { 790 return nil, err 791 } 792 req.Title = canonicalizeCrashTitle(req.Title, req.Corrupted, req.Suppressed) 793 if req.Corrupted || req.Suppressed { 794 req.AltTitles = []string{req.Title} 795 } else { 796 for i, t := range req.AltTitles { 797 req.AltTitles[i] = normalizeCrashTitle(t) 798 } 799 req.AltTitles = mergeStringList([]string{req.Title}, req.AltTitles) // dedup 800 } 801 req.Maintainers = email.MergeEmailLists(req.Maintainers) 802 803 ns := build.Namespace 804 bug, err := findBugForCrash(c, ns, req.AltTitles) 805 if err != nil { 806 return nil, fmt.Errorf("failed to find bug for the crash: %w", err) 807 } 808 if bug == nil { 809 bug, err = createBugForCrash(c, ns, req) 810 if err != nil { 811 return nil, fmt.Errorf("failed to create a bug: %w", err) 812 } 813 } 814 815 bugKey := bug.key(c) 816 now := timeNow(c) 817 reproLevel := ReproLevelNone 818 if len(req.ReproC) != 0 { 819 reproLevel = ReproLevelC 820 } else if len(req.ReproSyz) != 0 { 821 reproLevel = ReproLevelSyz 822 } 823 save := reproLevel != ReproLevelNone || 824 bug.NumCrashes < int64(maxCrashes()) || 825 now.Sub(bug.LastSavedCrash) > time.Hour || 826 bug.NumCrashes%20 == 0 || 827 !stringInList(bug.MergedTitles, req.Title) 828 if save { 829 if err := saveCrash(c, ns, req, bug, bugKey, build, assets); err != nil { 830 return nil, fmt.Errorf("failed to save the crash: %w", err) 831 } 832 } else { 833 log.Infof(c, "not saving crash for %q", bug.Title) 834 } 835 836 newSubsystems := []*subsystem.Subsystem{} 837 // Recalculate subsystems on the first saved crash and on the first saved repro, 838 // unless a user has already manually specified them. 839 calculateSubsystems := save && 840 !bug.hasUserSubsystems() && 841 (bug.NumCrashes == 0 || 842 bug.ReproLevel == ReproLevelNone && reproLevel != ReproLevelNone) 843 if calculateSubsystems { 844 newSubsystems, err = inferSubsystems(c, bug, bugKey, &debugtracer.NullTracer{}) 845 if err != nil { 846 log.Errorf(c, "%q: failed to extract subsystems: %s", bug.Title, err) 847 return nil, err 848 } 849 } 850 851 tx := func(c context.Context) error { 852 bug = new(Bug) 853 if err := db.Get(c, bugKey, bug); err != nil { 854 return fmt.Errorf("failed to get bug: %w", err) 855 } 856 bug.LastTime = now 857 if save { 858 bug.LastSavedCrash = now 859 } 860 if reproLevel != ReproLevelNone { 861 bug.NumRepro++ 862 bug.LastReproTime = now 863 } 864 if bug.ReproLevel < reproLevel { 865 bug.ReproLevel = reproLevel 866 } 867 if bug.HeadReproLevel < reproLevel { 868 bug.HeadReproLevel = reproLevel 869 } 870 if len(req.Report) != 0 { 871 bug.HasReport = true 872 } 873 if calculateSubsystems { 874 bug.SetAutoSubsystems(c, newSubsystems, now, getNsConfig(c, ns).Subsystems.Revision) 875 } 876 bug.increaseCrashStats(now) 877 bug.HappenedOn = mergeString(bug.HappenedOn, build.Manager) 878 // Migration of older entities (for new bugs Title is always in MergedTitles). 879 bug.MergedTitles = mergeString(bug.MergedTitles, bug.Title) 880 bug.MergedTitles = mergeString(bug.MergedTitles, req.Title) 881 bug.AltTitles = mergeStringList(bug.AltTitles, req.AltTitles) 882 if _, err = db.Put(c, bugKey, bug); err != nil { 883 return fmt.Errorf("failed to put bug: %w", err) 884 } 885 return nil 886 } 887 if err := db.RunInTransaction(c, tx, &db.TransactionOptions{XG: true}); err != nil { 888 return nil, fmt.Errorf("bug updating failed: %w", err) 889 } 890 if save { 891 purgeOldCrashes(c, bug, bugKey) 892 } 893 return bug, nil 894 } 895 896 func parseCrashAssets(c context.Context, req *dashapi.Crash) ([]Asset, error) { 897 assets := []Asset{} 898 for i, toAdd := range req.Assets { 899 newAsset, err := parseIncomingAsset(c, toAdd) 900 if err != nil { 901 return nil, fmt.Errorf("failed to parse asset #%d: %w", i, err) 902 } 903 assets = append(assets, newAsset) 904 } 905 return assets, nil 906 } 907 908 func (crash *Crash) UpdateReportingPriority(c context.Context, build *Build, bug *Bug) { 909 prio := int64(kernelRepoInfo(c, build).ReportingPriority) * 1e6 910 divReproPrio := int64(1) 911 if crash.ReproIsRevoked { 912 divReproPrio = 10 913 } 914 if crash.ReproC > 0 { 915 prio += 4e12 / divReproPrio 916 } else if crash.ReproSyz > 0 { 917 prio += 2e12 / divReproPrio 918 } 919 if crash.Title == bug.Title { 920 prio += 1e8 // prefer reporting crash that matches bug title 921 } 922 managerPrio := 0 923 if _, mgrConfig := activeManager(c, crash.Manager, bug.Namespace); mgrConfig != nil { 924 managerPrio = mgrConfig.Priority 925 } 926 prio += int64((managerPrio - MinManagerPriority) * 1e5) 927 if build.Arch == targets.AMD64 { 928 prio += 1e3 929 } 930 crash.ReportLen = prio 931 } 932 933 func saveCrash(c context.Context, ns string, req *dashapi.Crash, bug *Bug, bugKey *db.Key, 934 build *Build, assets []Asset) error { 935 crash := &Crash{ 936 Title: req.Title, 937 Manager: build.Manager, 938 BuildID: req.BuildID, 939 Time: timeNow(c), 940 Maintainers: email.MergeEmailLists(req.Maintainers, 941 GetEmails(req.Recipients, dashapi.To), 942 GetEmails(req.Recipients, dashapi.Cc)), 943 ReproOpts: req.ReproOpts, 944 Flags: int64(req.Flags), 945 Assets: assets, 946 ReportElements: CrashReportElements{ 947 GuiltyFiles: req.GuiltyFiles, 948 }, 949 } 950 var err error 951 if crash.Log, err = putText(c, ns, textCrashLog, req.Log, false); err != nil { 952 return err 953 } 954 if crash.Report, err = putText(c, ns, textCrashReport, req.Report, false); err != nil { 955 return err 956 } 957 if crash.ReproSyz, err = putText(c, ns, textReproSyz, req.ReproSyz, false); err != nil { 958 return err 959 } 960 if crash.ReproC, err = putText(c, ns, textReproC, req.ReproC, false); err != nil { 961 return err 962 } 963 if crash.MachineInfo, err = putText(c, ns, textMachineInfo, req.MachineInfo, true); err != nil { 964 return err 965 } 966 crash.UpdateReportingPriority(c, build, bug) 967 crashKey := db.NewIncompleteKey(c, "Crash", bugKey) 968 if _, err = db.Put(c, crashKey, crash); err != nil { 969 return fmt.Errorf("failed to put crash: %w", err) 970 } 971 return nil 972 } 973 974 func purgeOldCrashes(c context.Context, bug *Bug, bugKey *db.Key) { 975 const purgeEvery = 10 976 if bug.NumCrashes <= int64(2*maxCrashes()) || (bug.NumCrashes-1)%purgeEvery != 0 { 977 return 978 } 979 var crashes []*Crash 980 keys, err := db.NewQuery("Crash"). 981 Ancestor(bugKey). 982 Filter("Reported=", time.Time{}). 983 GetAll(c, &crashes) 984 if err != nil { 985 log.Errorf(c, "failed to fetch purge crashes: %v", err) 986 return 987 } 988 keyMap := make(map[*Crash]*db.Key) 989 for i, crash := range crashes { 990 keyMap[crash] = keys[i] 991 } 992 // Newest first. 993 sort.Slice(crashes, func(i, j int) bool { 994 return crashes[i].Time.After(crashes[j].Time) 995 }) 996 var toDelete []*db.Key 997 latestOnManager := make(map[string]bool) 998 uniqueTitle := make(map[string]bool) 999 deleted, reproCount, noreproCount := 0, 0, 0 1000 for _, crash := range crashes { 1001 if !crash.Reported.IsZero() { 1002 log.Errorf(c, "purging reported crash?") 1003 continue 1004 } 1005 // Preserve latest crash on each manager. 1006 if !latestOnManager[crash.Manager] { 1007 latestOnManager[crash.Manager] = true 1008 continue 1009 } 1010 // Preserve at least one crash with each title. 1011 if !uniqueTitle[crash.Title] { 1012 uniqueTitle[crash.Title] = true 1013 continue 1014 } 1015 // Preserve maxCrashes latest crashes with repro and without repro. 1016 count := &noreproCount 1017 if crash.ReproSyz != 0 || crash.ReproC != 0 { 1018 count = &reproCount 1019 } 1020 if *count < maxCrashes() { 1021 *count++ 1022 continue 1023 } 1024 toDelete = append(toDelete, keyMap[crash]) 1025 if crash.Log != 0 { 1026 toDelete = append(toDelete, db.NewKey(c, textCrashLog, "", crash.Log, nil)) 1027 } 1028 if crash.Report != 0 { 1029 toDelete = append(toDelete, db.NewKey(c, textCrashReport, "", crash.Report, nil)) 1030 } 1031 if crash.ReproSyz != 0 { 1032 toDelete = append(toDelete, db.NewKey(c, textReproSyz, "", crash.ReproSyz, nil)) 1033 } 1034 if crash.ReproC != 0 { 1035 toDelete = append(toDelete, db.NewKey(c, textReproC, "", crash.ReproC, nil)) 1036 } 1037 deleted++ 1038 if deleted == 2*purgeEvery { 1039 break 1040 } 1041 } 1042 if len(toDelete) == 0 { 1043 return 1044 } 1045 if err := db.DeleteMulti(c, toDelete); err != nil { 1046 log.Errorf(c, "failed to delete old crashes: %v", err) 1047 return 1048 } 1049 log.Infof(c, "deleted %v crashes for bug %q", deleted, bug.Title) 1050 } 1051 1052 func apiReportFailedRepro(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) { 1053 req := new(dashapi.CrashID) 1054 if err := json.Unmarshal(payload, req); err != nil { 1055 return nil, fmt.Errorf("failed to unmarshal request: %w", err) 1056 } 1057 req.Title = canonicalizeCrashTitle(req.Title, req.Corrupted, req.Suppressed) 1058 1059 bug, err := findExistingBugForCrash(c, ns, []string{req.Title}) 1060 if err != nil { 1061 return nil, err 1062 } 1063 if bug == nil { 1064 return nil, fmt.Errorf("%v: can't find bug for crash %q", ns, req.Title) 1065 } 1066 build, err := loadBuild(c, ns, req.BuildID) 1067 if err != nil { 1068 return nil, err 1069 } 1070 return nil, saveFailedReproLog(c, bug, build, req.ReproLog) 1071 } 1072 1073 func saveFailedReproLog(c context.Context, bug *Bug, build *Build, log []byte) error { 1074 now := timeNow(c) 1075 bugKey := bug.key(c) 1076 tx := func(c context.Context) error { 1077 bug := new(Bug) 1078 if err := db.Get(c, bugKey, bug); err != nil { 1079 return fmt.Errorf("failed to get bug: %w", err) 1080 } 1081 bug.NumRepro++ 1082 bug.LastReproTime = now 1083 if len(log) > 0 { 1084 err := saveReproAttempt(c, bug, build, log) 1085 if err != nil { 1086 return fmt.Errorf("failed to save repro log: %w", err) 1087 } 1088 } 1089 if _, err := db.Put(c, bugKey, bug); err != nil { 1090 return fmt.Errorf("failed to put bug: %w", err) 1091 } 1092 return nil 1093 } 1094 return db.RunInTransaction(c, tx, &db.TransactionOptions{ 1095 XG: true, 1096 Attempts: 30, 1097 }) 1098 } 1099 1100 const maxReproLogs = 5 1101 1102 func saveReproAttempt(c context.Context, bug *Bug, build *Build, log []byte) error { 1103 var deleteKeys []*db.Key 1104 for len(bug.ReproAttempts)+1 > maxReproLogs { 1105 deleteKeys = append(deleteKeys, 1106 db.NewKey(c, textReproLog, "", bug.ReproAttempts[0].Log, nil)) 1107 bug.ReproAttempts = bug.ReproAttempts[1:] 1108 } 1109 entry := BugReproAttempt{ 1110 Time: timeNow(c), 1111 Manager: build.Manager, 1112 } 1113 var err error 1114 if entry.Log, err = putText(c, bug.Namespace, textReproLog, log, false); err != nil { 1115 return err 1116 } 1117 if len(deleteKeys) > 0 { 1118 return db.DeleteMulti(c, deleteKeys) 1119 } 1120 bug.ReproAttempts = append(bug.ReproAttempts, entry) 1121 return nil 1122 } 1123 1124 func apiNeedRepro(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) { 1125 req := new(dashapi.CrashID) 1126 if err := json.Unmarshal(payload, req); err != nil { 1127 return nil, fmt.Errorf("failed to unmarshal request: %w", err) 1128 } 1129 if req.Corrupted { 1130 resp := &dashapi.NeedReproResp{ 1131 NeedRepro: false, 1132 } 1133 return resp, nil 1134 } 1135 req.Title = canonicalizeCrashTitle(req.Title, req.Corrupted, req.Suppressed) 1136 1137 bug, err := findExistingBugForCrash(c, ns, []string{req.Title}) 1138 if err != nil { 1139 return nil, err 1140 } 1141 if bug == nil { 1142 if req.MayBeMissing { 1143 // Manager does not send leak reports w/o repro to dashboard, we want to reproduce them. 1144 resp := &dashapi.NeedReproResp{ 1145 NeedRepro: true, 1146 } 1147 return resp, nil 1148 } 1149 return nil, fmt.Errorf("%v: can't find bug for crash %q", ns, req.Title) 1150 } 1151 resp := &dashapi.NeedReproResp{ 1152 NeedRepro: needRepro(c, bug), 1153 } 1154 return resp, nil 1155 } 1156 1157 func canonicalizeCrashTitle(title string, corrupted, suppressed bool) string { 1158 if corrupted { 1159 // The report is corrupted and the title is most likely invalid. 1160 // Such reports are usually unactionable and are discarded. 1161 // Collect them into a single bin. 1162 return corruptedReportTitle 1163 } 1164 if suppressed { 1165 // Collect all of them into a single bucket so that it's possible to control and assess them, 1166 // e.g. if there are some spikes in suppressed reports. 1167 return suppressedReportTitle 1168 } 1169 return normalizeCrashTitle(title) 1170 } 1171 1172 func normalizeCrashTitle(title string) string { 1173 return strings.TrimSpace(limitLength(title, maxTextLen)) 1174 } 1175 1176 func apiManagerStats(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) { 1177 req := new(dashapi.ManagerStatsReq) 1178 if err := json.Unmarshal(payload, req); err != nil { 1179 return nil, fmt.Errorf("failed to unmarshal request: %w", err) 1180 } 1181 now := timeNow(c) 1182 err := updateManager(c, ns, req.Name, func(mgr *Manager, stats *ManagerStats) error { 1183 mgr.Link = req.Addr 1184 mgr.LastAlive = now 1185 mgr.CurrentUpTime = req.UpTime 1186 if cur := int64(req.Corpus); cur > stats.MaxCorpus { 1187 stats.MaxCorpus = cur 1188 } 1189 if cur := int64(req.PCs); cur > stats.MaxPCs { 1190 stats.MaxPCs = cur 1191 } 1192 if cur := int64(req.Cover); cur > stats.MaxCover { 1193 stats.MaxCover = cur 1194 } 1195 if cur := int64(req.CrashTypes); cur > stats.CrashTypes { 1196 stats.CrashTypes = cur 1197 } 1198 stats.TotalFuzzingTime += req.FuzzingTime 1199 stats.TotalCrashes += int64(req.Crashes) 1200 stats.SuppressedCrashes += int64(req.SuppressedCrashes) 1201 stats.TotalExecs += int64(req.Execs) 1202 if cur := int64(req.TriagedCoverage); cur > stats.TriagedCoverage { 1203 stats.TriagedCoverage = cur 1204 } 1205 if cur := int64(req.TriagedPCs); cur > stats.TriagedPCs { 1206 stats.TriagedPCs = cur 1207 } 1208 return nil 1209 }) 1210 return nil, err 1211 } 1212 1213 func apiBugList(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) { 1214 keys, err := db.NewQuery("Bug"). 1215 Filter("Namespace=", ns). 1216 KeysOnly(). 1217 GetAll(c, nil) 1218 if err != nil { 1219 return nil, fmt.Errorf("failed to query bugs: %w", err) 1220 } 1221 resp := &dashapi.BugListResp{} 1222 for _, key := range keys { 1223 resp.List = append(resp.List, key.StringID()) 1224 } 1225 return resp, nil 1226 } 1227 1228 func apiUpdateReport(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) { 1229 req := new(dashapi.UpdateReportReq) 1230 if err := json.Unmarshal(payload, req); err != nil { 1231 return nil, fmt.Errorf("failed to unmarshal request: %w", err) 1232 } 1233 bug := new(Bug) 1234 bugKey := db.NewKey(c, "Bug", req.BugID, 0, nil) 1235 if err := db.Get(c, bugKey, bug); err != nil { 1236 return nil, fmt.Errorf("failed to get bug: %w", err) 1237 } 1238 if bug.Namespace != ns { 1239 return nil, fmt.Errorf("no such bug") 1240 } 1241 tx := func(c context.Context) error { 1242 crash := new(Crash) 1243 crashKey := db.NewKey(c, "Crash", "", req.CrashID, bugKey) 1244 if err := db.Get(c, crashKey, crash); err != nil { 1245 return fmt.Errorf("failed to query the crash: %w", err) 1246 } 1247 if req.GuiltyFiles != nil { 1248 crash.ReportElements.GuiltyFiles = *req.GuiltyFiles 1249 } 1250 if _, err := db.Put(c, crashKey, crash); err != nil { 1251 return fmt.Errorf("failed to put reported crash: %w", err) 1252 } 1253 return nil 1254 } 1255 return nil, db.RunInTransaction(c, tx, &db.TransactionOptions{Attempts: 5}) 1256 } 1257 1258 func apiLoadBug(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) { 1259 req := new(dashapi.LoadBugReq) 1260 if err := json.Unmarshal(payload, req); err != nil { 1261 return nil, fmt.Errorf("failed to unmarshal request: %w", err) 1262 } 1263 bug := new(Bug) 1264 bugKey := db.NewKey(c, "Bug", req.ID, 0, nil) 1265 if err := db.Get(c, bugKey, bug); err != nil { 1266 return nil, fmt.Errorf("failed to get bug: %w", err) 1267 } 1268 if bug.Namespace != ns { 1269 return nil, fmt.Errorf("no such bug") 1270 } 1271 return loadBugReport(c, bug) 1272 } 1273 1274 func apiLoadFullBug(c context.Context, r *http.Request, payload []byte) (interface{}, error) { 1275 req := new(dashapi.LoadFullBugReq) 1276 if err := json.Unmarshal(payload, req); err != nil { 1277 return nil, fmt.Errorf("failed to unmarshal request: %w", err) 1278 } 1279 bug, bugKey, err := findBugByReportingID(c, req.BugID) 1280 if err != nil { 1281 return nil, fmt.Errorf("failed to find the bug: %w", err) 1282 } 1283 bugReporting, _ := bugReportingByID(bug, req.BugID) 1284 if bugReporting == nil { 1285 return nil, fmt.Errorf("failed to find the bug reporting: %w", err) 1286 } 1287 return loadFullBugInfo(c, bug, bugKey, bugReporting) 1288 } 1289 1290 func loadBugReport(c context.Context, bug *Bug) (*dashapi.BugReport, error) { 1291 crash, crashKey, err := findCrashForBug(c, bug) 1292 if err != nil { 1293 return nil, err 1294 } 1295 // Create report for the last reporting so that it's stable and ExtID does not change over time. 1296 bugReporting := &bug.Reporting[len(bug.Reporting)-1] 1297 reporting := getNsConfig(c, bug.Namespace).ReportingByName(bugReporting.Name) 1298 if reporting == nil { 1299 return nil, fmt.Errorf("reporting %v is missing in config", bugReporting.Name) 1300 } 1301 return createBugReport(c, bug, crash, crashKey, bugReporting, reporting) 1302 } 1303 1304 func apiAddBuildAssets(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) { 1305 req := new(dashapi.AddBuildAssetsReq) 1306 if err := json.Unmarshal(payload, req); err != nil { 1307 return nil, fmt.Errorf("failed to unmarshal request: %w", err) 1308 } 1309 assets := []Asset{} 1310 for i, toAdd := range req.Assets { 1311 asset, err := parseIncomingAsset(c, toAdd) 1312 if err != nil { 1313 return nil, fmt.Errorf("failed to parse asset #%d: %w", i, err) 1314 } 1315 assets = append(assets, asset) 1316 } 1317 _, err := appendBuildAssets(c, ns, req.BuildID, assets) 1318 if err != nil { 1319 return nil, err 1320 } 1321 return nil, nil 1322 } 1323 1324 func parseIncomingAsset(c context.Context, newAsset dashapi.NewAsset) (Asset, error) { 1325 typeInfo := asset.GetTypeDescription(newAsset.Type) 1326 if typeInfo == nil { 1327 return Asset{}, fmt.Errorf("unknown asset type") 1328 } 1329 _, err := url.ParseRequestURI(newAsset.DownloadURL) 1330 if err != nil { 1331 return Asset{}, fmt.Errorf("invalid URL: %w", err) 1332 } 1333 return Asset{ 1334 Type: newAsset.Type, 1335 DownloadURL: newAsset.DownloadURL, 1336 CreateDate: timeNow(c), 1337 }, nil 1338 } 1339 1340 func apiNeededAssetsList(c context.Context, r *http.Request, payload []byte) (interface{}, error) { 1341 return queryNeededAssets(c) 1342 } 1343 1344 func findExistingBugForCrash(c context.Context, ns string, titles []string) (*Bug, error) { 1345 // First, try to find an existing bug that we already used to report this crash title. 1346 var bugs []*Bug 1347 _, err := db.NewQuery("Bug"). 1348 Filter("Namespace=", ns). 1349 Filter("MergedTitles=", titles[0]). 1350 GetAll(c, &bugs) 1351 if err != nil { 1352 return nil, fmt.Errorf("failed to query bugs: %w", err) 1353 } 1354 // We can find bugs with different bug.Title and uncomparable bug.Seq's. 1355 // But there should be only one active bug for each crash title, 1356 // so if we sort by Seq, the first active bug is our target bug. 1357 sort.Slice(bugs, func(i, j int) bool { 1358 return bugs[i].Seq > bugs[j].Seq 1359 }) 1360 for _, bug := range bugs { 1361 if active, err := isActiveBug(c, bug); err != nil { 1362 return nil, err 1363 } else if active { 1364 return bug, nil 1365 } 1366 } 1367 // This is required for incremental migration. 1368 // Older bugs don't have MergedTitles, so we need to check Title as well 1369 // (reportCrash will set MergedTitles later). 1370 for _, title := range titles { 1371 _, err = db.NewQuery("Bug"). 1372 Filter("Namespace=", ns). 1373 Filter("Title=", title). 1374 Order("-Seq"). 1375 Limit(1). 1376 GetAll(c, &bugs) 1377 if err != nil { 1378 return nil, fmt.Errorf("failed to query bugs: %w", err) 1379 } 1380 if len(bugs) != 0 { 1381 bug := bugs[0] 1382 if active, err := isActiveBug(c, bug); err != nil { 1383 return nil, err 1384 } else if active { 1385 return bug, nil 1386 } 1387 } 1388 } 1389 return nil, nil 1390 } 1391 1392 func findBugForCrash(c context.Context, ns string, titles []string) (*Bug, error) { 1393 // First, try to find an existing bug that we already used to report this crash title. 1394 bug, err := findExistingBugForCrash(c, ns, titles) 1395 if bug != nil || err != nil { 1396 return bug, err 1397 } 1398 // If there is no active bug for this crash title, try to find an existing candidate based on AltTitles. 1399 var bugs []*Bug 1400 for _, title := range titles { 1401 var bugs1 []*Bug 1402 _, err := db.NewQuery("Bug"). 1403 Filter("Namespace=", ns). 1404 Filter("AltTitles=", title). 1405 GetAll(c, &bugs1) 1406 if err != nil { 1407 return nil, fmt.Errorf("failed to query bugs: %w", err) 1408 } 1409 bugs = append(bugs, bugs1...) 1410 } 1411 // Sort to get determinism and skip inactive bugs. 1412 sort.Slice(bugs, func(i, j int) bool { 1413 if bugs[i].Title != bugs[j].Title { 1414 return bugs[i].Title < bugs[j].Title 1415 } 1416 return bugs[i].Seq > bugs[j].Seq 1417 }) 1418 var best *Bug 1419 bestPrio := 0 1420 for i, bug := range bugs { 1421 if i != 0 && bugs[i-1].Title == bug.Title { 1422 continue // skip inactive bugs 1423 } 1424 if active, err := isActiveBug(c, bug); err != nil { 1425 return nil, err 1426 } else if !active { 1427 continue 1428 } 1429 // Generally we should have few candidates (one in most cases). 1430 // However, it's possible if e.g. we first get a data race between A<->B, 1431 // then a race between C<->D and now we handle a race between B<->D, 1432 // it can be merged into any of the previous ones. 1433 // The priority here is very basic. The only known case we want to handle is bug title renaming 1434 // where we have an active bug with title A, but then A is renamed to B and A is attached as alt title. 1435 // In such case we want to merge the new crash into the old one. However, it's also unlikely that 1436 // in this case we have any other candidates. 1437 // Overall selection algorithm can be arbitrary changed because the selection for existing crashes 1438 // is fixed with bug.MergedTitles (stable for existing bugs/crashes). 1439 prio := 0 1440 if stringInList(titles[1:], bug.Title) { 1441 prio = 2 1442 } else if stringInList(bug.AltTitles[1:], titles[0]) { 1443 prio = 1 1444 } 1445 if best == nil || prio > bestPrio { 1446 best, bestPrio = bug, prio 1447 } 1448 } 1449 return best, nil 1450 } 1451 1452 func createBugForCrash(c context.Context, ns string, req *dashapi.Crash) (*Bug, error) { 1453 var bug *Bug 1454 now := timeNow(c) 1455 tx := func(c context.Context) error { 1456 for seq := int64(0); ; seq++ { 1457 bug = new(Bug) 1458 bugHash := bugKeyHash(c, ns, req.Title, seq) 1459 bugKey := db.NewKey(c, "Bug", bugHash, 0, nil) 1460 if err := db.Get(c, bugKey, bug); err != nil { 1461 if err != db.ErrNoSuchEntity { 1462 return fmt.Errorf("failed to get bug: %w", err) 1463 } 1464 bug = &Bug{ 1465 Namespace: ns, 1466 Seq: seq, 1467 Title: req.Title, 1468 MergedTitles: []string{req.Title}, 1469 AltTitles: req.AltTitles, 1470 Status: BugStatusOpen, 1471 NumCrashes: 0, 1472 NumRepro: 0, 1473 ReproLevel: ReproLevelNone, 1474 HasReport: false, 1475 FirstTime: now, 1476 LastTime: now, 1477 SubsystemsTime: now, 1478 } 1479 err = bug.updateReportings(c, getNsConfig(c, ns), now) 1480 if err != nil { 1481 return err 1482 } 1483 if _, err = db.Put(c, bugKey, bug); err != nil { 1484 return fmt.Errorf("failed to put new bug: %w", err) 1485 } 1486 return nil 1487 } 1488 canon, err := canonicalBug(c, bug) 1489 if err != nil { 1490 return err 1491 } 1492 if canon.Status != BugStatusOpen { 1493 continue 1494 } 1495 return nil 1496 } 1497 } 1498 if err := db.RunInTransaction(c, tx, &db.TransactionOptions{ 1499 XG: true, 1500 Attempts: 30, 1501 }); err != nil { 1502 return nil, err 1503 } 1504 return bug, nil 1505 } 1506 1507 func isActiveBug(c context.Context, bug *Bug) (bool, error) { 1508 if bug == nil { 1509 return false, nil 1510 } 1511 canon, err := canonicalBug(c, bug) 1512 if err != nil { 1513 return false, err 1514 } 1515 return canon.Status == BugStatusOpen, nil 1516 } 1517 1518 func needRepro(c context.Context, bug *Bug) bool { 1519 if !needReproForBug(c, bug) { 1520 return false 1521 } 1522 canon, err := canonicalBug(c, bug) 1523 if err != nil { 1524 log.Errorf(c, "failed to get canonical bug: %v", err) 1525 return false 1526 } 1527 return needReproForBug(c, canon) 1528 } 1529 1530 var syzErrorTitleRe = regexp.MustCompile(`^SYZFAIL:|^SYZFATAL:`) 1531 1532 func needReproForBug(c context.Context, bug *Bug) bool { 1533 // We already have fixing commits. 1534 if len(bug.Commits) > 0 { 1535 return false 1536 } 1537 if bug.Title == corruptedReportTitle || 1538 bug.Title == suppressedReportTitle { 1539 return false 1540 } 1541 if !getNsConfig(c, bug.Namespace).NeedRepro(bug) { 1542 return false 1543 } 1544 bestReproLevel := ReproLevelC 1545 // For some bugs there's anyway no chance to find a C repro. 1546 if syzErrorTitleRe.MatchString(bug.Title) { 1547 bestReproLevel = ReproLevelSyz 1548 } 1549 if bug.HeadReproLevel < bestReproLevel { 1550 // We have not found a best-level repro yet, try until we do. 1551 return bug.NumRepro < maxReproPerBug || timeSince(c, bug.LastReproTime) >= reproRetryPeriod 1552 } 1553 // When the best repro is already found, still do a repro attempt once in a while. 1554 return timeSince(c, bug.LastReproTime) >= reproStalePeriod 1555 } 1556 1557 func putText(c context.Context, ns, tag string, data []byte, dedup bool) (int64, error) { 1558 if ns == "" { 1559 return 0, fmt.Errorf("putting text outside of namespace") 1560 } 1561 if len(data) == 0 { 1562 return 0, nil 1563 } 1564 const ( 1565 // Kernel crash log is capped at ~1MB, but vm.Diagnose can add more. 1566 // These text files usually compress very well. 1567 maxTextLen = 10 << 20 1568 maxCompressedLen = 1000 << 10 // datastore entity limit is 1MB 1569 ) 1570 if len(data) > maxTextLen { 1571 data = data[:maxTextLen] 1572 } 1573 b := new(bytes.Buffer) 1574 for { 1575 z, _ := gzip.NewWriterLevel(b, gzip.BestCompression) 1576 z.Write(data) 1577 z.Close() 1578 if len(b.Bytes()) < maxCompressedLen { 1579 break 1580 } 1581 data = data[:len(data)/10*9] 1582 b.Reset() 1583 } 1584 var key *db.Key 1585 if dedup { 1586 h := hash.Hash([]byte(ns), b.Bytes()) 1587 key = db.NewKey(c, tag, "", h.Truncate64(), nil) 1588 } else { 1589 key = db.NewIncompleteKey(c, tag, nil) 1590 } 1591 text := &Text{ 1592 Namespace: ns, 1593 Text: b.Bytes(), 1594 } 1595 key, err := db.Put(c, key, text) 1596 if err != nil { 1597 return 0, err 1598 } 1599 return key.IntID(), nil 1600 } 1601 1602 func getText(c context.Context, tag string, id int64) ([]byte, string, error) { 1603 if id == 0 { 1604 return nil, "", nil 1605 } 1606 text := new(Text) 1607 if err := db.Get(c, db.NewKey(c, tag, "", id, nil), text); err != nil { 1608 return nil, "", fmt.Errorf("failed to read text %v: %w", tag, err) 1609 } 1610 d, err := gzip.NewReader(bytes.NewBuffer(text.Text)) 1611 if err != nil { 1612 return nil, "", fmt.Errorf("failed to read text %v: %w", tag, err) 1613 } 1614 data, err := io.ReadAll(d) 1615 if err != nil { 1616 return nil, "", fmt.Errorf("failed to read text %v: %w", tag, err) 1617 } 1618 return data, text.Namespace, nil 1619 } 1620 1621 // limitLength essentially does return s[:max], 1622 // but it ensures that we dot not split UTF-8 rune in half. 1623 // Otherwise appengine python scripts will break badly. 1624 func limitLength(s string, max int) string { 1625 s = strings.TrimSpace(s) 1626 if len(s) <= max { 1627 return s 1628 } 1629 for { 1630 s = s[:max] 1631 r, size := utf8.DecodeLastRuneInString(s) 1632 if r != utf8.RuneError || size != 1 { 1633 return s 1634 } 1635 max-- 1636 } 1637 } 1638 1639 func GetEmails(r dashapi.Recipients, filter dashapi.RecipientType) []string { 1640 emails := []string{} 1641 for _, user := range r { 1642 if user.Type == filter { 1643 emails = append(emails, user.Address.Address) 1644 } 1645 } 1646 sort.Strings(emails) 1647 return emails 1648 } 1649 1650 // Verifies that the given credentials are acceptable and returns the 1651 // corresponding namespace. 1652 func checkClient(conf *GlobalConfig, name0, secretPassword, oauthSubject string) (string, error) { 1653 checkAuth := func(ns, a string) (string, error) { 1654 if strings.HasPrefix(a, auth.OauthMagic) && a == oauthSubject { 1655 return ns, nil 1656 } 1657 if a != secretPassword { 1658 return ns, ErrAccess 1659 } 1660 return ns, nil 1661 } 1662 for name, authenticator := range conf.Clients { 1663 if name == name0 { 1664 return checkAuth("", authenticator) 1665 } 1666 } 1667 for ns, cfg := range conf.Namespaces { 1668 for name, authenticator := range cfg.Clients { 1669 if name == name0 { 1670 return checkAuth(ns, authenticator) 1671 } 1672 } 1673 } 1674 return "", ErrAccess 1675 } 1676 1677 func handleRefreshSubsystems(w http.ResponseWriter, r *http.Request) { 1678 c := appengine.NewContext(r) 1679 const updateBugsCount = 25 1680 for ns := range getConfig(c).Namespaces { 1681 err := reassignBugSubsystems(c, ns, updateBugsCount) 1682 if err != nil { 1683 log.Errorf(c, "failed to update subsystems for %s: %v", ns, err) 1684 } 1685 } 1686 } 1687 1688 func apiSaveDiscussion(c context.Context, r *http.Request, payload []byte) (interface{}, error) { 1689 req := new(dashapi.SaveDiscussionReq) 1690 if err := json.Unmarshal(payload, req); err != nil { 1691 return nil, fmt.Errorf("failed to unmarshal request: %w", err) 1692 } 1693 d := req.Discussion 1694 newBugIDs := []string{} 1695 for _, id := range d.BugIDs { 1696 _, _, err := findBugByReportingID(c, id) 1697 if err == nil { 1698 newBugIDs = append(newBugIDs, id) 1699 } 1700 } 1701 d.BugIDs = newBugIDs 1702 if len(d.BugIDs) == 0 { 1703 return nil, nil 1704 } 1705 return nil, mergeDiscussion(c, d) 1706 } 1707 1708 func emergentlyStopped(c context.Context) (bool, error) { 1709 keys, err := db.NewQuery("EmergencyStop"). 1710 Limit(1). 1711 KeysOnly(). 1712 GetAll(c, nil) 1713 if err != nil { 1714 return false, err 1715 } 1716 return len(keys) > 0, nil 1717 } 1718 1719 func recordEmergencyStop(c context.Context) error { 1720 key := db.NewKey(c, "EmergencyStop", "all", 0, nil) 1721 _, err := db.Put(c, key, &EmergencyStop{ 1722 Time: timeNow(c), 1723 User: user.Current(c).Email, 1724 }) 1725 return err 1726 } 1727 1728 // Share crash logs for non-reproduced bugs with syz-managers. 1729 // In future, this can also take care of repro exchange between instances 1730 // in the place of syz-hub. 1731 func apiLogToReproduce(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) { 1732 req := new(dashapi.LogToReproReq) 1733 if err := json.Unmarshal(payload, req); err != nil { 1734 return nil, fmt.Errorf("failed to unmarshal request: %w", err) 1735 } 1736 build, err := loadBuild(c, ns, req.BuildID) 1737 if err != nil { 1738 return nil, err 1739 } 1740 bugs, _, err := loadAllBugs(c, func(query *db.Query) *db.Query { 1741 return query.Filter("Namespace=", ns). 1742 Filter("HappenedOn=", build.Manager). 1743 Filter("Status=", BugStatusOpen) 1744 }) 1745 if err != nil { 1746 return nil, fmt.Errorf("failed to query bugs: %w", err) 1747 } 1748 rand.New(rand.NewSource(timeNow(c).UnixNano())).Shuffle(len(bugs), func(i, j int) { 1749 bugs[i], bugs[j] = bugs[j], bugs[i] 1750 }) 1751 // Let's limit the load on the DB. 1752 const bugsToConsider = 10 1753 checkedBugs := 0 1754 for _, bug := range bugs { 1755 if bug.ReproLevel != ReproLevelNone { 1756 continue 1757 } 1758 if len(bug.Commits) > 0 || len(bug.ReproAttempts) > 0 { 1759 // For now let's focus on all bugs where we have never ever 1760 // finished a bug reproduction process. 1761 continue 1762 } 1763 checkedBugs++ 1764 if checkedBugs > bugsToConsider { 1765 break 1766 } 1767 resp, err := logToReproForBug(c, bug, build.Manager) 1768 if resp != nil || err != nil { 1769 return resp, err 1770 } 1771 } 1772 return nil, nil 1773 } 1774 1775 func logToReproForBug(c context.Context, bug *Bug, manager string) (*dashapi.LogToReproResp, error) { 1776 const considerCrashes = 10 1777 crashes, _, err := queryCrashesForBug(c, bug.key(c), considerCrashes) 1778 if err != nil { 1779 return nil, err 1780 } 1781 for _, crash := range crashes { 1782 if crash.Manager != manager { 1783 continue 1784 } 1785 crashLog, _, err := getText(c, textCrashLog, crash.Log) 1786 if err != nil { 1787 return nil, fmt.Errorf("failed to query a crash log: %w", err) 1788 } 1789 return &dashapi.LogToReproResp{ 1790 Title: bug.Title, 1791 CrashLog: crashLog, 1792 }, nil 1793 } 1794 return nil, nil 1795 }