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  }