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  }