github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/dashboard/app/repro_test.go (about) 1 // Copyright 2018 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 "fmt" 8 "net/http" 9 "testing" 10 "time" 11 12 "github.com/google/syzkaller/dashboard/dashapi" 13 ) 14 15 // Normal workflow: 16 // - upload crash -> need repro 17 // - upload syz repro -> still need repro 18 // - upload C repro -> don't need repro 19 func testNeedRepro1(t *testing.T, crashCtor func(c *Ctx) *dashapi.Crash, newBug bool) { 20 c := NewCtx(t) 21 defer c.Close() 22 23 crash1 := crashCtor(c) 24 resp, _ := c.client.ReportCrash(crash1) 25 c.expectEQ(resp.NeedRepro, true) 26 27 cid := testCrashID(crash1) 28 needRepro, _ := c.client.NeedRepro(cid) 29 c.expectEQ(needRepro, true) 30 31 // Still need repro for this crash. 32 resp, _ = c.client.ReportCrash(crash1) 33 c.expectEQ(resp.NeedRepro, true) 34 needRepro, _ = c.client.NeedRepro(cid) 35 c.expectEQ(needRepro, true) 36 37 crash2 := new(dashapi.Crash) 38 *crash2 = *crash1 39 crash2.ReproOpts = []byte("opts") 40 crash2.ReproSyz = []byte("repro syz") 41 resp, _ = c.client.ReportCrash(crash2) 42 c.expectEQ(resp.NeedRepro, true) 43 needRepro, _ = c.client.NeedRepro(cid) 44 c.expectEQ(needRepro, true) 45 46 // MayBeMissing flag must not affect bugs that actually exist. 47 cidMissing := testCrashID(crash1) 48 cidMissing.MayBeMissing = true 49 needRepro, _ = c.client.NeedRepro(cidMissing) 50 c.expectEQ(needRepro, true) 51 52 crash2.ReproC = []byte("repro C") 53 resp, _ = c.client.ReportCrash(crash2) 54 c.expectEQ(resp.NeedRepro, false) 55 needRepro, _ = c.client.NeedRepro(cid) 56 c.expectEQ(needRepro, false) 57 58 needRepro, _ = c.client.NeedRepro(cidMissing) 59 c.expectEQ(needRepro, false) 60 61 resp, _ = c.client.ReportCrash(crash2) 62 c.expectEQ(resp.NeedRepro, false) 63 if newBug { 64 c.client.pollBug() 65 } 66 } 67 68 func TestNeedRepro1_normal(t *testing.T) { testNeedRepro1(t, normalCrash, true) } 69 func TestNeedRepro1_dup(t *testing.T) { testNeedRepro1(t, dupCrash, false) } 70 func TestNeedRepro1_closed(t *testing.T) { testNeedRepro1(t, closedCrash, true) } 71 func TestNeedRepro1_closedRepro(t *testing.T) { testNeedRepro1(t, closedWithReproCrash, true) } 72 73 // Upload C repro with first crash -> don't need repro. 74 func testNeedRepro2(t *testing.T, crashCtor func(c *Ctx) *dashapi.Crash, newBug bool) { 75 c := NewCtx(t) 76 defer c.Close() 77 78 crash1 := crashCtor(c) 79 crash1.ReproOpts = []byte("opts") 80 crash1.ReproSyz = []byte("repro syz") 81 crash1.ReproC = []byte("repro C") 82 resp, _ := c.client.ReportCrash(crash1) 83 c.expectEQ(resp.NeedRepro, false) 84 85 needRepro, _ := c.client.NeedRepro(testCrashID(crash1)) 86 c.expectEQ(needRepro, false) 87 if newBug { 88 c.client.pollBug() 89 } 90 } 91 92 func TestNeedRepro2_normal(t *testing.T) { testNeedRepro2(t, normalCrash, true) } 93 func TestNeedRepro2_dup(t *testing.T) { testNeedRepro2(t, dupCrash, false) } 94 func TestNeedRepro2_closed(t *testing.T) { testNeedRepro2(t, closedCrash, true) } 95 func TestNeedRepro2_closedRepro(t *testing.T) { testNeedRepro2(t, closedWithReproCrash, true) } 96 97 // Test that after uploading 5 failed repros, app stops requesting repros. 98 func testNeedRepro3(t *testing.T, crashCtor func(c *Ctx) *dashapi.Crash) { 99 c := NewCtx(t) 100 defer c.Close() 101 102 crash1 := crashCtor(c) 103 for i := 0; i < maxReproPerBug; i++ { 104 resp, _ := c.client.ReportCrash(crash1) 105 c.expectEQ(resp.NeedRepro, true) 106 needRepro, _ := c.client.NeedRepro(testCrashID(crash1)) 107 c.expectEQ(needRepro, true) 108 c.client.ReportFailedRepro(testCrashID(crash1)) 109 } 110 111 for i := 0; i < 3; i++ { 112 // No more repros today. 113 c.advanceTime(time.Hour) 114 resp, _ := c.client.ReportCrash(crash1) 115 c.expectEQ(resp.NeedRepro, false) 116 needRepro, _ := c.client.NeedRepro(testCrashID(crash1)) 117 c.expectEQ(needRepro, false) 118 119 // Then another repro after a day. 120 c.advanceTime(25 * time.Hour) 121 for j := 0; j < 2; j++ { 122 resp, _ := c.client.ReportCrash(crash1) 123 c.expectEQ(resp.NeedRepro, true) 124 needRepro, _ := c.client.NeedRepro(testCrashID(crash1)) 125 c.expectEQ(needRepro, true) 126 } 127 c.client.ReportFailedRepro(testCrashID(crash1)) 128 } 129 } 130 131 func TestNeedRepro3_normal(t *testing.T) { testNeedRepro3(t, normalCrash) } 132 func TestNeedRepro3_dup(t *testing.T) { testNeedRepro3(t, dupCrash) } 133 func TestNeedRepro3_closed(t *testing.T) { testNeedRepro3(t, closedCrash) } 134 func TestNeedRepro3_closedRepro(t *testing.T) { testNeedRepro3(t, closedWithReproCrash) } 135 136 func normalCrash(c *Ctx) *dashapi.Crash { 137 build := testBuild(1) 138 c.client.UploadBuild(build) 139 crash := testCrash(build, 1) 140 c.client.ReportCrash(crash) 141 c.client.pollBug() 142 return crash 143 } 144 145 func dupCrash(c *Ctx) *dashapi.Crash { 146 build := testBuild(1) 147 c.client.UploadBuild(build) 148 c.client.ReportCrash(testCrash(build, 1)) 149 crash2 := testCrash(build, 2) 150 c.client.ReportCrash(crash2) 151 reports := c.client.pollBugs(2) 152 c.client.updateBug(reports[1].ID, dashapi.BugStatusDup, reports[0].ID) 153 return crash2 154 } 155 156 func closedCrash(c *Ctx) *dashapi.Crash { 157 return closedCrashImpl(c, false) 158 } 159 160 func closedWithReproCrash(c *Ctx) *dashapi.Crash { 161 return closedCrashImpl(c, true) 162 } 163 164 func closedCrashImpl(c *Ctx, withRepro bool) *dashapi.Crash { 165 build := testBuild(1) 166 c.client.UploadBuild(build) 167 168 crash := testCrash(build, 1) 169 if withRepro { 170 crash.ReproC = []byte("repro C") 171 } 172 resp, _ := c.client.ReportCrash(crash) 173 c.expectEQ(resp.NeedRepro, !withRepro) 174 175 rep := c.client.pollBug() 176 c.client.updateBug(rep.ID, dashapi.BugStatusInvalid, "") 177 178 crash.ReproC = nil 179 c.client.ReportCrash(crash) 180 c.client.pollBug() 181 return crash 182 } 183 184 func TestNeedReproMissing(t *testing.T) { 185 c := NewCtx(t) 186 defer c.Close() 187 188 client := c.makeClient(client1, password1, false) 189 190 cid := &dashapi.CrashID{ 191 BuildID: "some missing build", 192 Title: "some missing title", 193 } 194 needRepro, err := client.NeedRepro(cid) 195 c.expectNE(err, nil) 196 c.expectEQ(needRepro, false) 197 198 cid.MayBeMissing = true 199 needRepro, err = client.NeedRepro(cid) 200 c.expectEQ(err, nil) 201 c.expectEQ(needRepro, true) 202 } 203 204 // In addition to the above, do a number of quick tests of the needReproForBug function. 205 func TestNeedReproIsolated(t *testing.T) { 206 c := NewCtx(t) 207 defer c.Close() 208 209 nowTime := c.mockedTime 210 tests := []struct { 211 bug *Bug 212 needRepro bool 213 }{ 214 { 215 // A bug without a repro. 216 bug: &Bug{ 217 Title: "normal bug without a repro", 218 }, 219 needRepro: true, 220 }, 221 { 222 // Corrupted bug. 223 bug: &Bug{ 224 Title: corruptedReportTitle, 225 }, 226 needRepro: false, 227 }, 228 { 229 // Suppressed bug. 230 bug: &Bug{ 231 Title: suppressedReportTitle, 232 }, 233 needRepro: false, 234 }, 235 { 236 // A bug without a C repro. 237 bug: &Bug{ 238 Title: "some normal bug with a syz-repro", 239 ReproLevel: ReproLevelSyz, 240 HeadReproLevel: ReproLevelSyz, 241 }, 242 needRepro: true, 243 }, 244 { 245 // A bug for which we have recently found a repro. 246 bug: &Bug{ 247 Title: "some normal recent bug", 248 ReproLevel: ReproLevelC, 249 HeadReproLevel: ReproLevelC, 250 LastReproTime: nowTime.Add(-time.Hour * 24), 251 }, 252 needRepro: false, 253 }, 254 { 255 // A bug which has an old C repro. 256 bug: &Bug{ 257 Title: "some normal bug with old repro", 258 ReproLevel: ReproLevelC, 259 HeadReproLevel: ReproLevelC, 260 NumRepro: 2 * maxReproPerBug, 261 LastReproTime: nowTime.Add(-reproStalePeriod), 262 }, 263 needRepro: true, 264 }, 265 { 266 // Several failed repro attepts are OK. 267 bug: &Bug{ 268 Title: "some normal bug with several fails", 269 NumRepro: maxReproPerBug - 1, 270 LastReproTime: nowTime, 271 }, 272 needRepro: true, 273 }, 274 { 275 // ... but there are limits. 276 bug: &Bug{ 277 Title: "some normal bug with too much fails", 278 NumRepro: maxReproPerBug, 279 LastReproTime: nowTime, 280 }, 281 needRepro: false, 282 }, 283 { 284 // Make sure we try until we find a C repro, not just a syz repro. 285 bug: &Bug{ 286 Title: "too many fails, but only a syz repro", 287 ReproLevel: ReproLevelSyz, 288 HeadReproLevel: ReproLevelSyz, 289 NumRepro: maxReproPerBug, 290 LastReproTime: nowTime.Add(-24 * time.Hour), 291 }, 292 needRepro: true, 293 }, 294 { 295 // We don't need a C repro for SYZFATAL: bugs. 296 bug: &Bug{ 297 Title: "SYZFATAL: Manager.Check call failed", 298 ReproLevel: ReproLevelSyz, 299 HeadReproLevel: ReproLevelSyz, 300 LastReproTime: nowTime.Add(-24 * time.Hour), 301 }, 302 needRepro: false, 303 }, 304 { 305 // .. and for SYZFAIL: bugs. 306 bug: &Bug{ 307 Title: "SYZFAIL: clock_gettime failed", 308 ReproLevel: ReproLevelSyz, 309 HeadReproLevel: ReproLevelSyz, 310 LastReproTime: nowTime.Add(-24 * time.Hour), 311 }, 312 needRepro: false, 313 }, 314 { 315 // Yet make sure that we request at least a syz repro. 316 bug: &Bug{ 317 Title: "SYZFATAL: Manager.Check call failed", 318 }, 319 needRepro: true, 320 }, 321 { 322 // A bug with a revoked repro. 323 bug: &Bug{ 324 Title: "some normal bug with a syz-repro", 325 ReproLevel: ReproLevelC, 326 HeadReproLevel: ReproLevelSyz, 327 LastReproTime: nowTime.Add(-24 * time.Hour), 328 }, 329 needRepro: true, 330 }, 331 } 332 333 for _, test := range tests { 334 bug := test.bug 335 if bug.Namespace == "" { 336 bug.Namespace = "test1" 337 } 338 funcResult := needReproForBug(c.ctx, bug) 339 if funcResult != test.needRepro { 340 t.Errorf("for %#v expected needRepro=%v, got needRepro=%v", 341 bug, test.needRepro, funcResult) 342 } 343 } 344 } 345 346 func TestFailedReproLogs(t *testing.T) { 347 c := NewCtx(t) 348 defer c.Close() 349 350 build := testBuild(1) 351 c.client.UploadBuild(build) 352 353 crash1 := &dashapi.Crash{ 354 BuildID: "build1", 355 Title: "title1", 356 Log: []byte("log1"), 357 Report: []byte("report1"), 358 } 359 c.client.ReportCrash(crash1) 360 361 resp, _ := c.client.ReportingPollBugs("test") 362 c.expectEQ(len(resp.Reports), 1) 363 rep := resp.Reports[0] 364 c.client.ReportingUpdate(&dashapi.BugUpdate{ 365 ID: rep.ID, 366 Status: dashapi.BugStatusOpen, 367 }) 368 369 // Report max attempts. 370 cid := &dashapi.CrashID{ 371 BuildID: crash1.BuildID, 372 Title: crash1.Title, 373 } 374 for i := 0; i < maxReproLogs; i++ { 375 c.advanceTime(time.Minute) 376 cid.ReproLog = []byte(fmt.Sprintf("report log %#v", i)) 377 err := c.client.ReportFailedRepro(cid) 378 c.expectOK(err) 379 } 380 381 dbBug, _, _ := c.loadBug(rep.ID) 382 firstRecords := dbBug.ReproAttempts 383 c.expectEQ(len(firstRecords), maxReproLogs) 384 385 // Report one more. 386 cid.ReproLog = []byte(fmt.Sprintf("report log %#v", maxReproLogs)) 387 err := c.client.ReportFailedRepro(cid) 388 c.expectOK(err) 389 390 dbBug, _, _ = c.loadBug(rep.ID) 391 lastRecords := dbBug.ReproAttempts 392 c.expectEQ(len(firstRecords), maxReproLogs) 393 394 // Ensure the first record was dropped. 395 checkResponseStatusCode(c, AccessAdmin, 396 textLink(textReproLog, firstRecords[0].Log), http.StatusNotFound) 397 398 // Ensure that the second record is readable. 399 reply, err := c.AuthGET(AccessAdmin, textLink(textReproLog, lastRecords[0].Log)) 400 c.expectOK(err) 401 c.expectEQ(reply, []byte("report log 1")) 402 } 403 404 func TestLogToReproduce(t *testing.T) { 405 c := NewCtx(t) 406 defer c.Close() 407 client := c.client 408 409 build := testBuild(1) 410 client.UploadBuild(build) 411 412 // Also add some unrelated crash, which should not appear in responses. 413 build2 := testBuild(2) 414 client.UploadBuild(build2) 415 client.ReportCrash(testCrash(build2, 3)) 416 client.pollBug() 417 418 // Bug with a reproducer. 419 crash1 := testCrashWithRepro(build, 1) 420 client.ReportCrash(crash1) 421 client.pollBug() 422 resp, err := client.LogToRepro(&dashapi.LogToReproReq{BuildID: "build1"}) 423 c.expectOK(err) 424 c.expectEQ(resp.CrashLog, []byte(nil)) 425 426 // Bug without a reproducer. 427 crash2 := &dashapi.Crash{ 428 BuildID: "build1", 429 Title: "title2", 430 Log: []byte("log2"), 431 Report: []byte("report2"), 432 } 433 client.ReportCrash(crash2) 434 client.pollBug() 435 resp, err = client.LogToRepro(&dashapi.LogToReproReq{BuildID: "build1"}) 436 c.expectOK(err) 437 c.expectEQ(resp.Title, "title2") 438 c.expectEQ(resp.CrashLog, []byte("log2")) 439 440 // Suppose we tried to find a repro, but failed. 441 err = client.ReportFailedRepro(&dashapi.CrashID{ 442 BuildID: crash2.BuildID, 443 Title: crash2.Title, 444 ReproLog: []byte("abcd"), 445 }) 446 c.expectOK(err) 447 448 // Now this crash should not be suggested. 449 resp, err = client.LogToRepro(&dashapi.LogToReproReq{BuildID: "build1"}) 450 c.expectOK(err) 451 c.expectEQ(resp.CrashLog, []byte(nil)) 452 } 453 454 // A frequent case -- when trying to find a reproducer for one bug, 455 // we have found a reproducer for a different bug. 456 // We want to remember the reproduction log in this case. 457 func TestReproForDifferentCrash(t *testing.T) { 458 c := NewCtx(t) 459 defer c.Close() 460 461 client := c.client 462 build := testBuild(1) 463 client.UploadBuild(build) 464 465 // Original crash. 466 crash := &dashapi.Crash{ 467 BuildID: "build1", 468 Title: "title1", 469 Log: []byte("log1"), 470 Report: []byte("report1"), 471 } 472 client.ReportCrash(crash) 473 oldBug := client.pollBug() 474 475 // Now we have "found" a reproducer with a different title. 476 crash.Title = "new title" 477 crash.ReproOpts = []byte("opts") 478 crash.ReproSyz = []byte("repro syz") 479 crash.ReproLog = []byte("repro log") 480 crash.OriginalTitle = "title1" 481 client.ReportCrash(crash) 482 client.pollBug() 483 484 // Ensure that we have saved the reproduction log in this case. 485 dbBug, _, _ := c.loadBug(oldBug.ID) 486 c.expectEQ(len(dbBug.ReproAttempts), 1) 487 }