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 }