github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/pkg/cover/report_test.go (about) 1 // Copyright 2020 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 // It may or may not work on other OSes. 5 // If you test on another OS and it works, enable it. 6 //go:build linux 7 8 package cover 9 10 import ( 11 "bytes" 12 "encoding/csv" 13 "encoding/json" 14 "fmt" 15 "os" 16 "path/filepath" 17 "reflect" 18 "regexp" 19 "runtime" 20 "slices" 21 "strconv" 22 "strings" 23 "testing" 24 "time" 25 26 "github.com/google/syzkaller/pkg/cover/backend" 27 "github.com/google/syzkaller/pkg/mgrconfig" 28 "github.com/google/syzkaller/pkg/osutil" 29 "github.com/google/syzkaller/pkg/symbolizer" 30 _ "github.com/google/syzkaller/sys" 31 "github.com/google/syzkaller/sys/targets" 32 "github.com/stretchr/testify/assert" 33 ) 34 35 type Test struct { 36 Name string 37 CFlags []string 38 LDFlags []string 39 Progs []Prog 40 DebugInfo bool 41 AddCover bool 42 AddBadPc bool 43 // Set to true if the test should be skipped under broken kcov. 44 SkipIfKcovIsBroken bool 45 // Inexact coverage generated by AddCover=true may override empty Result. 46 Result string 47 Supports func(target *targets.Target) bool 48 } 49 50 func TestReportGenerator(t *testing.T) { 51 tests := []Test{ 52 { 53 Name: "no-coverage", 54 DebugInfo: true, 55 AddCover: true, 56 Result: `.* doesn't contain coverage callbacks \(set CONFIG_KCOV=y on linux\)`, 57 }, 58 { 59 Name: "no-debug-info", 60 CFlags: []string{"-fsanitize-coverage=trace-pc"}, 61 AddCover: true, 62 Result: `failed to parse DWARF.*\(set CONFIG_DEBUG_INFO=y on linux\)`, 63 }, 64 { 65 Name: "no-pcs", 66 CFlags: []string{"-fsanitize-coverage=trace-pc"}, 67 DebugInfo: true, 68 Result: `no coverage collected so far`, 69 }, 70 { 71 Name: "bad-pcs", 72 CFlags: []string{"-fsanitize-coverage=trace-pc"}, 73 DebugInfo: true, 74 Progs: []Prog{{Data: "data", PCs: []uint64{0x1, 0x2}}}, 75 Result: `coverage doesn't match any coverage callbacks`, 76 }, 77 { 78 Name: "good", 79 AddCover: true, 80 CFlags: []string{"-fsanitize-coverage=trace-pc"}, 81 DebugInfo: true, 82 }, 83 { 84 Name: "mismatch-pcs", 85 AddCover: true, 86 AddBadPc: true, 87 CFlags: []string{"-fsanitize-coverage=trace-pc"}, 88 DebugInfo: true, 89 SkipIfKcovIsBroken: true, 90 Result: `.* do not have matching coverage callbacks`, 91 }, 92 { 93 Name: "good-pie", 94 AddCover: true, 95 CFlags: []string{"-fsanitize-coverage=trace-pc", "-fpie"}, 96 LDFlags: []string{"-pie", "-Wl,--section-start=.text=0x33300000"}, 97 DebugInfo: true, 98 Supports: func(target *targets.Target) bool { 99 return target.OS == targets.Fuchsia || 100 // Fails with "relocation truncated to fit: R_AARCH64_CALL26 against symbol `memcpy'". 101 target.OS == targets.Linux && target.Arch != targets.ARM64 102 }, 103 }, 104 { 105 Name: "good-pie-relocs", 106 AddCover: true, 107 // This produces a binary that resembles CONFIG_RANDOMIZE_BASE=y. 108 // Symbols and .text section has addresses around 0x33300000, 109 // but debug info has all PC ranges around 0 address. 110 CFlags: []string{"-fsanitize-coverage=trace-pc", "-fpie"}, 111 LDFlags: []string{"-pie", "-Wl,--section-start=.text=0x33300000,--emit-relocs"}, 112 DebugInfo: true, 113 Supports: func(target *targets.Target) bool { 114 if target.OS == targets.Fuchsia { 115 return true 116 } 117 if target.OS == targets.Linux { 118 if target.Arch == targets.RiscV64 { 119 // When the binary is compiled with gcc and parsed with 120 // llvm-addr2line, we get an invalid "func_name", which 121 // breaks our tests. 122 fmt.Printf("target.CCompiler=%s", target.CCompiler) 123 return target.CCompiler == "clang" 124 } 125 if target.Arch == targets.ARM64 || target.Arch == targets.ARM || 126 target.Arch == targets.I386 { 127 return false 128 } 129 return true 130 } 131 return false 132 }, 133 }, 134 } 135 t.Parallel() 136 for os, arches := range targets.List { 137 if os == targets.TestOS { 138 continue 139 } 140 for _, target := range arches { 141 target := targets.Get(target.OS, target.Arch) 142 if target.BuildOS != runtime.GOOS { 143 continue 144 } 145 t.Run(target.OS+"-"+target.Arch, func(t *testing.T) { 146 t.Parallel() 147 if target.BrokenCompiler != "" { 148 t.Skip("skipping the test due to broken cross-compiler:\n" + target.BrokenCompiler) 149 } 150 for _, test := range tests { 151 test := test 152 t.Run(test.Name, func(t *testing.T) { 153 if test.Supports != nil && !test.Supports(target) { 154 t.Skip("unsupported target") 155 } 156 t.Parallel() 157 testReportGenerator(t, target, test) 158 }) 159 } 160 }) 161 } 162 } 163 } 164 165 func testReportGenerator(t *testing.T, target *targets.Target, test Test) { 166 reps, err := generateReport(t, target, &test) 167 if err != nil { 168 if test.Result == "" { 169 t.Fatalf("expected no error, but got:\n%v", err) 170 } 171 if !regexp.MustCompile(test.Result).MatchString(err.Error()) { 172 t.Fatalf("expected error %q, but got:\n%v", test.Result, err) 173 } 174 return 175 } 176 if test.Result != "" { 177 t.Fatalf("got no error, but expected %q", test.Result) 178 } 179 checkCSVReport(t, reps.csv) 180 checkJSONLReport(t, reps.jsonl) 181 } 182 183 const kcovCode = ` 184 #ifdef ASLR_BASE 185 #define _GNU_SOURCE 186 #endif 187 188 #include <stdio.h> 189 190 #ifdef ASLR_BASE 191 #include <dlfcn.h> 192 #include <link.h> 193 #include <stddef.h> 194 195 void* aslr_base() { 196 struct link_map* map = NULL; 197 void* handle = dlopen(NULL, RTLD_LAZY | RTLD_NOLOAD); 198 if (handle != NULL) { 199 dlinfo(handle, RTLD_DI_LINKMAP, &map); 200 dlclose(handle); 201 } 202 return map ? map->l_addr : NULL; 203 } 204 #else 205 void* aslr_base() { return NULL; } 206 #endif 207 208 void __sanitizer_cov_trace_pc() { printf("%llu", (long long)(__builtin_return_address(0) - aslr_base())); } 209 ` 210 211 func buildTestBinary(t *testing.T, target *targets.Target, test *Test, dir string) string { 212 kcovSrc := filepath.Join(dir, "kcov.c") 213 kcovObj := filepath.Join(dir, "kcov.o") 214 if err := osutil.WriteFile(kcovSrc, []byte(kcovCode)); err != nil { 215 t.Fatal(err) 216 } 217 218 aslrDefine := "-DNO_ASLR_BASE" 219 if target.OS == targets.Linux || target.OS == targets.OpenBSD || 220 target.OS == targets.FreeBSD || target.OS == targets.NetBSD { 221 aslrDefine = "-DASLR_BASE" 222 } 223 aslrExtraLibs := []string{} 224 if target.OS == targets.Linux { 225 aslrExtraLibs = []string{"-ldl"} 226 } 227 228 targetCFlags := slices.DeleteFunc(slices.Clone(target.CFlags), func(flag string) bool { 229 return strings.HasPrefix(flag, "-std=c++") 230 }) 231 kcovFlags := append([]string{"-c", "-fpie", "-w", "-x", "c", "-o", kcovObj, kcovSrc, aslrDefine}, targetCFlags...) 232 src := filepath.Join(dir, "main.c") 233 obj := filepath.Join(dir, "main.o") 234 bin := filepath.Join(dir, target.KernelObject) 235 if err := osutil.WriteFile(src, []byte(`int main() {}`)); err != nil { 236 t.Fatal(err) 237 } 238 if _, err := osutil.RunCmd(time.Hour, "", target.CCompiler, kcovFlags...); err != nil { 239 t.Fatal(err) 240 } 241 242 // We used to compile and link with a single compiler invocation, 243 // but clang has a bug that it tries to link in ubsan runtime when 244 // -fsanitize-coverage=trace-pc is provided during linking and 245 // ubsan runtime is missing for arm/arm64/riscv arches in the llvm packages. 246 // So we first compile with -fsanitize-coverage and then link w/o it. 247 cflags := append(append([]string{"-w", "-c", "-o", obj, src}, targetCFlags...), test.CFlags...) 248 if test.DebugInfo { 249 // TODO: pkg/cover doesn't support DWARF5 yet, which is the default in Clang. 250 cflags = append([]string{"-g", "-gdwarf-4"}, cflags...) 251 } 252 if _, err := osutil.RunCmd(time.Hour, "", target.CCompiler, cflags...); err != nil { 253 errText := err.Error() 254 errText = strings.ReplaceAll(errText, "‘", "'") 255 errText = strings.ReplaceAll(errText, "’", "'") 256 if strings.Contains(errText, "error: unrecognized command line option '-fsanitize-coverage=trace-pc'") && 257 os.Getenv("SYZ_ENV") == "" { 258 t.Skip("skipping test, -fsanitize-coverage=trace-pc is not supported") 259 } 260 t.Fatal(err) 261 } 262 263 ldflags := append(append(append([]string{"-o", bin, obj, kcovObj}, aslrExtraLibs...), 264 targetCFlags...), test.LDFlags...) 265 staticIdx, pieIdx := -1, -1 266 for i, arg := range ldflags { 267 switch arg { 268 case "-static": 269 staticIdx = i 270 case "-pie": 271 pieIdx = i 272 } 273 } 274 if target.OS == targets.Fuchsia && pieIdx != -1 { 275 // Fuchsia toolchain fails when given -pie: 276 // clang-12: error: argument unused during compilation: '-pie' 277 ldflags[pieIdx] = ldflags[len(ldflags)-1] 278 ldflags = ldflags[:len(ldflags)-1] 279 } else if pieIdx != -1 && staticIdx != -1 { 280 // -static and -pie are incompatible during linking. 281 ldflags[staticIdx] = ldflags[len(ldflags)-1] 282 ldflags = ldflags[:len(ldflags)-1] 283 } 284 if _, err := osutil.RunCmd(time.Hour, "", target.CCompiler, ldflags...); err != nil { 285 // Arm linker in the env image has a bug when linking a clang-produced files. 286 if regexp.MustCompile(`arm-linux-gnueabi.* assertion fail`).MatchString(err.Error()) { 287 t.Skipf("skipping test, broken arm linker (%v)", err) 288 } 289 t.Fatal(err) 290 } 291 return bin 292 } 293 294 type reports struct { 295 html []byte 296 csv []byte 297 jsonl []byte 298 } 299 300 func generateReport(t *testing.T, target *targets.Target, test *Test) (*reports, error) { 301 dir := t.TempDir() 302 bin := buildTestBinary(t, target, test, dir) 303 cfg := &mgrconfig.Config{ 304 Derived: mgrconfig.Derived{ 305 SysTarget: target, 306 }, 307 KernelObj: dir, 308 KernelSrc: dir, 309 KernelBuildSrc: dir, 310 Type: "", 311 } 312 subsystem := []mgrconfig.Subsystem{ 313 { 314 Name: "sound", 315 Paths: []string{ 316 "sound", 317 "techpack/audio", 318 }, 319 }, 320 } 321 322 // Deep copy, as we are going to modify progs. Our test generate multiple reports from the same 323 // test object in parallel. Without copying we have a datarace here. 324 progs := []Prog{} 325 for _, p := range test.Progs { 326 progs = append(progs, Prog{Sig: p.Sig, Data: p.Data, PCs: append([]uint64{}, p.PCs...)}) 327 } 328 329 rg, err := MakeReportGenerator(cfg, subsystem, nil, false) 330 if err != nil { 331 return nil, err 332 } 333 if !rg.PreciseCoverage && test.SkipIfKcovIsBroken { 334 t.Skip("coverage testing requested, but kcov is broken") 335 } 336 if test.AddCover { 337 var pcs []uint64 338 Inexact := false 339 // Sanitizers crash when installing signal handlers with static libc. 340 const sanitizerOptions = "handle_segv=0:handle_sigbus=0:handle_sigfpe=0" 341 cmd := osutil.Command(bin) 342 cmd.Env = append([]string{ 343 "UBSAN_OPTIONS=" + sanitizerOptions, 344 "ASAN_OPTIONS=" + sanitizerOptions, 345 }, os.Environ()...) 346 if output, err := osutil.Run(time.Minute, cmd); err == nil { 347 pc, err := strconv.ParseUint(string(output), 10, 64) 348 if err != nil { 349 t.Fatal(err) 350 } 351 pcs = append(pcs, backend.PreviousInstructionPC(target, pc)) 352 t.Logf("using exact coverage PC 0x%x", pcs[0]) 353 } else if target.OS == runtime.GOOS && (target.Arch == runtime.GOARCH || target.VMArch == runtime.GOARCH) { 354 t.Fatal(err) 355 } else { 356 symb := symbolizer.NewSymbolizer(target) 357 text, err := symb.ReadTextSymbols(bin) 358 if err != nil { 359 t.Fatal(err) 360 } 361 if nmain := len(text["main"]); nmain != 1 { 362 t.Fatalf("got %v main symbols", nmain) 363 } 364 main := text["main"][0] 365 for off := 0; off < main.Size; off++ { 366 pcs = append(pcs, main.Addr+uint64(off)) 367 } 368 t.Logf("using inexact coverage range 0x%x-0x%x", main.Addr, main.Addr+uint64(main.Size)) 369 Inexact = true 370 } 371 if Inexact && test.Result == "" && rg.PreciseCoverage { 372 test.Result = fmt.Sprintf("%d out of %d PCs returned by kcov do not have matching coverage callbacks", 373 len(pcs)-1, len(pcs)) 374 } 375 if test.AddBadPc { 376 pcs = append(pcs, 0xdeadbeef) 377 } 378 progs = append(progs, Prog{Data: "main", PCs: pcs}) 379 } 380 html := new(bytes.Buffer) 381 params := CoverHandlerParams{ 382 Progs: progs, 383 } 384 if err := rg.DoHTML(html, params); err != nil { 385 return nil, err 386 } 387 htmlTable := new(bytes.Buffer) 388 if err := rg.DoHTMLTable(htmlTable, params); err != nil { 389 return nil, err 390 } 391 _ = htmlTable 392 csv := new(bytes.Buffer) 393 if err := rg.DoCSV(csv, params); err != nil { 394 return nil, err 395 } 396 csvFiles := new(bytes.Buffer) 397 if err := rg.DoCSVFiles(csvFiles, params); err != nil { 398 return nil, err 399 } 400 _ = csvFiles 401 jsonl := new(bytes.Buffer) 402 if err := rg.DoCoverJSONL(jsonl, params); err != nil { 403 return nil, err 404 } 405 return &reports{ 406 html: html.Bytes(), 407 csv: csv.Bytes(), 408 jsonl: jsonl.Bytes(), 409 }, nil 410 } 411 412 func checkCSVReport(t *testing.T, CSVReport []byte) { 413 csvReader := csv.NewReader(bytes.NewBuffer(CSVReport)) 414 lines, err := csvReader.ReadAll() 415 if err != nil { 416 t.Fatal(err) 417 } 418 419 if !reflect.DeepEqual(lines[0], csvHeader) { 420 t.Fatalf("heading line in CSV doesn't match %v", lines[0]) 421 } 422 423 foundMain := false 424 for _, line := range lines { 425 if line[2] == "main" { 426 foundMain = true 427 if line[3] != "1" && line[4] != "1" { 428 t.Fatalf("function coverage percentage doesn't match %v vs. %v", line[3], "100") 429 } 430 } 431 } 432 if !foundMain { 433 t.Fatalf("no main in the CSV report") 434 } 435 } 436 437 // nolint:lll 438 func checkJSONLReport(t *testing.T, r []byte) { 439 compacted := new(bytes.Buffer) 440 if err := json.Compact(compacted, sampleCoverJSON); err != nil { 441 t.Errorf("failed to prepare compacted json: %v", err) 442 } 443 compacted.Write([]byte("\n")) 444 445 // PC is hard to predict here. Let's fix it. 446 actualString := regexp.MustCompile(`"pc":[0-9]*`).ReplaceAllString( 447 string(r), `"pc":12345`) 448 assert.Equal(t, compacted.String(), actualString) 449 }