github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/dashboard/app/reporting_test.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 "context" 8 "fmt" 9 "html" 10 "reflect" 11 "regexp" 12 "testing" 13 "time" 14 15 "github.com/google/go-cmp/cmp" 16 "github.com/google/syzkaller/dashboard/dashapi" 17 "github.com/google/syzkaller/pkg/email" 18 "github.com/google/syzkaller/sys/targets" 19 "github.com/stretchr/testify/assert" 20 ) 21 22 func TestReportBug(t *testing.T) { 23 c := NewCtx(t) 24 defer c.Close() 25 26 build := testBuild(1) 27 c.client.UploadBuild(build) 28 29 crash1 := &dashapi.Crash{ 30 BuildID: "build1", 31 Title: "title1", 32 Maintainers: []string{`"Foo Bar" <foo@bar.com>`, `bar@foo.com`}, 33 Log: []byte("log1"), 34 Flags: dashapi.CrashUnderStrace, 35 Report: []byte("report1"), 36 MachineInfo: []byte("machine info 1"), 37 GuiltyFiles: []string{"a.c"}, 38 } 39 c.client.ReportCrash(crash1) 40 41 // Must get no reports for "unknown" type. 42 resp, _ := c.client.ReportingPollBugs("unknown") 43 c.expectEQ(len(resp.Reports), 0) 44 45 // Must get a proper report for "test" type. 46 resp, _ = c.client.ReportingPollBugs("test") 47 c.expectEQ(len(resp.Reports), 1) 48 rep := resp.Reports[0] 49 c.expectNE(rep.ID, "") 50 _, dbCrash, dbBuild := c.loadBug(rep.ID) 51 want := &dashapi.BugReport{ 52 Type: dashapi.ReportNew, 53 BugStatus: dashapi.BugStatusOpen, 54 Namespace: "test1", 55 Config: []byte(`{"Index":1}`), 56 ID: rep.ID, 57 OS: targets.Linux, 58 Arch: targets.AMD64, 59 VMArch: targets.AMD64, 60 UserSpaceArch: targets.AMD64, 61 First: true, 62 Moderation: true, 63 Title: "title1", 64 Link: fmt.Sprintf("https://testapp.appspot.com/bug?extid=%v", rep.ID), 65 CreditEmail: fmt.Sprintf("syzbot+%v@testapp.appspotmail.com", rep.ID), 66 Maintainers: []string{"bar@foo.com", "foo@bar.com", "subsystemA@list.com", "subsystemA@person.com"}, 67 CompilerID: "compiler1", 68 BuildID: "build1", 69 BuildTime: timeNow(c.ctx), 70 KernelRepo: "repo1", 71 KernelRepoAlias: "repo1 branch1", 72 KernelBranch: "branch1", 73 KernelCommit: "1111111111111111111111111111111111111111", 74 KernelCommitTitle: build.KernelCommitTitle, 75 KernelCommitDate: buildCommitDate, 76 KernelConfig: []byte("config1"), 77 KernelConfigLink: externalLink(c.ctx, textKernelConfig, dbBuild.KernelConfig), 78 SyzkallerCommit: "syzkaller_commit1", 79 MachineInfo: []byte("machine info 1"), 80 MachineInfoLink: externalLink(c.ctx, textMachineInfo, dbCrash.MachineInfo), 81 Log: []byte("log1"), 82 LogLink: externalLink(c.ctx, textCrashLog, dbCrash.Log), 83 LogHasStrace: true, 84 Report: []byte("report1"), 85 ReportLink: externalLink(c.ctx, textCrashReport, dbCrash.Report), 86 ReproOpts: []uint8{}, 87 CrashID: rep.CrashID, 88 CrashTime: timeNow(c.ctx), 89 NumCrashes: 1, 90 Manager: "manager1", 91 HappenedOn: []string{"repo1 branch1"}, 92 Assets: []dashapi.Asset{}, 93 ReportElements: &dashapi.ReportElements{GuiltyFiles: []string{"a.c"}}, 94 Subsystems: []dashapi.BugSubsystem{ 95 { 96 Name: "subsystemA", 97 Link: "https://testapp.appspot.com/test1/s/subsystemA", 98 }, 99 }, 100 } 101 c.expectEQ(want, rep) 102 103 // Since we did not update bug status yet, should get the same report again. 104 c.expectEQ(c.client.pollBug(), want) 105 106 // Now add syz repro and check that we get another bug report. 107 crash1.ReproOpts = []byte("some opts") 108 crash1.ReproSyz = []byte("getpid()") 109 want.Type = dashapi.ReportRepro 110 want.First = false 111 want.ReproSyz = []byte(syzReproPrefix + "#some opts\ngetpid()") 112 want.ReproOpts = []byte("some opts") 113 c.client.ReportCrash(crash1) 114 rep1 := c.client.pollBug() 115 c.expectNE(want.CrashID, rep1.CrashID) 116 _, dbCrash, _ = c.loadBug(rep.ID) 117 want.CrashID = rep1.CrashID 118 want.NumCrashes = 2 119 want.ReproSyzLink = externalLink(c.ctx, textReproSyz, dbCrash.ReproSyz) 120 want.LogLink = externalLink(c.ctx, textCrashLog, dbCrash.Log) 121 want.ReportLink = externalLink(c.ctx, textCrashReport, dbCrash.Report) 122 c.expectEQ(want, rep1) 123 124 reply, _ := c.client.ReportingUpdate(&dashapi.BugUpdate{ 125 ID: rep.ID, 126 Status: dashapi.BugStatusOpen, 127 ReproLevel: dashapi.ReproLevelSyz, 128 }) 129 c.expectEQ(reply.OK, true) 130 131 // After bug update should not get the report again. 132 c.client.pollBugs(0) 133 134 // Now close the bug in the first reporting. 135 c.client.updateBug(rep.ID, dashapi.BugStatusUpstream, "") 136 137 // Check that bug updates for the first reporting fail now. 138 reply, _ = c.client.ReportingUpdate(&dashapi.BugUpdate{ID: rep.ID, Status: dashapi.BugStatusOpen}) 139 c.expectEQ(reply.OK, false) 140 141 // Report another crash with syz repro for this bug, 142 // ensure that we report the new crash in the next reporting. 143 crash1.Report = []byte("report2") 144 c.client.ReportCrash(crash1) 145 146 // Check that we get the report in the second reporting. 147 rep2 := c.client.pollBug() 148 c.expectNE(rep2.ID, "") 149 c.expectNE(rep2.ID, rep.ID) 150 want.Type = dashapi.ReportNew 151 want.ID = rep2.ID 152 want.Report = []byte("report2") 153 want.LogLink = rep2.LogLink 154 want.ReportLink = rep2.ReportLink 155 want.CrashID = rep2.CrashID 156 want.ReproSyzLink = rep2.ReproSyzLink 157 want.ReproOpts = []byte("some opts") 158 want.Link = fmt.Sprintf("https://testapp.appspot.com/bug?extid=%v", rep2.ID) 159 want.CreditEmail = fmt.Sprintf("syzbot+%v@testapp.appspotmail.com", rep2.ID) 160 want.First = true 161 want.Moderation = false 162 want.Config = []byte(`{"Index":2}`) 163 want.NumCrashes = 3 164 c.expectEQ(want, rep2) 165 166 // Check that that we can't upstream the bug in the final reporting. 167 reply, _ = c.client.ReportingUpdate(&dashapi.BugUpdate{ 168 ID: rep2.ID, 169 Status: dashapi.BugStatusUpstream, 170 }) 171 c.expectEQ(reply.OK, false) 172 } 173 174 func TestInvalidBug(t *testing.T) { 175 c := NewCtx(t) 176 defer c.Close() 177 178 build := testBuild(1) 179 c.client.UploadBuild(build) 180 181 crash1 := testCrashWithRepro(build, 1) 182 c.client.ReportCrash(crash1) 183 184 rep := c.client.pollBug() 185 c.expectEQ(rep.Title, "title1") 186 187 reply, _ := c.client.ReportingUpdate(&dashapi.BugUpdate{ 188 ID: rep.ID, 189 Status: dashapi.BugStatusOpen, 190 ReproLevel: dashapi.ReproLevelC, 191 }) 192 c.expectEQ(reply.OK, true) 193 194 { 195 closed, _ := c.client.ReportingPollClosed([]string{rep.ID, "foobar"}) 196 c.expectEQ(len(closed), 0) 197 } 198 199 // Mark the bug as invalid. 200 c.client.updateBug(rep.ID, dashapi.BugStatusInvalid, "") 201 202 { 203 closed, _ := c.client.ReportingPollClosed([]string{rep.ID, "foobar"}) 204 c.expectEQ(len(closed), 1) 205 c.expectEQ(closed[0], rep.ID) 206 } 207 208 // Now it should not be reported in either reporting. 209 c.client.pollBugs(0) 210 211 // Now a similar crash happens again. 212 crash2 := &dashapi.Crash{ 213 BuildID: "build1", 214 Title: "title1", 215 Log: []byte("log2"), 216 Report: []byte("report2"), 217 ReproC: []byte("int main() { return 1; }"), 218 } 219 c.client.ReportCrash(crash2) 220 221 // Now it should be reported again. 222 rep = c.client.pollBug() 223 c.expectNE(rep.ID, "") 224 _, dbCrash, dbBuild := c.loadBug(rep.ID) 225 want := &dashapi.BugReport{ 226 Type: dashapi.ReportNew, 227 BugStatus: dashapi.BugStatusOpen, 228 Namespace: "test1", 229 Config: []byte(`{"Index":1}`), 230 ID: rep.ID, 231 OS: targets.Linux, 232 Arch: targets.AMD64, 233 VMArch: targets.AMD64, 234 UserSpaceArch: targets.AMD64, 235 First: true, 236 Moderation: true, 237 Title: "title1 (2)", 238 Link: fmt.Sprintf("https://testapp.appspot.com/bug?extid=%v", rep.ID), 239 CreditEmail: fmt.Sprintf("syzbot+%v@testapp.appspotmail.com", rep.ID), 240 BuildID: "build1", 241 BuildTime: timeNow(c.ctx), 242 CompilerID: "compiler1", 243 KernelRepo: "repo1", 244 KernelRepoAlias: "repo1 branch1", 245 KernelBranch: "branch1", 246 KernelCommit: "1111111111111111111111111111111111111111", 247 KernelCommitTitle: build.KernelCommitTitle, 248 KernelCommitDate: buildCommitDate, 249 KernelConfig: []byte("config1"), 250 KernelConfigLink: externalLink(c.ctx, textKernelConfig, dbBuild.KernelConfig), 251 SyzkallerCommit: "syzkaller_commit1", 252 Log: []byte("log2"), 253 LogLink: externalLink(c.ctx, textCrashLog, dbCrash.Log), 254 Report: []byte("report2"), 255 ReportLink: externalLink(c.ctx, textCrashReport, dbCrash.Report), 256 ReproC: []byte("int main() { return 1; }"), 257 ReproCLink: externalLink(c.ctx, textReproC, dbCrash.ReproC), 258 ReproOpts: []uint8{}, 259 CrashID: rep.CrashID, 260 CrashTime: timeNow(c.ctx), 261 NumCrashes: 1, 262 Manager: "manager1", 263 HappenedOn: []string{"repo1 branch1"}, 264 Assets: []dashapi.Asset{}, 265 ReportElements: &dashapi.ReportElements{}, 266 } 267 c.expectEQ(want, rep) 268 c.client.ReportFailedRepro(testCrashID(crash1)) 269 } 270 271 func TestReportingQuota(t *testing.T) { 272 c := NewCtx(t) 273 defer c.Close() 274 275 c.updateReporting("test1", "reporting1", 276 func(r Reporting) Reporting { 277 // Set a low daily limit. 278 r.DailyLimit = 2 279 return r 280 }) 281 282 build := testBuild(1) 283 c.client.UploadBuild(build) 284 285 const numReports = 5 286 for i := 0; i < numReports; i++ { 287 c.client.ReportCrash(testCrash(build, i)) 288 } 289 290 for _, reports := range []int{2, 2, 1, 0, 0} { 291 c.advanceTime(24 * time.Hour) 292 c.client.pollBugs(reports) 293 // Out of quota for today, so must get 0 reports. 294 c.client.pollBugs(0) 295 } 296 } 297 298 func TestReproReportingQuota(t *testing.T) { 299 // Test that new repro reports are also covered by daily limits. 300 c := NewCtx(t) 301 defer c.Close() 302 303 client := c.client 304 c.updateReporting("test1", "reporting1", 305 func(r Reporting) Reporting { 306 // Set a low daily limit. 307 r.DailyLimit = 2 308 return r 309 }) 310 311 build := testBuild(1) 312 client.UploadBuild(build) 313 314 // First report of two. 315 c.advanceTime(time.Minute) 316 client.ReportCrash(testCrash(build, 1)) 317 client.pollBug() 318 319 // Second report of two. 320 c.advanceTime(time.Minute) 321 crash := testCrash(build, 2) 322 client.ReportCrash(crash) 323 client.pollBug() 324 325 // Now we "find" a reproducer. 326 c.advanceTime(time.Minute) 327 client.ReportCrash(testCrashWithRepro(build, 1)) 328 329 // But there's no quota for it. 330 client.pollBugs(0) 331 332 // Wait a day and the quota appears. 333 c.advanceTime(time.Hour * 24) 334 client.pollBug() 335 } 336 337 // Basic dup scenario: mark one bug as dup of another. 338 func TestReportingDup(t *testing.T) { 339 c := NewCtx(t) 340 defer c.Close() 341 342 build := testBuild(1) 343 c.client.UploadBuild(build) 344 345 crash1 := testCrash(build, 1) 346 c.client.ReportCrash(crash1) 347 348 crash2 := testCrash(build, 2) 349 c.client.ReportCrash(crash2) 350 351 reports := c.client.pollBugs(2) 352 rep1 := reports[0] 353 rep2 := reports[1] 354 355 // Dup. 356 c.client.updateBug(rep2.ID, dashapi.BugStatusDup, rep1.ID) 357 { 358 // Both must be reported as open. 359 closed, _ := c.client.ReportingPollClosed([]string{rep1.ID, rep2.ID}) 360 c.expectEQ(len(closed), 0) 361 } 362 363 // Undup. 364 c.client.updateBug(rep2.ID, dashapi.BugStatusOpen, "") 365 366 // Dup again. 367 c.client.updateBug(rep2.ID, dashapi.BugStatusDup, rep1.ID) 368 369 // Dup crash happens again, new bug must not be created. 370 c.client.ReportCrash(crash2) 371 c.client.pollBugs(0) 372 373 // Now close the original bug, and check that new bugs for dup are now created. 374 c.client.updateBug(rep1.ID, dashapi.BugStatusInvalid, "") 375 { 376 // Now both must be reported as closed. 377 closed, _ := c.client.ReportingPollClosed([]string{rep1.ID, rep2.ID}) 378 c.expectEQ(len(closed), 2) 379 c.expectEQ(closed[0], rep1.ID) 380 c.expectEQ(closed[1], rep2.ID) 381 } 382 383 c.client.ReportCrash(crash2) 384 rep3 := c.client.pollBug() 385 c.expectEQ(rep3.Title, crash2.Title+" (2)") 386 387 // Unduping after the canonical bugs was closed must not work 388 // (we already created new bug for this report). 389 reply, _ := c.client.ReportingUpdate(&dashapi.BugUpdate{ 390 ID: rep2.ID, 391 Status: dashapi.BugStatusOpen, 392 }) 393 c.expectEQ(reply.OK, false) 394 } 395 396 // Dup bug onto a closed bug. 397 // A new crash report must create a new bug. 398 func TestReportingDupToClosed(t *testing.T) { 399 c := NewCtx(t) 400 defer c.Close() 401 402 build := testBuild(1) 403 c.client.UploadBuild(build) 404 405 crash1 := testCrash(build, 1) 406 c.client.ReportCrash(crash1) 407 408 crash2 := testCrash(build, 2) 409 c.client.ReportCrash(crash2) 410 411 reports := c.client.pollBugs(2) 412 c.client.updateBug(reports[0].ID, dashapi.BugStatusInvalid, "") 413 c.client.updateBug(reports[1].ID, dashapi.BugStatusDup, reports[0].ID) 414 415 c.client.ReportCrash(crash2) 416 rep2 := c.client.pollBug() 417 c.expectEQ(rep2.Title, crash2.Title+" (2)") 418 } 419 420 // Test that marking dups across reporting levels is not permitted. 421 func TestReportingDupCrossReporting(t *testing.T) { 422 c := NewCtx(t) 423 defer c.Close() 424 425 build := testBuild(1) 426 c.client.UploadBuild(build) 427 428 crash1 := testCrash(build, 1) 429 c.client.ReportCrash(crash1) 430 431 crash2 := testCrash(build, 2) 432 c.client.ReportCrash(crash2) 433 434 reports := c.client.pollBugs(2) 435 rep1 := reports[0] 436 rep2 := reports[1] 437 438 // Upstream second bug. 439 c.client.updateBug(rep2.ID, dashapi.BugStatusUpstream, "") 440 rep3 := c.client.pollBug() 441 442 { 443 closed, _ := c.client.ReportingPollClosed([]string{rep1.ID, rep2.ID, rep3.ID}) 444 c.expectEQ(len(closed), 1) 445 c.expectEQ(closed[0], rep2.ID) 446 } 447 448 // Duping must fail all ways. 449 cmds := []*dashapi.BugUpdate{ 450 {ID: rep1.ID, DupOf: rep1.ID}, 451 {ID: rep1.ID, DupOf: rep2.ID}, 452 {ID: rep2.ID, DupOf: rep1.ID}, 453 {ID: rep2.ID, DupOf: rep2.ID}, 454 {ID: rep2.ID, DupOf: rep3.ID}, 455 {ID: rep3.ID, DupOf: rep1.ID}, 456 {ID: rep3.ID, DupOf: rep2.ID}, 457 {ID: rep3.ID, DupOf: rep3.ID}, 458 } 459 for _, cmd := range cmds { 460 t.Logf("duping %v -> %v", cmd.ID, cmd.DupOf) 461 cmd.Status = dashapi.BugStatusDup 462 reply, _ := c.client.ReportingUpdate(cmd) 463 c.expectEQ(reply.OK, false) 464 } 465 // Special case of cross-reporting duping: 466 cmd := &dashapi.BugUpdate{ 467 Status: dashapi.BugStatusDup, 468 ID: rep1.ID, 469 DupOf: rep3.ID, 470 } 471 t.Logf("duping %v -> %v", cmd.ID, cmd.DupOf) 472 reply, _ := c.client.ReportingUpdate(cmd) 473 c.expectTrue(reply.OK) 474 } 475 476 // Test that dups can't form a cycle. 477 // The test builds cycles of length 1..4. 478 func TestReportingDupCycle(t *testing.T) { 479 c := NewCtx(t) 480 defer c.Close() 481 482 build := testBuild(1) 483 c.client.UploadBuild(build) 484 485 const N = 4 486 reps := make([]*dashapi.BugReport, N) 487 for i := 0; i < N; i++ { 488 t.Logf("*************** %v ***************", i) 489 c.client.ReportCrash(testCrash(build, i)) 490 reps[i] = c.client.pollBug() 491 replyError := "Can't dup bug to itself." 492 if i != 0 { 493 replyError = "Setting this dup would lead to a bug cycle, cycles are not allowed." 494 reply, _ := c.client.ReportingUpdate(&dashapi.BugUpdate{ 495 Status: dashapi.BugStatusDup, 496 ID: reps[i-1].ID, 497 DupOf: reps[i].ID, 498 }) 499 c.expectEQ(reply.OK, true) 500 } 501 reply, _ := c.client.ReportingUpdate(&dashapi.BugUpdate{ 502 Status: dashapi.BugStatusDup, 503 ID: reps[i].ID, 504 DupOf: reps[0].ID, 505 }) 506 c.expectEQ(reply.OK, false) 507 c.expectEQ(reply.Error, false) 508 c.expectEQ(reply.Text, replyError) 509 c.advanceTime(24 * time.Hour) 510 } 511 } 512 513 func TestReportingFilter(t *testing.T) { 514 c := NewCtx(t) 515 defer c.Close() 516 517 build := testBuild(1) 518 c.client.UploadBuild(build) 519 520 crash1 := testCrash(build, 1) 521 crash1.Title = "skip with repro 1" 522 c.client.ReportCrash(crash1) 523 524 // This does not skip first reporting, because it does not have repro. 525 rep1 := c.client.pollBug() 526 c.expectEQ(string(rep1.Config), `{"Index":1}`) 527 528 crash1.ReproSyz = []byte("getpid()") 529 c.client.ReportCrash(crash1) 530 531 // This has repro but was already reported to first reporting, 532 // so repro must go to the first reporting as well. 533 rep2 := c.client.pollBug() 534 c.expectEQ(string(rep2.Config), `{"Index":1}`) 535 536 // Now upstream it and it must go to the second reporting. 537 c.client.updateBug(rep1.ID, dashapi.BugStatusUpstream, "") 538 539 rep3 := c.client.pollBug() 540 c.expectEQ(string(rep3.Config), `{"Index":2}`) 541 542 // Now report a bug that must go to the second reporting right away. 543 crash2 := testCrash(build, 2) 544 crash2.Title = "skip with repro 2" 545 crash2.ReproSyz = []byte("getpid()") 546 c.client.ReportCrash(crash2) 547 548 rep4 := c.client.pollBug() 549 c.expectEQ(string(rep4.Config), `{"Index":2}`) 550 } 551 552 func TestMachineInfo(t *testing.T) { 553 c := NewCtx(t) 554 defer c.Close() 555 556 build := testBuild(1) 557 c.client.UploadBuild(build) 558 559 machineInfo := []byte("info1") 560 561 // Create a crash with machine information and check the returned machine 562 // information field is equal. 563 crash := &dashapi.Crash{ 564 BuildID: "build1", 565 Title: "title1", 566 Maintainers: []string{`"Foo Bar" <foo@bar.com>`, `bar@foo.com`}, 567 Log: []byte("log1"), 568 Report: []byte("report1"), 569 MachineInfo: machineInfo, 570 } 571 c.client.ReportCrash(crash) 572 rep := c.client.pollBug() 573 c.expectEQ(machineInfo, rep.MachineInfo) 574 575 // Check that a link to machine information page is created on the dashboard, 576 // and the content is correct. 577 indexPage, err := c.AuthGET(AccessAdmin, "/test1") 578 c.expectOK(err) 579 bugLinkRegex := regexp.MustCompile(`<a href="(/bug\?extid=[^"]+)">title1</a>`) 580 bugLinkSubmatch := bugLinkRegex.FindSubmatch(indexPage) 581 c.expectEQ(len(bugLinkSubmatch), 2) 582 bugURL := html.UnescapeString(string(bugLinkSubmatch[1])) 583 584 bugPage, err := c.AuthGET(AccessAdmin, bugURL) 585 c.expectOK(err) 586 infoLinkRegex := regexp.MustCompile(`<a href="(/text\?tag=MachineInfo[^"]+)">info</a>`) 587 infoLinkSubmatch := infoLinkRegex.FindSubmatch(bugPage) 588 c.expectEQ(len(infoLinkSubmatch), 2) 589 infoURL := html.UnescapeString(string(infoLinkSubmatch[1])) 590 591 receivedInfo, err := c.AuthGET(AccessAdmin, infoURL) 592 c.expectOK(err) 593 c.expectEQ(receivedInfo, machineInfo) 594 } 595 596 func TestAltTitles1(t *testing.T) { 597 c := NewCtx(t) 598 defer c.Close() 599 600 build := testBuild(1) 601 c.client.UploadBuild(build) 602 603 // crash2.AltTitles matches crash1.Title. 604 crash1 := testCrash(build, 1) 605 crash2 := testCrashWithRepro(build, 2) 606 crash2.AltTitles = []string{crash1.Title} 607 608 c.client.ReportCrash(crash1) 609 rep := c.client.pollBug() 610 c.expectEQ(rep.Title, crash1.Title) 611 c.expectEQ(rep.Log, crash1.Log) 612 613 c.client.ReportCrash(crash2) 614 rep = c.client.pollBug() 615 c.expectEQ(rep.Title, crash1.Title) 616 c.expectEQ(rep.Log, crash2.Log) 617 } 618 619 func TestAltTitles2(t *testing.T) { 620 c := NewCtx(t) 621 defer c.Close() 622 623 build := testBuild(1) 624 c.client.UploadBuild(build) 625 626 // crash2.Title matches crash1.AltTitles, but reported in opposite order. 627 crash1 := testCrash(build, 1) 628 crash2 := testCrash(build, 2) 629 crash2.AltTitles = []string{crash1.Title} 630 631 c.client.ReportCrash(crash2) 632 rep := c.client.pollBug() 633 c.expectEQ(rep.Title, crash2.Title) 634 c.expectEQ(rep.Log, crash2.Log) 635 636 c.client.ReportCrash(crash1) 637 c.client.pollBugs(0) 638 } 639 640 func TestAltTitles3(t *testing.T) { 641 c := NewCtx(t) 642 defer c.Close() 643 644 build := testBuild(1) 645 c.client.UploadBuild(build) 646 647 // crash2.AltTitles matches crash1.AltTitles. 648 crash1 := testCrash(build, 1) 649 crash1.AltTitles = []string{"foobar"} 650 crash2 := testCrash(build, 2) 651 crash2.AltTitles = crash1.AltTitles 652 653 c.client.ReportCrash(crash1) 654 c.client.pollBugs(1) 655 c.client.ReportCrash(crash2) 656 c.client.pollBugs(0) 657 } 658 659 func TestAltTitles4(t *testing.T) { 660 c := NewCtx(t) 661 defer c.Close() 662 663 build := testBuild(1) 664 c.client.UploadBuild(build) 665 666 // crash1.AltTitles matches crash2.AltTitles which matches crash3.AltTitles. 667 crash1 := testCrash(build, 1) 668 crash1.AltTitles = []string{"foobar1"} 669 crash2 := testCrash(build, 2) 670 crash2.AltTitles = []string{"foobar1", "foobar2"} 671 crash3 := testCrash(build, 3) 672 crash3.AltTitles = []string{"foobar2"} 673 674 c.client.ReportCrash(crash1) 675 c.client.pollBugs(1) 676 c.client.ReportCrash(crash2) 677 c.client.pollBugs(0) 678 c.client.ReportCrash(crash3) 679 c.client.pollBugs(0) 680 } 681 682 func TestAltTitles5(t *testing.T) { 683 c := NewCtx(t) 684 defer c.Close() 685 686 build := testBuild(1) 687 c.client.UploadBuild(build) 688 689 // Test which of the possible existing bugs we choose for merging. 690 crash1 := testCrash(build, 1) 691 crash1.AltTitles = []string{"foo"} 692 c.client.ReportCrash(crash1) 693 c.client.pollBugs(1) 694 695 crash2 := testCrash(build, 2) 696 crash2.Title = "bar" 697 c.client.ReportCrash(crash2) 698 c.client.pollBugs(1) 699 700 crash3 := testCrash(build, 3) 701 c.client.ReportCrash(crash3) 702 c.client.pollBugs(1) 703 crash3.AltTitles = []string{"bar"} 704 c.client.ReportCrash(crash3) 705 c.client.pollBugs(0) 706 707 crash := testCrashWithRepro(build, 10) 708 crash.Title = "foo" 709 crash.AltTitles = []string{"bar"} 710 c.client.ReportCrash(crash) 711 rep := c.client.pollBug() 712 c.expectEQ(rep.Title, crash2.Title) 713 c.expectEQ(rep.Log, crash.Log) 714 } 715 716 func TestAltTitles6(t *testing.T) { 717 c := NewCtx(t) 718 defer c.Close() 719 720 build := testBuild(1) 721 c.client.UploadBuild(build) 722 723 // Test which of the possible existing bugs we choose for merging in presence of closed bugs. 724 crash1 := testCrash(build, 1) 725 crash1.AltTitles = []string{"foo"} 726 c.client.ReportCrash(crash1) 727 rep := c.client.pollBug() 728 c.client.updateBug(rep.ID, dashapi.BugStatusInvalid, "") 729 c.client.ReportCrash(crash1) 730 c.client.pollBug() 731 732 crash2 := testCrash(build, 2) 733 crash2.Title = "bar" 734 c.client.ReportCrash(crash2) 735 rep = c.client.pollBug() 736 c.client.updateBug(rep.ID, dashapi.BugStatusInvalid, "") 737 738 c.advanceTime(24 * time.Hour) 739 crash3 := testCrash(build, 3) 740 c.client.ReportCrash(crash3) 741 c.client.pollBugs(1) 742 crash3.AltTitles = []string{"foo"} 743 c.client.ReportCrash(crash3) 744 c.client.pollBugs(0) 745 746 crash := testCrashWithRepro(build, 10) 747 crash.Title = "foo" 748 crash.AltTitles = []string{"bar"} 749 c.client.ReportCrash(crash) 750 rep = c.client.pollBug() 751 c.expectEQ(rep.Title, crash1.Title+" (2)") 752 c.expectEQ(rep.Log, crash.Log) 753 } 754 755 func TestAltTitles7(t *testing.T) { 756 c := NewCtx(t) 757 defer c.Close() 758 759 build := testBuild(1) 760 c.client.UploadBuild(build) 761 762 // Test that bug merging is stable: if we started merging into a bug, we continue merging into that bug 763 // even if a better candidate appears. 764 crash1 := testCrash(build, 1) 765 crash1.AltTitles = []string{"foo"} 766 c.client.ReportCrash(crash1) 767 c.client.pollBug() 768 769 // This will be merged into crash1. 770 crash2 := testCrash(build, 2) 771 crash2.AltTitles = []string{"foo"} 772 c.client.ReportCrash(crash2) 773 c.client.pollBugs(0) 774 775 // Now report a better candidate. 776 crash3 := testCrash(build, 3) 777 crash3.Title = "aaa" 778 c.client.ReportCrash(crash3) 779 c.client.pollBug() 780 crash3.AltTitles = []string{crash2.Title} 781 c.client.ReportCrash(crash3) 782 c.client.pollBugs(0) 783 784 // Now report crash2 with a repro and ensure that it's still merged into crash1. 785 crash2.ReproOpts = []byte("some opts") 786 crash2.ReproSyz = []byte("getpid()") 787 c.client.ReportCrash(crash2) 788 rep := c.client.pollBug() 789 c.expectEQ(rep.Title, crash1.Title) 790 c.expectEQ(rep.Log, crash2.Log) 791 } 792 793 func TestDetachExternalTracker(t *testing.T) { 794 c := NewCtx(t) 795 defer c.Close() 796 797 build := testBuild(1) 798 c.client.UploadBuild(build) 799 800 crash1 := testCrash(build, 1) 801 c.client.ReportCrash(crash1) 802 803 // Get single report for "test" type. 804 resp, _ := c.client.ReportingPollBugs("test") 805 c.expectEQ(len(resp.Reports), 1) 806 rep1 := resp.Reports[0] 807 c.expectNE(rep1.ID, "") 808 c.expectEQ(string(rep1.Config), `{"Index":1}`) 809 810 // Signal detach_reporting for current bug. 811 reply, _ := c.client.ReportingUpdate(&dashapi.BugUpdate{ 812 ID: rep1.ID, 813 Status: dashapi.BugStatusUpstream, 814 ReproLevel: dashapi.ReproLevelNone, 815 Link: "http://URI/1", 816 CrashID: rep1.CrashID, 817 }) 818 c.expectEQ(reply.OK, true) 819 820 // Now add syz repro to check it doesn't use first reporting. 821 crash1.ReproOpts = []byte("some opts") 822 crash1.ReproSyz = []byte("getpid()") 823 c.client.ReportCrash(crash1) 824 825 // Fetch bug and check reporting path (Config) is different. 826 rep2 := c.client.pollBug() 827 c.expectNE(rep2.ID, "") 828 c.expectEQ(string(rep2.Config), `{"Index":2}`) 829 830 closed, _ := c.client.ReportingPollClosed([]string{rep1.ID, rep2.ID}) 831 c.expectEQ(len(closed), 1) 832 c.expectEQ(closed[0], rep1.ID) 833 } 834 835 func TestUpdateBugReporting(t *testing.T) { 836 c := NewCtx(t) 837 defer c.Close() 838 setIDs := func(bug *Bug, arr []BugReporting) { 839 for i := range arr { 840 arr[i].ID = bugReportingHash(bug.keyHash(c.ctx), arr[i].Name) 841 } 842 } 843 now := timeNow(c.ctx) 844 // We test against the test2 namespace. 845 cfg := c.config().Namespaces["test2"] 846 tests := []struct { 847 Before []BugReporting 848 After []BugReporting 849 Error bool 850 }{ 851 // Initially empty object. 852 { 853 Before: []BugReporting{}, 854 After: []BugReporting{ 855 { 856 Name: "reporting1", 857 }, 858 { 859 Name: "reporting2", 860 }, 861 { 862 Name: "reporting3", 863 }, 864 }, 865 }, 866 // Prepending and appending new reporting objects, the bug is not reported yet. 867 { 868 Before: []BugReporting{ 869 { 870 Name: "reporting2", 871 }, 872 }, 873 After: []BugReporting{ 874 { 875 Name: "reporting1", 876 }, 877 { 878 Name: "reporting2", 879 }, 880 { 881 Name: "reporting3", 882 }, 883 }, 884 }, 885 // The order or reportings is changed. 886 { 887 Before: []BugReporting{ 888 { 889 Name: "reporting2", 890 }, 891 { 892 Name: "reporting1", 893 }, 894 { 895 Name: "reporting3", 896 }, 897 }, 898 After: []BugReporting{}, 899 Error: true, 900 }, 901 // Prepending and appending new reporting objects, the bug is already reported. 902 { 903 Before: []BugReporting{ 904 { 905 Name: "reporting2", 906 Reported: now, 907 ExtID: "abcd", 908 }, 909 }, 910 After: []BugReporting{ 911 { 912 Name: "reporting1", 913 Closed: now, 914 Reported: now, 915 Dummy: true, 916 }, 917 { 918 Name: "reporting2", 919 Reported: now, 920 ExtID: "abcd", 921 }, 922 { 923 Name: "reporting3", 924 }, 925 }, 926 }, 927 // It must look like as if the new Reporting was immediate. 928 { 929 Before: []BugReporting{ 930 { 931 Name: "reporting1", 932 Reported: now.Add(-24 * time.Hour), 933 ExtID: "abcd", 934 }, 935 { 936 Name: "reporting3", 937 Reported: now, 938 ExtID: "efgh", 939 }, 940 }, 941 After: []BugReporting{ 942 { 943 Name: "reporting1", 944 Reported: now.Add(-24 * time.Hour), 945 ExtID: "abcd", 946 }, 947 { 948 Name: "reporting2", 949 Reported: now.Add(-24 * time.Hour), 950 Closed: now.Add(-24 * time.Hour), 951 Dummy: true, 952 }, 953 { 954 Name: "reporting3", 955 Reported: now, 956 ExtID: "efgh", 957 }, 958 }, 959 }, 960 } 961 for _, test := range tests { 962 bug := &Bug{ 963 Title: "bug", 964 Reporting: test.Before, 965 Namespace: "test2", 966 } 967 setIDs(bug, bug.Reporting) 968 setIDs(bug, test.After) 969 hasError := bug.updateReportings(c.ctx, cfg, now) != nil 970 if hasError != test.Error { 971 t.Errorf("before: %#v, expected error: %v, got error: %v", test.Before, test.Error, hasError) 972 } 973 if !test.Error && !reflect.DeepEqual(bug.Reporting, test.After) { 974 t.Errorf("before: %#v, expected after: %#v, got after: %#v", test.Before, test.After, bug.Reporting) 975 } 976 } 977 } 978 979 func TestFullBugInfo(t *testing.T) { 980 c := NewCtx(t) 981 defer c.Close() 982 983 build := testBuild(1) 984 c.client.UploadBuild(build) 985 986 const crashTitle = "WARNING: abcd" 987 988 // Oldest crash: with strace. 989 crashStrace := testCrashWithRepro(build, 1) 990 crashStrace.Title = crashTitle 991 crashStrace.Flags = dashapi.CrashUnderStrace 992 crashStrace.Report = []byte("with strace") 993 c.client.ReportCrash(crashStrace) 994 rep := c.client.pollBug() 995 996 // Newer: just with repro. 997 c.advanceTime(24 * 7 * time.Hour) 998 crashRepro := testCrashWithRepro(build, 1) 999 crashRepro.Title = crashTitle 1000 crashRepro.Report = []byte("with repro") 1001 c.client.ReportCrash(crashRepro) 1002 1003 // Ensure we have some bisect jobs done. 1004 pollResp := c.client.pollJobs(build.Manager) 1005 c.expectNE(pollResp.ID, "") 1006 jobID := pollResp.ID 1007 done := &dashapi.JobDoneReq{ 1008 ID: jobID, 1009 Build: *testBuild(3), 1010 Log: []byte("bisect log"), 1011 Commits: []dashapi.Commit{ 1012 { 1013 Hash: "111111111111111111111111", 1014 Title: "kernel: break build", 1015 Author: "hacker@kernel.org", 1016 CC: []string{"reviewer1@kernel.org"}, 1017 Date: time.Date(2000, 2, 9, 4, 5, 6, 7, time.UTC), 1018 }, 1019 }, 1020 } 1021 c.client.expectOK(c.client.JobDone(done)) 1022 c.client.pollBug() 1023 1024 // Yet newer: no repro. 1025 c.advanceTime(24 * 7 * time.Hour) 1026 crashNew := testCrash(build, 1) 1027 crashNew.Title = crashTitle 1028 c.client.ReportCrash(crashNew) 1029 1030 // And yet newer. 1031 c.advanceTime(24 * time.Hour) 1032 crashNew2 := testCrash(build, 1) 1033 crashNew2.Title = crashTitle 1034 crashNew2.Report = []byte("newest") 1035 c.client.ReportCrash(crashNew2) 1036 1037 // Also create a bug in another namespace. 1038 otherBuild := testBuild(2) 1039 c.client2.UploadBuild(otherBuild) 1040 1041 otherCrash := testCrash(otherBuild, 1) 1042 otherCrash.Title = crashTitle 1043 otherCrash.ReproOpts = []byte("repro opts") 1044 otherCrash.ReproSyz = []byte("repro syz") 1045 c.client2.ReportCrash(otherCrash) 1046 otherPollMsg := c.client2.pollEmailBug() 1047 1048 _, err := c.POST("/_ah/mail/", fmt.Sprintf(`Sender: syzkaller@googlegroups.com 1049 Date: Tue, 15 Aug 2017 14:59:00 -0700 1050 Message-ID: <1234> 1051 Subject: crash1 1052 From: %v 1053 To: foo@bar.com 1054 Content-Type: text/plain 1055 1056 This email is only needed to capture the link 1057 -- 1058 To post to this group, send email to syzkaller@googlegroups.com. 1059 To view this discussion on the web visit https://groups.google.com/d/msgid/syzkaller/1234@google.com. 1060 For more options, visit https://groups.google.com/d/optout. 1061 `, otherPollMsg.Sender)) 1062 c.expectOK(err) 1063 1064 _, otherExtBugID, _ := email.RemoveAddrContext(otherPollMsg.Sender) 1065 1066 // Query the full bug info. 1067 info, err := c.client.LoadFullBug(&dashapi.LoadFullBugReq{BugID: rep.ID}) 1068 c.expectOK(err) 1069 if info.BisectCause == nil { 1070 t.Fatalf("info.BisectCause is empty") 1071 } 1072 if info.BisectCause.BisectCause == nil { 1073 t.Fatalf("info.BisectCause.BisectCause is empty") 1074 } 1075 c.expectEQ(info.SimilarBugs, []*dashapi.SimilarBugInfo{{ 1076 Title: crashTitle, 1077 Namespace: "test2", 1078 Status: dashapi.BugStatusOpen, 1079 ReproLevel: dashapi.ReproLevelSyz, 1080 Link: "https://testapp.appspot.com/bug?extid=" + otherExtBugID, 1081 ReportLink: "https://groups.google.com/d/msgid/syzkaller/1234@google.com", 1082 }}) 1083 1084 // There must be 3 crashes. 1085 reportsOrder := [][]byte{[]byte("newest"), []byte("with repro"), []byte("with strace")} 1086 c.expectEQ(len(info.Crashes), len(reportsOrder)) 1087 for i, report := range reportsOrder { 1088 c.expectEQ(info.Crashes[i].Report, report) 1089 } 1090 } 1091 1092 func TestUpdateReportApi(t *testing.T) { 1093 c := NewCtx(t) 1094 defer c.Close() 1095 1096 build := testBuild(1) 1097 c.client.UploadBuild(build) 1098 1099 // Report a crash. 1100 c.client.ReportCrash(testCrashWithRepro(build, 1)) 1101 c.client.pollBug() 1102 1103 listResp, err := c.client.BugList() 1104 c.expectOK(err) 1105 c.expectEQ(len(listResp.List), 1) 1106 1107 // Load the bug info. 1108 bugID := listResp.List[0] 1109 rep, err := c.client.LoadBug(bugID) 1110 c.expectOK(err) 1111 1112 // Now update the crash. 1113 setGuiltyFiles := []string{"fs/a.c", "net/b.c"} 1114 err = c.client.UpdateReport(&dashapi.UpdateReportReq{ 1115 BugID: bugID, 1116 CrashID: rep.CrashID, 1117 GuiltyFiles: &setGuiltyFiles, 1118 }) 1119 c.expectOK(err) 1120 1121 // And make sure it's been updated. 1122 ret, err := c.client.LoadBug(bugID) 1123 if err != nil { 1124 t.Fatal(err) 1125 } 1126 if ret.ReportElements == nil { 1127 t.Fatalf("ReportElements is nil") 1128 } 1129 if diff := cmp.Diff(ret.ReportElements.GuiltyFiles, setGuiltyFiles); diff != "" { 1130 t.Fatal(diff) 1131 } 1132 } 1133 1134 func TestReportDecommissionedBugs(t *testing.T) { 1135 c := NewCtx(t) 1136 defer c.Close() 1137 1138 client := c.makeClient(clientTestDecomm, keyTestDecomm, true) 1139 build := testBuild(1) 1140 client.UploadBuild(build) 1141 1142 crash := testCrash(build, 1) 1143 client.ReportCrash(crash) 1144 rep := client.pollBug() 1145 1146 closed, _ := client.ReportingPollClosed([]string{rep.ID}) 1147 c.expectEQ(len(closed), 0) 1148 1149 // And now let's decommission the namespace. 1150 c.decommission(rep.Namespace) 1151 1152 closed, _ = client.ReportingPollClosed([]string{rep.ID}) 1153 c.expectEQ(len(closed), 1) 1154 c.expectEQ(closed[0], rep.ID) 1155 } 1156 1157 func TestObsoletePeriod(t *testing.T) { 1158 base := time.Now() 1159 c := context.Background() 1160 config := getConfig(c) 1161 tests := []struct { 1162 name string 1163 bug *Bug 1164 period time.Duration 1165 }{ 1166 { 1167 name: "frequent final bug", 1168 bug: &Bug{ 1169 // Once in a day. 1170 NumCrashes: 30, 1171 FirstTime: base, 1172 LastTime: base.Add(time.Hour * 24 * 30), 1173 Reporting: []BugReporting{{Reported: base}}, 1174 }, 1175 // 80 days are definitely enough. 1176 period: config.Obsoleting.MinPeriod, 1177 }, 1178 { 1179 name: "very short-living final bug", 1180 bug: &Bug{ 1181 NumCrashes: 5, 1182 FirstTime: base, 1183 LastTime: base.Add(time.Hour * 24), 1184 Reporting: []BugReporting{{Reported: base}}, 1185 }, 1186 // Too few crashes, wait max time. 1187 period: config.Obsoleting.MaxPeriod, 1188 }, 1189 { 1190 name: "rare stable final bug", 1191 bug: &Bug{ 1192 // Once in 20 days. 1193 NumCrashes: 20, 1194 FirstTime: base, 1195 LastTime: base.Add(time.Hour * 24 * 400), 1196 Reporting: []BugReporting{{Reported: base}}, 1197 }, 1198 // Wait max time. 1199 period: config.Obsoleting.MaxPeriod, 1200 }, 1201 { 1202 name: "frequent non-final bug", 1203 bug: &Bug{ 1204 // Once in a day. 1205 NumCrashes: 10, 1206 FirstTime: base, 1207 LastTime: base.Add(time.Hour * 24 * 10), 1208 Reporting: []BugReporting{{}}, 1209 }, 1210 // 40 days are also enough. 1211 period: config.Obsoleting.NonFinalMinPeriod, 1212 }, 1213 } 1214 1215 for _, test := range tests { 1216 test := test 1217 t.Run(test.name, func(t *testing.T) { 1218 ret := test.bug.obsoletePeriod(c) 1219 assert.Equal(t, test.period, ret) 1220 }) 1221 } 1222 } 1223 1224 func TestReportRevokedRepro(t *testing.T) { 1225 // There was a bug (#4412) where syzbot infinitely re-reported reproducers 1226 // for a bug that was upstreamed after its repro was revoked. 1227 // Recreate this situation. 1228 c := NewCtx(t) 1229 defer c.Close() 1230 1231 client := c.makeClient(clientPublic, keyPublic, true) 1232 build := testBuild(1) 1233 build.KernelRepo = "git://mygit.com/git.git" 1234 build.KernelBranch = "main" 1235 client.UploadBuild(build) 1236 1237 crash := testCrash(build, 1) 1238 crash.ReproOpts = []byte("repro opts") 1239 crash.ReproSyz = []byte("repro syz") 1240 client.ReportCrash(crash) 1241 rep1 := client.pollBug() 1242 client.expectNE(rep1.ReproSyz, nil) 1243 1244 // Revoke the reproducer. 1245 c.advanceTime(c.config().Obsoleting.ReproRetestStart + time.Hour) 1246 jobResp := client.pollSpecificJobs(build.Manager, dashapi.ManagerJobs{TestPatches: true}) 1247 c.expectEQ(jobResp.Type, dashapi.JobTestPatch) 1248 client.expectOK(client.JobDone(&dashapi.JobDoneReq{ 1249 ID: jobResp.ID, 1250 })) 1251 1252 c.advanceTime(time.Hour) 1253 client.ReportCrash(testCrash(build, 1)) 1254 1255 // Upstream the bug. 1256 c.advanceTime(time.Hour) 1257 client.updateBug(rep1.ID, dashapi.BugStatusUpstream, "") 1258 rep2 := client.pollBug() 1259 1260 // Also ensure that we do not report the revoked reproducer. 1261 client.expectEQ(rep2.Type, dashapi.ReportNew) 1262 client.expectEQ(rep2.ReproSyz, []byte(nil)) 1263 1264 // Expect no further reports. 1265 client.pollBugs(0) 1266 } 1267 1268 func TestWaitForRepro(t *testing.T) { 1269 c := NewCtx(t) 1270 defer c.Close() 1271 1272 client := c.client 1273 c.setWaitForRepro("test1", time.Hour*24) 1274 1275 build := testBuild(1) 1276 client.UploadBuild(build) 1277 1278 // Normal crash witout repro. 1279 client.ReportCrash(testCrash(build, 1)) 1280 client.pollBugs(0) 1281 c.advanceTime(time.Hour * 24) 1282 client.pollBug() 1283 1284 // A crash first without repro, then with it. 1285 client.ReportCrash(testCrash(build, 2)) 1286 c.advanceTime(time.Hour * 12) 1287 client.pollBugs(0) 1288 client.ReportCrash(testCrashWithRepro(build, 2)) 1289 client.pollBug() 1290 1291 // A crash with a reproducer. 1292 c.advanceTime(time.Minute) 1293 client.ReportCrash(testCrashWithRepro(build, 3)) 1294 client.pollBug() 1295 1296 // A crahs that will never have a reproducer. 1297 c.advanceTime(time.Minute) 1298 crash := testCrash(build, 4) 1299 crash.Title = "upstream test error: abcd" 1300 client.ReportCrash(crash) 1301 client.pollBug() 1302 }