github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/pkg/report/report_test.go (about)

     1  // Copyright 2015 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 report
     5  
     6  import (
     7  	"bufio"
     8  	"bytes"
     9  	"flag"
    10  	"fmt"
    11  	"os"
    12  	"path/filepath"
    13  	"reflect"
    14  	"regexp"
    15  	"sort"
    16  	"strings"
    17  	"testing"
    18  
    19  	"github.com/google/syzkaller/pkg/mgrconfig"
    20  	"github.com/google/syzkaller/pkg/osutil"
    21  	"github.com/google/syzkaller/pkg/report/crash"
    22  	"github.com/google/syzkaller/pkg/testutil"
    23  	"github.com/google/syzkaller/sys/targets"
    24  	"github.com/stretchr/testify/assert"
    25  )
    26  
    27  var flagUpdate = flag.Bool("update", false, "update test files accordingly to current results")
    28  
    29  func TestParse(t *testing.T) {
    30  	forEachFile(t, "report", testParseFile)
    31  }
    32  
    33  type ParseTest struct {
    34  	FileName   string
    35  	Log        []byte
    36  	Title      string
    37  	AltTitles  []string
    38  	Type       crash.Type
    39  	Frame      string
    40  	StartLine  string
    41  	EndLine    string
    42  	Corrupted  bool
    43  	Suppressed bool
    44  	HasReport  bool
    45  	Report     []byte
    46  	Executor   string
    47  	// Only used in report parsing:
    48  	corruptedReason string
    49  }
    50  
    51  func (test *ParseTest) Equal(other *ParseTest) bool {
    52  	if test.Title != other.Title ||
    53  		test.Corrupted != other.Corrupted ||
    54  		test.Suppressed != other.Suppressed ||
    55  		test.Type != other.Type {
    56  		return false
    57  	}
    58  	if !reflect.DeepEqual(test.AltTitles, other.AltTitles) {
    59  		return false
    60  	}
    61  	if test.Frame != "" && test.Frame != other.Frame {
    62  		return false
    63  	}
    64  	if test.HasReport && !bytes.Equal(test.Report, other.Report) {
    65  		return false
    66  	}
    67  	return test.Executor == other.Executor
    68  }
    69  
    70  func (test *ParseTest) Headers() []byte {
    71  	buf := new(bytes.Buffer)
    72  	fmt.Fprintf(buf, "TITLE: %v\n", test.Title)
    73  	for _, t := range test.AltTitles {
    74  		fmt.Fprintf(buf, "ALT: %v\n", t)
    75  	}
    76  	if test.Type != crash.UnknownType {
    77  		fmt.Fprintf(buf, "TYPE: %v\n", test.Type)
    78  	}
    79  	if test.Frame != "" {
    80  		fmt.Fprintf(buf, "FRAME: %v\n", test.Frame)
    81  	}
    82  	if test.Corrupted {
    83  		fmt.Fprintf(buf, "CORRUPTED: Y\n")
    84  	}
    85  	if test.Suppressed {
    86  		fmt.Fprintf(buf, "SUPPRESSED: Y\n")
    87  	}
    88  	if test.Executor != "" {
    89  		fmt.Fprintf(buf, "EXECUTOR: %s\n", test.Executor)
    90  	}
    91  	return buf.Bytes()
    92  }
    93  
    94  func testParseFile(t *testing.T, reporter *Reporter, fn string) {
    95  	test := parseReport(t, reporter, fn)
    96  	testParseImpl(t, reporter, test)
    97  }
    98  
    99  func parseReport(t *testing.T, reporter *Reporter, fn string) *ParseTest {
   100  	data, err := os.ReadFile(fn)
   101  	if err != nil {
   102  		t.Fatal(err)
   103  	}
   104  	// Strip all \r from reports because the merger removes it.
   105  	data = bytes.ReplaceAll(data, []byte{'\r'}, nil)
   106  	const (
   107  		phaseHeaders = iota
   108  		phaseLog
   109  		phaseReport
   110  	)
   111  	phase := phaseHeaders
   112  	test := &ParseTest{
   113  		FileName: fn,
   114  	}
   115  	prevEmptyLine := false
   116  	s := bufio.NewScanner(bytes.NewReader(data))
   117  	for s.Scan() {
   118  		switch phase {
   119  		case phaseHeaders:
   120  			ln := s.Text()
   121  			if ln == "" {
   122  				phase = phaseLog
   123  				continue
   124  			}
   125  			parseHeaderLine(t, test, ln)
   126  		case phaseLog:
   127  			if prevEmptyLine && string(s.Bytes()) == "REPORT:" {
   128  				test.HasReport = true
   129  				phase = phaseReport
   130  			} else {
   131  				test.Log = append(test.Log, s.Bytes()...)
   132  				test.Log = append(test.Log, '\n')
   133  			}
   134  		case phaseReport:
   135  			test.Report = append(test.Report, s.Bytes()...)
   136  			test.Report = append(test.Report, '\n')
   137  		}
   138  		prevEmptyLine = len(s.Bytes()) == 0
   139  	}
   140  	if s.Err() != nil {
   141  		t.Fatalf("file scanning error: %v", s.Err())
   142  	}
   143  	if len(test.Log) == 0 {
   144  		t.Fatalf("can't find log in input file")
   145  	}
   146  	sort.Strings(test.AltTitles)
   147  	return test
   148  }
   149  
   150  func parseHeaderLine(t *testing.T, test *ParseTest, ln string) {
   151  	const (
   152  		titlePrefix      = "TITLE: "
   153  		altTitlePrefix   = "ALT: "
   154  		typePrefix       = "TYPE: "
   155  		framePrefix      = "FRAME: "
   156  		startPrefix      = "START: "
   157  		endPrefix        = "END: "
   158  		corruptedPrefix  = "CORRUPTED: "
   159  		suppressedPrefix = "SUPPRESSED: "
   160  		executorPrefix   = "EXECUTOR: "
   161  	)
   162  	switch {
   163  	case strings.HasPrefix(ln, "#"):
   164  	case strings.HasPrefix(ln, titlePrefix):
   165  		test.Title = ln[len(titlePrefix):]
   166  	case strings.HasPrefix(ln, altTitlePrefix):
   167  		test.AltTitles = append(test.AltTitles, ln[len(altTitlePrefix):])
   168  	case strings.HasPrefix(ln, typePrefix):
   169  		test.Type = crash.Type(ln[len(typePrefix):])
   170  	case strings.HasPrefix(ln, framePrefix):
   171  		test.Frame = ln[len(framePrefix):]
   172  	case strings.HasPrefix(ln, startPrefix):
   173  		test.StartLine = ln[len(startPrefix):]
   174  	case strings.HasPrefix(ln, endPrefix):
   175  		test.EndLine = ln[len(endPrefix):]
   176  	case strings.HasPrefix(ln, corruptedPrefix):
   177  		switch v := ln[len(corruptedPrefix):]; v {
   178  		case "Y":
   179  			test.Corrupted = true
   180  		case "N":
   181  			test.Corrupted = false
   182  		default:
   183  			t.Fatalf("unknown CORRUPTED value %q", v)
   184  		}
   185  	case strings.HasPrefix(ln, suppressedPrefix):
   186  		switch v := ln[len(suppressedPrefix):]; v {
   187  		case "Y":
   188  			test.Suppressed = true
   189  		case "N":
   190  			test.Suppressed = false
   191  		default:
   192  			t.Fatalf("unknown SUPPRESSED value %q", v)
   193  		}
   194  	case strings.HasPrefix(ln, executorPrefix):
   195  		test.Executor = ln[len(executorPrefix):]
   196  	default:
   197  		t.Fatalf("unknown header field %q", ln)
   198  	}
   199  }
   200  
   201  func testFromReport(rep *Report) *ParseTest {
   202  	if rep == nil {
   203  		return &ParseTest{}
   204  	}
   205  	ret := &ParseTest{
   206  		Title:           rep.Title,
   207  		AltTitles:       rep.AltTitles,
   208  		Corrupted:       rep.Corrupted,
   209  		corruptedReason: rep.CorruptedReason,
   210  		Suppressed:      rep.Suppressed,
   211  		Type:            TitleToCrashType(rep.Title),
   212  		Frame:           rep.Frame,
   213  		Report:          rep.Report,
   214  	}
   215  	if rep.Executor != nil {
   216  		ret.Executor = fmt.Sprintf("proc=%d, id=%d", rep.Executor.ProcID, rep.Executor.ExecID)
   217  	}
   218  	sort.Strings(ret.AltTitles)
   219  	return ret
   220  }
   221  
   222  func testParseImpl(t *testing.T, reporter *Reporter, test *ParseTest) {
   223  	rep := reporter.Parse(test.Log)
   224  	containsCrash := reporter.ContainsCrash(test.Log)
   225  	expectCrash := (test.Title != "")
   226  	if expectCrash && !containsCrash {
   227  		t.Fatalf("did not find crash")
   228  	}
   229  	if !expectCrash && containsCrash {
   230  		t.Fatalf("found unexpected crash")
   231  	}
   232  	if rep != nil && rep.Title == "" {
   233  		t.Fatalf("found crash, but title is empty")
   234  	}
   235  	parsed := testFromReport(rep)
   236  	if !test.Equal(parsed) {
   237  		if *flagUpdate && test.StartLine+test.EndLine == "" {
   238  			updateReportTest(t, test, parsed)
   239  		}
   240  		t.Fatalf("want:\n%s\ngot:\n%sCorrupted reason: %q",
   241  			test.Headers(), parsed.Headers(), parsed.corruptedReason)
   242  	}
   243  	if parsed.Title != "" && len(rep.Report) == 0 {
   244  		t.Fatalf("found crash message but report is empty")
   245  	}
   246  	if rep == nil {
   247  		return
   248  	}
   249  	checkReport(t, reporter, rep, test)
   250  }
   251  
   252  func checkReport(t *testing.T, reporter *Reporter, rep *Report, test *ParseTest) {
   253  	if test.HasReport && !bytes.Equal(rep.Report, test.Report) {
   254  		t.Fatalf("extracted wrong report:\n%s\nwant:\n%s", rep.Report, test.Report)
   255  	}
   256  	if !bytes.Equal(rep.Output, test.Log) {
   257  		t.Fatalf("bad Output:\n%s", rep.Output)
   258  	}
   259  	if rep.StartPos != 0 && rep.EndPos != 0 && rep.StartPos >= rep.EndPos {
   260  		t.Fatalf("StartPos %v >= EndPos %v", rep.StartPos, rep.EndPos)
   261  	}
   262  	if rep.EndPos > len(rep.Output) {
   263  		t.Fatalf("EndPos %v > len(Output) %v", rep.EndPos, len(rep.Output))
   264  	}
   265  	if rep.SkipPos <= rep.StartPos || rep.SkipPos > rep.EndPos {
   266  		t.Fatalf("bad SkipPos %v: StartPos %v EndPos %v", rep.SkipPos, rep.StartPos, rep.EndPos)
   267  	}
   268  	if test.StartLine != "" {
   269  		if test.EndLine == "" {
   270  			test.EndLine = test.StartLine
   271  		}
   272  		startPos := bytes.Index(test.Log, []byte(test.StartLine))
   273  		endPos := bytes.Index(test.Log, []byte(test.EndLine)) + len(test.EndLine)
   274  		if rep.StartPos != startPos || rep.EndPos != endPos {
   275  			t.Fatalf("bad start/end pos %v-%v, want %v-%v, line %q",
   276  				rep.StartPos, rep.EndPos, startPos, endPos,
   277  				string(test.Log[rep.StartPos:rep.EndPos]))
   278  		}
   279  	}
   280  	if rep.StartPos != 0 {
   281  		// If we parse from StartPos, we must find the same report.
   282  		rep1 := reporter.ParseFrom(test.Log, rep.StartPos)
   283  		if rep1 == nil || rep1.Title != rep.Title || rep1.StartPos != rep.StartPos {
   284  			t.Fatalf("did not find the same report from rep.StartPos=%v", rep.StartPos)
   285  		}
   286  		// If we parse from EndPos, we must not find the same report.
   287  		rep2 := reporter.ParseFrom(test.Log, rep.EndPos)
   288  		if rep2 != nil && rep2.Title == rep.Title {
   289  			t.Fatalf("found the same report after rep.EndPos=%v", rep.EndPos)
   290  		}
   291  	}
   292  }
   293  
   294  func updateReportTest(t *testing.T, test, parsed *ParseTest) {
   295  	buf := new(bytes.Buffer)
   296  	buf.Write(parsed.Headers())
   297  	fmt.Fprintf(buf, "\n%s", test.Log)
   298  	if test.HasReport {
   299  		fmt.Fprintf(buf, "REPORT:\n%s", parsed.Report)
   300  	}
   301  	if err := os.WriteFile(test.FileName, buf.Bytes(), 0640); err != nil {
   302  		t.Logf("failed to update test file: %v", err)
   303  	}
   304  }
   305  
   306  func TestGuiltyFile(t *testing.T) {
   307  	forEachFile(t, "guilty", testGuiltyFile)
   308  }
   309  
   310  func testGuiltyFile(t *testing.T, reporter *Reporter, fn string) {
   311  	vars, report := parseGuiltyTest(t, fn)
   312  	file := vars["FILE"]
   313  	rep := reporter.Parse(report)
   314  	if rep == nil {
   315  		t.Fatalf("did not find crash in the input")
   316  	}
   317  	// Parse doesn't generally run on already symbolized output,
   318  	// but here we run it on symbolized output because we can't symbolize in tests.
   319  	// The problem is with duplicated lines due to inlined frames,
   320  	// Parse can strip such report after first title line because it thinks
   321  	// that the duplicated title line is beginning on another report.
   322  	// In such case we restore whole report, but still keep StartPos that
   323  	// Parse produces at least in some cases.
   324  	if !bytes.HasSuffix(report, rep.Report) {
   325  		rep.Report = report
   326  		rep.StartPos = 0
   327  	}
   328  	if err := reporter.Symbolize(rep); err != nil {
   329  		t.Fatalf("failed to symbolize report: %v", err)
   330  	}
   331  	if rep.GuiltyFile != file {
   332  		t.Fatalf("got guilty %q, want %q", rep.GuiltyFile, file)
   333  	}
   334  }
   335  
   336  func TestRawGuiltyFile(t *testing.T) {
   337  	forEachFile(t, "guilty_raw", testRawGuiltyFile)
   338  }
   339  
   340  func testRawGuiltyFile(t *testing.T, reporter *Reporter, fn string) {
   341  	vars, report := parseGuiltyTest(t, fn)
   342  	outFile := reporter.ReportToGuiltyFile(vars["TITLE"], report)
   343  	if outFile != vars["FILE"] {
   344  		t.Fatalf("expected %#v, got %#v", vars["FILE"], outFile)
   345  	}
   346  }
   347  
   348  func parseGuiltyTest(t *testing.T, fn string) (map[string]string, []byte) {
   349  	data, err := os.ReadFile(fn)
   350  	if err != nil {
   351  		t.Fatal(err)
   352  	}
   353  	nlnl := bytes.Index(data, []byte{'\n', '\n'})
   354  	if nlnl == -1 {
   355  		t.Fatalf("no \\n\\n in file")
   356  	}
   357  	vars := map[string]string{}
   358  	s := bufio.NewScanner(bytes.NewReader(data[:nlnl]))
   359  	for s.Scan() {
   360  		ln := strings.TrimSpace(s.Text())
   361  		if ln == "" || ln[0] == '#' {
   362  			continue
   363  		}
   364  		colon := strings.IndexByte(ln, ':')
   365  		if colon == -1 {
   366  			t.Fatalf("no : in %s", ln)
   367  		}
   368  		vars[strings.TrimSpace(ln[:colon])] = strings.TrimSpace(ln[colon+1:])
   369  	}
   370  	return vars, data[nlnl+2:]
   371  }
   372  
   373  func TestSymbolize(t *testing.T) {
   374  	// We cannot fully test symbolization as we need kernel binaries with debug info, but
   375  	// let's at least test symbol demangling that's done as part of Symbolize().
   376  	forEachFile(t, "symbolize", testSymbolizeFile)
   377  }
   378  
   379  func testSymbolizeFile(t *testing.T, reporter *Reporter, fn string) {
   380  	test := parseReport(t, reporter, fn)
   381  	if !test.HasReport {
   382  		t.Fatalf("the test must have the REPORT section")
   383  	}
   384  	rep := reporter.Parse(test.Log)
   385  	if rep == nil {
   386  		t.Fatalf("did not find crash")
   387  	}
   388  	err := reporter.Symbolize(rep)
   389  	if err != nil {
   390  		t.Fatalf("failed to symbolize: %v", err)
   391  	}
   392  	parsed := testFromReport(rep)
   393  	if !test.Equal(parsed) {
   394  		if *flagUpdate {
   395  			updateReportTest(t, test, parsed)
   396  		}
   397  		assert.Equal(t, string(test.Report), string(rep.Report), "extracted wrong report")
   398  		t.Fatalf("want:\n%s\ngot:\n%sCorrupted reason: %q",
   399  			test.Headers(), parsed.Headers(), parsed.corruptedReason)
   400  	}
   401  }
   402  
   403  func forEachFile(t *testing.T, dir string, fn func(t *testing.T, reporter *Reporter, fn string)) {
   404  	for os := range ctors {
   405  		if os == targets.Windows {
   406  			continue // not implemented
   407  		}
   408  		cfg := &mgrconfig.Config{
   409  			Derived: mgrconfig.Derived{
   410  				TargetOS:   os,
   411  				TargetArch: targets.AMD64,
   412  				SysTarget:  targets.Get(os, targets.AMD64),
   413  			},
   414  		}
   415  		reporter, err := NewReporter(cfg)
   416  		if err != nil {
   417  			t.Fatal(err)
   418  		}
   419  		// There is little point in re-parsing all test files in race mode.
   420  		// Just make sure there are no obvious races by running few reports from "all" dir.
   421  		if !testutil.RaceEnabled {
   422  			for _, file := range readDir(t, filepath.Join("testdata", os, dir)) {
   423  				t.Run(fmt.Sprintf("%v/%v", os, filepath.Base(file)), func(t *testing.T) {
   424  					fn(t, reporter, file)
   425  				})
   426  			}
   427  		}
   428  		for _, file := range readDir(t, filepath.Join("testdata", "all", dir)) {
   429  			t.Run(fmt.Sprintf("%v/all/%v", os, filepath.Base(file)), func(t *testing.T) {
   430  				fn(t, reporter, file)
   431  			})
   432  		}
   433  	}
   434  }
   435  
   436  func readDir(t *testing.T, dir string) (files []string) {
   437  	if !osutil.IsExist(dir) {
   438  		return nil
   439  	}
   440  	entries, err := os.ReadDir(dir)
   441  	if err != nil {
   442  		t.Fatal(err)
   443  	}
   444  	testFilenameRe := regexp.MustCompile("^[0-9]+$")
   445  	for _, ent := range entries {
   446  		if !testFilenameRe.MatchString(ent.Name()) {
   447  			continue
   448  		}
   449  		files = append(files, filepath.Join(dir, ent.Name()))
   450  	}
   451  	return
   452  }
   453  
   454  func TestReplace(t *testing.T) {
   455  	tests := []struct {
   456  		where  string
   457  		start  int
   458  		end    int
   459  		what   string
   460  		result string
   461  	}{
   462  		{"0123456789", 3, 5, "abcdef", "012abcdef56789"},
   463  		{"0123456789", 3, 5, "ab", "012ab56789"},
   464  		{"0123456789", 3, 3, "abcd", "012abcd3456789"},
   465  		{"0123456789", 0, 2, "abcd", "abcd23456789"},
   466  		{"0123456789", 0, 0, "ab", "ab0123456789"},
   467  		{"0123456789", 10, 10, "ab", "0123456789ab"},
   468  		{"0123456789", 8, 10, "ab", "01234567ab"},
   469  		{"0123456789", 5, 5, "", "0123456789"},
   470  		{"0123456789", 3, 8, "", "01289"},
   471  		{"0123456789", 3, 8, "ab", "012ab89"},
   472  		{"0123456789", 0, 5, "a", "a56789"},
   473  		{"0123456789", 5, 10, "ab", "01234ab"},
   474  	}
   475  	for _, test := range tests {
   476  		t.Run(fmt.Sprintf("%+v", test), func(t *testing.T) {
   477  			result := replace([]byte(test.where), test.start, test.end, []byte(test.what))
   478  			if test.result != string(result) {
   479  				t.Errorf("want '%v', got '%v'", test.result, string(result))
   480  			}
   481  		})
   482  	}
   483  }
   484  
   485  func TestFuzz(t *testing.T) {
   486  	for _, data := range []string{
   487  		"kernel panicType 'help' for a list of commands",
   488  		"0000000000000000000\n\n\n\n\n\nBooting the kernel.",
   489  		"ZIRCON KERNEL PANICHalted",
   490  		"BUG:Disabling lock debugging due to kernel taint",
   491  		"[0.0] WARNING: ? 0+0x0/0",
   492  		"BUG: login: [0.0] ",
   493  		"cleaned vnode",
   494  		"kernel:",
   495  	} {
   496  		Fuzz([]byte(data)[:len(data):len(data)])
   497  	}
   498  }
   499  
   500  func TestTruncate(t *testing.T) {
   501  	assert.Equal(t, []byte(`01234
   502  
   503  <<cut 11 bytes out>>`), Truncate([]byte(`0123456789ABCDEF`), 5, 0))
   504  	assert.Equal(t, []byte(`<<cut 11 bytes out>>
   505  
   506  BCDEF`), Truncate([]byte(`0123456789ABCDEF`), 0, 5))
   507  	assert.Equal(t, []byte(`0123
   508  
   509  <<cut 9 bytes out>>
   510  
   511  DEF`), Truncate([]byte(`0123456789ABCDEF`), 4, 3))
   512  }
   513  
   514  func TestSplitReportBytes(t *testing.T) {
   515  	tests := []struct {
   516  		name      string
   517  		input     []byte
   518  		wantFirst string
   519  	}{
   520  		{
   521  			name:      "empty",
   522  			input:     nil,
   523  			wantFirst: "",
   524  		},
   525  		{
   526  			name:      "single",
   527  			input:     []byte("report1"),
   528  			wantFirst: "report1",
   529  		},
   530  		{
   531  			name:      "split in the middle",
   532  			input:     []byte("report1" + reportSeparator + "report2"),
   533  			wantFirst: "report1",
   534  		},
   535  		{
   536  			name:      "split in the middle, save new line",
   537  			input:     []byte("report1\n" + reportSeparator + "report2"),
   538  			wantFirst: "report1\n",
   539  		},
   540  	}
   541  	for _, test := range tests {
   542  		t.Run(test.name, func(t *testing.T) {
   543  			splitted := SplitReportBytes(test.input)
   544  			assert.Equal(t, test.wantFirst, string(splitted[0]))
   545  		})
   546  	}
   547  }