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