// Copyright 2020 syzkaller project authors. All rights reserved. // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. // It may or may not work on other OSes. // If you test on another OS and it works, enable it. //go:build linux package cover import ( "bytes" "encoding/csv" "encoding/json" "errors" "fmt" "os" "path/filepath" "reflect" "regexp" "runtime" "slices" "strconv" "strings" "testing" "time" "github.com/google/syzkaller/pkg/cover/backend" "github.com/google/syzkaller/pkg/mgrconfig" "github.com/google/syzkaller/pkg/osutil" "github.com/google/syzkaller/pkg/symbolizer" _ "github.com/google/syzkaller/sys" "github.com/google/syzkaller/sys/targets" "github.com/stretchr/testify/assert" ) type Test struct { Name string CFlags []string LDFlags []string Progs []Prog DebugInfo bool AddCover bool AddBadPc bool // Set to true if the test should be skipped under broken kcov. SkipIfKcovIsBroken bool // Inexact coverage generated by AddCover=true may override empty Result. Result string Supports func(target *targets.Target) bool } func TestReportGenerator(t *testing.T) { tests := []Test{ { Name: "no-coverage", DebugInfo: true, AddCover: true, Result: `.* doesn't contain coverage callbacks \(set CONFIG_KCOV=y on linux\)`, }, { Name: "no-debug-info", CFlags: []string{"-fsanitize-coverage=trace-pc"}, AddCover: true, Result: `failed to parse DWARF.*\(set CONFIG_DEBUG_INFO=y on linux\)`, }, { Name: "no-pcs", CFlags: []string{"-fsanitize-coverage=trace-pc"}, DebugInfo: true, Result: `no coverage collected so far`, }, { Name: "bad-pcs", CFlags: []string{"-fsanitize-coverage=trace-pc"}, DebugInfo: true, Progs: []Prog{{Data: "data", PCs: []uint64{0x1, 0x2}}}, Result: `coverage doesn't match any coverage callbacks`, }, { Name: "good", AddCover: true, CFlags: []string{"-fsanitize-coverage=trace-pc"}, DebugInfo: true, }, { Name: "mismatch-pcs", AddCover: true, AddBadPc: true, CFlags: []string{"-fsanitize-coverage=trace-pc"}, DebugInfo: true, SkipIfKcovIsBroken: true, Result: `.* do not have matching coverage callbacks`, }, { Name: "good-pie", AddCover: true, CFlags: []string{"-fsanitize-coverage=trace-pc", "-fpie"}, LDFlags: []string{"-pie", "-Wl,--section-start=.text=0x33300000"}, DebugInfo: true, Supports: func(target *targets.Target) bool { return target.OS == targets.Fuchsia || // Fails with "relocation truncated to fit: R_AARCH64_CALL26 against symbol `memcpy'". target.OS == targets.Linux && target.Arch != targets.ARM64 }, }, { Name: "good-pie-relocs", AddCover: true, // This produces a binary that resembles CONFIG_RANDOMIZE_BASE=y. // Symbols and .text section has addresses around 0x33300000, // but debug info has all PC ranges around 0 address. CFlags: []string{"-fsanitize-coverage=trace-pc", "-fpie"}, LDFlags: []string{"-pie", "-Wl,--section-start=.text=0x33300000,--emit-relocs"}, DebugInfo: true, Supports: func(target *targets.Target) bool { if target.OS == targets.Fuchsia { return true } if target.OS == targets.Linux { if target.Arch == targets.RiscV64 { // When the binary is compiled with gcc and parsed with // llvm-addr2line, we get an invalid "func_name", which // breaks our tests. fmt.Printf("target.CCompiler=%s", target.CCompiler) return target.CCompiler == "clang" } if target.Arch == targets.ARM64 || target.Arch == targets.ARM || target.Arch == targets.I386 { return false } return true } return false }, }, } t.Parallel() for os, arches := range targets.List { if os == targets.TestOS { continue } for _, target := range arches { target := targets.Get(target.OS, target.Arch) if target.BuildOS != runtime.GOOS { continue } t.Run(target.OS+"-"+target.Arch, func(t *testing.T) { t.Parallel() if target.BrokenCompiler != "" { t.Skip("skipping the test due to broken cross-compiler:\n" + target.BrokenCompiler) } for _, test := range tests { t.Run(test.Name, func(t *testing.T) { if test.Supports != nil && !test.Supports(target) { t.Skip("unsupported target") } t.Parallel() testReportGenerator(t, target, test) }) } }) } } } func testReportGenerator(t *testing.T, target *targets.Target, test Test) { reps, err := generateReport(t, target, &test) if err != nil { if test.Result == "" { t.Fatalf("expected no error, but got:\n%v", err) } if !regexp.MustCompile(test.Result).MatchString(err.Error()) { t.Fatalf("expected error %q, but got:\n%v", test.Result, err) } return } if test.Result != "" { t.Fatalf("got no error, but expected %q", test.Result) } checkCSVReport(t, reps.csv.Bytes()) checkJSONLReport(t, reps.jsonl.Bytes(), sampleCoverJSON) checkJSONLReport(t, reps.jsonlPrograms.Bytes(), sampleJSONLlProgs) } const kcovCode = ` #ifdef ASLR_BASE #define _GNU_SOURCE #endif #include #ifdef ASLR_BASE #include #include #include void* aslr_base() { struct link_map* map = NULL; void* handle = dlopen(NULL, RTLD_LAZY | RTLD_NOLOAD); if (handle != NULL) { dlinfo(handle, RTLD_DI_LINKMAP, &map); dlclose(handle); } return map ? (void *)map->l_addr : NULL; } #else void* aslr_base() { return NULL; } #endif void __sanitizer_cov_trace_pc() { printf("%llu", (long long)(__builtin_return_address(0) - aslr_base())); } ` func buildTestBinary(t *testing.T, target *targets.Target, test *Test, dir string) string { kcovSrc := filepath.Join(dir, "kcov.c") kcovObj := filepath.Join(dir, "kcov.o") if err := osutil.WriteFile(kcovSrc, []byte(kcovCode)); err != nil { t.Fatal(err) } aslrDefine := "-DNO_ASLR_BASE" if target.OS == targets.Linux || target.OS == targets.OpenBSD || target.OS == targets.FreeBSD || target.OS == targets.NetBSD { aslrDefine = "-DASLR_BASE" } aslrExtraLibs := []string{} if target.OS == targets.Linux { aslrExtraLibs = []string{"-ldl"} } targetCFlags := slices.DeleteFunc(slices.Clone(target.CFlags), func(flag string) bool { return strings.HasPrefix(flag, "-std=c++") }) kcovFlags := append([]string{"-c", "-fpie", "-w", "-x", "c", "-o", kcovObj, kcovSrc, aslrDefine}, targetCFlags...) src := filepath.Join(dir, "main.c") obj := filepath.Join(dir, "main.o") bin := filepath.Join(dir, target.KernelObject) if err := osutil.WriteFile(src, []byte(`int main() {}`)); err != nil { t.Fatal(err) } if _, err := osutil.RunCmd(time.Hour, "", target.CCompiler, kcovFlags...); err != nil { t.Fatal(err) } // We used to compile and link with a single compiler invocation, // but clang has a bug that it tries to link in ubsan runtime when // -fsanitize-coverage=trace-pc is provided during linking and // ubsan runtime is missing for arm/arm64/riscv arches in the llvm packages. // So we first compile with -fsanitize-coverage and then link w/o it. cflags := append(append([]string{"-w", "-c", "-o", obj, src}, targetCFlags...), test.CFlags...) if test.DebugInfo { // TODO: pkg/cover doesn't support DWARF5 yet, which is the default in Clang. cflags = append([]string{"-g", "-gdwarf-4"}, cflags...) } if _, err := osutil.RunCmd(time.Hour, "", target.CCompiler, cflags...); err != nil { errText := err.Error() errText = strings.ReplaceAll(errText, "‘", "'") errText = strings.ReplaceAll(errText, "’", "'") if strings.Contains(errText, "error: unrecognized command line option '-fsanitize-coverage=trace-pc'") && os.Getenv("SYZ_ENV") == "" { t.Skip("skipping test, -fsanitize-coverage=trace-pc is not supported") } t.Fatal(err) } ldflags := append(append(append([]string{"-o", bin, obj, kcovObj}, aslrExtraLibs...), targetCFlags...), test.LDFlags...) staticIdx, pieIdx := -1, -1 for i, arg := range ldflags { switch arg { case "-static": staticIdx = i case "-pie": pieIdx = i } } if target.OS == targets.Fuchsia && pieIdx != -1 { // Fuchsia toolchain fails when given -pie: // clang-12: error: argument unused during compilation: '-pie' ldflags[pieIdx] = ldflags[len(ldflags)-1] ldflags = ldflags[:len(ldflags)-1] } else if pieIdx != -1 && staticIdx != -1 { // -static and -pie are incompatible during linking. ldflags[staticIdx] = ldflags[len(ldflags)-1] ldflags = ldflags[:len(ldflags)-1] } if _, err := osutil.RunCmd(time.Hour, "", target.CCompiler, ldflags...); err != nil { // Arm linker in the env image has a bug when linking a clang-produced files. var vErr *osutil.VerboseError if errors.As(err, &vErr) && regexp.MustCompile(`arm-linux-gnueabi.* assertion fail`).Match(vErr.Output) { t.Skipf("skipping test, broken arm linker (%v)", err) } t.Fatal(err) } return bin } type reports struct { csv *bytes.Buffer jsonl *bytes.Buffer jsonlPrograms *bytes.Buffer } func generateReport(t *testing.T, target *targets.Target, test *Test) (*reports, error) { dir := t.TempDir() bin := buildTestBinary(t, target, test, dir) cfg := &mgrconfig.Config{ Derived: mgrconfig.Derived{ SysTarget: target, }, KernelObj: dir, KernelSrc: dir, KernelBuildSrc: dir, Type: "", } cfg.KernelSubsystem = []mgrconfig.Subsystem{ { Name: "sound", Paths: []string{ "sound", "techpack/audio", }, }, } modules, err := backend.DiscoverModules(cfg.SysTarget, cfg.KernelObj, cfg.ModuleObj) if err != nil { return nil, err } // Deep copy, as we are going to modify progs. Our test generate multiple reports from the same // test object in parallel. Without copying we have a datarace here. progs := []Prog{} for _, p := range test.Progs { progs = append(progs, Prog{Sig: p.Sig, Data: p.Data, PCs: append([]uint64{}, p.PCs...)}) } rg, err := MakeReportGenerator(cfg, modules) if err != nil { return nil, err } if !rg.PreciseCoverage && test.SkipIfKcovIsBroken { t.Skip("coverage testing requested, but kcov is broken") } if test.AddCover { var pcs []uint64 Inexact := false // Sanitizers crash when installing signal handlers with static libc. const sanitizerOptions = "handle_segv=0:handle_sigbus=0:handle_sigfpe=0" cmd := osutil.Command(bin) cmd.Env = append([]string{ "UBSAN_OPTIONS=" + sanitizerOptions, "ASAN_OPTIONS=" + sanitizerOptions, }, os.Environ()...) if output, err := osutil.Run(time.Minute, cmd); err == nil { pc, err := strconv.ParseUint(string(output), 10, 64) if err != nil { t.Fatal(err) } pcs = append(pcs, backend.PreviousInstructionPC(target, "", pc)) t.Logf("using exact coverage PC 0x%x", pcs[0]) } else if target.OS == runtime.GOOS && (target.Arch == runtime.GOARCH || target.VMArch == runtime.GOARCH) { t.Fatal(err) } else { text, err := symbolizer.ReadTextSymbols(bin) if err != nil { t.Fatal(err) } if nmain := len(text["main"]); nmain != 1 { t.Fatalf("got %v main symbols", nmain) } main := text["main"][0] for off := 0; off < main.Size; off++ { pcs = append(pcs, main.Addr+uint64(off)) } t.Logf("using inexact coverage range 0x%x-0x%x", main.Addr, main.Addr+uint64(main.Size)) Inexact = true } if Inexact && test.Result == "" && rg.PreciseCoverage { test.Result = fmt.Sprintf("%d out of %d PCs returned by kcov do not have matching coverage callbacks", len(pcs)-1, len(pcs)) } if test.AddBadPc { pcs = append(pcs, 0xdeadbeef) } progs = append(progs, Prog{Data: "main", PCs: pcs}) } params := HandlerParams{ Progs: progs, } if err := rg.DoHTML(new(bytes.Buffer), params); err != nil { return nil, err } assert.NoError(t, rg.DoSubsystemCover(new(bytes.Buffer), params)) assert.NoError(t, rg.DoFileCover(new(bytes.Buffer), params)) res := &reports{ csv: new(bytes.Buffer), jsonl: new(bytes.Buffer), jsonlPrograms: new(bytes.Buffer), } assert.NoError(t, rg.DoFuncCover(res.csv, params)) assert.NoError(t, rg.DoCoverJSONL(res.jsonl, params)) assert.NoError(t, rg.DoCoverPrograms(res.jsonlPrograms, params)) return res, nil } func checkCSVReport(t *testing.T, CSVReport []byte) { csvReader := csv.NewReader(bytes.NewBuffer(CSVReport)) lines, err := csvReader.ReadAll() if err != nil { t.Fatal(err) } if !reflect.DeepEqual(lines[0], csvHeader) { t.Fatalf("heading line in CSV doesn't match %v", lines[0]) } foundMain := false for _, line := range lines { if line[2] == "main" { foundMain = true if line[3] != "1" && line[4] != "1" { t.Fatalf("function coverage percentage doesn't match %v vs. %v", line[3], "100") } } } if !foundMain { t.Fatalf("no main in the CSV report") } } func checkJSONLReport(t *testing.T, gotBytes, wantBytes []byte) { compacted := new(bytes.Buffer) if err := json.Compact(compacted, wantBytes); err != nil { t.Errorf("failed to prepare compacted json: %v", err) } compacted.Write([]byte("\n")) // PC is hard to predict here. Let's fix it. actualString := regexp.MustCompile(`"pc":[0-9]*`).ReplaceAllString( string(gotBytes), `"pc":12345`) assert.Equal(t, compacted.String(), actualString) } var sampleJSONLlProgs = []byte(`{ "program": "main", "coverage": [ { "file_path": "main.c", "functions": [ { "func_name": "main", "blocks": [ { "hit_count": 1, "from_line": 1, "from_column": 0, "to_line": 1, "to_column": -1 } ] } ] } ] }`) func makeFileStat(name string) fileStats { return fileStats{ Name: name, CoveredLines: 1, TotalLines: 8, CoveredPCs: 1, TotalPCs: 4, TotalFunctions: 2, CoveredFunctions: 1, CoveredPCsInFunctions: 1, TotalPCsInCoveredFunctions: 2, TotalPCsInFunctions: 2, } } func TestCoverByFilePrefixes(t *testing.T) { datas := []fileStats{ makeFileStat("a"), makeFileStat("a/1"), makeFileStat("a/2"), makeFileStat("a/2/A"), makeFileStat("a/3"), } subsystems := []mgrconfig.Subsystem{ { Name: "test", Paths: []string{ "a", "-a/2", }, }, } d := groupCoverByFilePrefixes(datas, subsystems) assert.Equal(t, d["test"], map[string]string{ "name": "test", "lines": "3 / 24 / 12.50%", "PCsInFiles": "3 / 12 / 25.00%", "Funcs": "3 / 6 / 50.00%", "PCsInFuncs": "3 / 6 / 50.00%", "PCsInCoveredFuncs": "3 / 6 / 50.00%", }) }