pkg/cover: add test for report generation

Test various combinations of no debug info,
no coverage instrumentation, no PCs, bad PCs, good PCs,
and what errors we produce for these.
Also implement support for cross-arch reports:
prefix objdump with cross-compile prefix
(e.g. aarch64-linux-gnu-objdump instead of objdump).
This commit is contained in:
Dmitry Vyukov 2020-05-27 14:26:00 +02:00
parent 9072c1268e
commit fdf90f622b
7 changed files with 274 additions and 64 deletions

View File

@ -19,9 +19,11 @@ import (
"github.com/google/syzkaller/pkg/osutil"
"github.com/google/syzkaller/pkg/symbolizer"
"github.com/google/syzkaller/sys/targets"
)
type ReportGenerator struct {
target *targets.Target
srcDir string
buildDir string
objDir string
@ -39,26 +41,24 @@ type symbol struct {
end uint64
}
func MakeReportGenerator(vmlinux, srcDir, buildDir, arch string) (*ReportGenerator, error) {
func MakeReportGenerator(target *targets.Target, kernelObject, srcDir, buildDir string) (*ReportGenerator, error) {
rg := &ReportGenerator{
target: target,
srcDir: srcDir,
buildDir: buildDir,
objDir: filepath.Dir(vmlinux),
objDir: filepath.Dir(kernelObject),
pcs: make(map[uint64][]symbolizer.Frame),
}
errc := make(chan error)
go func() {
var err error
rg.symbols, err = readSymbols(vmlinux)
rg.symbols, err = readSymbols(kernelObject)
errc <- err
}()
frames, err := objdumpAndSymbolize(vmlinux, arch)
frames, err := objdumpAndSymbolize(target, kernelObject)
if err != nil {
return nil, err
}
if len(frames) == 0 {
return nil, fmt.Errorf("%v does not have debug info (set CONFIG_DEBUG_INFO=y)", vmlinux)
}
if err := <-errc; err != nil {
return nil, err
}
@ -85,10 +85,12 @@ type line struct {
func (rg *ReportGenerator) Do(w io.Writer, progs []Prog) error {
coveredPCs := make(map[uint64]bool)
allPCs := make(map[uint64]bool)
symbols := make(map[uint64]bool)
files := make(map[string]*file)
for progIdx, prog := range progs {
for _, pc := range prog.PCs {
allPCs[pc] = true
symbols[rg.findSymbol(pc)] = true
frames, ok := rg.pcs[pc]
if !ok {
@ -110,8 +112,11 @@ func (rg *ReportGenerator) Do(w io.Writer, progs []Prog) error {
}
}
}
if len(allPCs) == 0 {
return fmt.Errorf("no coverage collected so far")
}
if len(coveredPCs) == 0 {
return fmt.Errorf("no coverage data available")
return fmt.Errorf("coverage (%v) doesn't match coverage callbacks", len(allPCs))
}
for pc, frames := range rg.pcs {
covered := coveredPCs[pc]
@ -334,8 +339,8 @@ func readSymbols(obj string) ([]symbol, error) {
// objdumpAndSymbolize collects list of PCs of __sanitizer_cov_trace_pc calls
// in the kernel and symbolizes them.
func objdumpAndSymbolize(obj, arch string) ([]symbolizer.Frame, error) {
errc := make(chan error)
func objdumpAndSymbolize(target *targets.Target, obj string) ([]symbolizer.Frame, error) {
errc := make(chan error, 1)
pcchan := make(chan []uint64, 10)
var frames []symbolizer.Frame
go func() {
@ -354,12 +359,21 @@ func objdumpAndSymbolize(obj, arch string) ([]symbolizer.Frame, error) {
}
errc <- err
}()
cmd := osutil.Command("objdump", "-d", "--no-show-raw-insn", obj)
objdump := "objdump"
if target.Triple != "" {
objdump = target.Triple + "-" + objdump
}
cmd := osutil.Command(objdump, "-d", "--no-show-raw-insn", obj)
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
defer stdout.Close()
stderr, err := cmd.StderrPipe()
if err != nil {
return nil, err
}
defer stderr.Close()
if err := cmd.Start(); err != nil {
return nil, fmt.Errorf("failed to run objdump on %v: %v", obj, err)
}
@ -368,46 +382,76 @@ func objdumpAndSymbolize(obj, arch string) ([]symbolizer.Frame, error) {
cmd.Wait()
}()
s := bufio.NewScanner(stdout)
callInsnS, traceFuncS := archCallInsn(arch)
callInsn, traceFunc := []byte(callInsnS), []byte(traceFuncS)
callInsns, traceFuncs := archCallInsn(target)
var pcs []uint64
npcs := 0
for s.Scan() {
ln := s.Bytes()
if pos := bytes.Index(ln, callInsn); pos == -1 {
continue
} else if !bytes.Contains(ln[pos:], traceFunc) {
continue
}
for len(ln) != 0 && ln[0] == ' ' {
ln = ln[1:]
}
colon := bytes.IndexByte(ln, ':')
if colon == -1 {
continue
}
pc, err := strconv.ParseUint(string(ln[:colon]), 16, 64)
if err != nil {
continue
}
pcs = append(pcs, pc)
if len(pcs) == 100 {
pcchan <- pcs
pcs = nil
if pc := parseLine(callInsns, traceFuncs, s.Bytes()); pc != 0 {
npcs++
pcs = append(pcs, pc)
if len(pcs) == 100 {
pcchan <- pcs
pcs = nil
}
}
}
if len(pcs) != 0 {
pcchan <- pcs
}
close(pcchan)
stderrOut, _ := ioutil.ReadAll(stderr)
if err := cmd.Wait(); err != nil {
return nil, fmt.Errorf("failed to run objdump on %v: %v\n%s", obj, err, stderrOut)
}
if err := s.Err(); err != nil {
return nil, fmt.Errorf("failed to run objdump output: %v", err)
return nil, fmt.Errorf("failed to run objdump on %v: %v\n%s", obj, err, stderrOut)
}
if npcs == 0 {
return nil, fmt.Errorf("%v doesn't contain coverage callbacks '%s%s' (set CONFIG_KCOV=y)",
obj, callInsns, traceFuncs)
}
if err := <-errc; err != nil {
return nil, err
}
if len(frames) == 0 {
return nil, fmt.Errorf("%v doesn't have debug info (set CONFIG_DEBUG_INFO=y)", obj)
}
return frames, nil
}
func parseLine(callInsns, traceFuncs [][]byte, ln []byte) uint64 {
pos := -1
for _, callInsn := range callInsns {
if pos = bytes.Index(ln, callInsn); pos != -1 {
break
}
}
if pos == -1 {
return 0
}
hasCall := false
for _, traceFunc := range traceFuncs {
if hasCall = bytes.Contains(ln[pos:], traceFunc); hasCall {
break
}
}
if !hasCall {
return 0
}
for len(ln) != 0 && ln[0] == ' ' {
ln = ln[1:]
}
colon := bytes.IndexByte(ln, ':')
if colon == -1 {
return 0
}
pc, err := strconv.ParseUint(string(ln[:colon]), 16, 64)
if err != nil {
return 0
}
return pc
}
func parseFile(fn string) ([][]byte, error) {
data, err := ioutil.ReadFile(fn)
if err != nil {
@ -429,8 +473,8 @@ func parseFile(fn string) ([][]byte, error) {
return lines, nil
}
func PreviousInstructionPC(arch string, pc uint64) uint64 {
switch arch {
func PreviousInstructionPC(target *targets.Target, pc uint64) uint64 {
switch target.Arch {
case "amd64":
return pc - 5
case "386":
@ -446,33 +490,40 @@ func PreviousInstructionPC(arch string, pc uint64) uint64 {
case "mips64le":
return pc - 8
default:
panic(fmt.Sprintf("unknown arch %q", arch))
panic(fmt.Sprintf("unknown arch %q", target.Arch))
}
}
func archCallInsn(arch string) (string, string) {
const callName = " <__sanitizer_cov_trace_pc>"
switch arch {
func archCallInsn(target *targets.Target) ([][]byte, [][]byte) {
callName := [][]byte{[]byte(" <__sanitizer_cov_trace_pc>")}
switch target.Arch {
case "amd64":
// ffffffff8100206a: callq ffffffff815cc1d0 <__sanitizer_cov_trace_pc>
return "\tcallq ", callName
return [][]byte{[]byte("\tcallq ")}, callName
case "386":
// c1000102: call c10001f0 <__sanitizer_cov_trace_pc>
return "\tcall ", callName
return [][]byte{[]byte("\tcall ")}, callName
case "arm64":
// ffff0000080d9cc0: bl ffff00000820f478 <__sanitizer_cov_trace_pc>
return "\tbl\t", callName
return [][]byte{[]byte("\tbl\t")}, callName
case "arm":
// 8010252c: bl 801c3280 <__sanitizer_cov_trace_pc>
return "\tbl\t", callName
return [][]byte{[]byte("\tbl\t")}, callName
case "ppc64le":
// c00000000006d904: bl c000000000350780 <.__sanitizer_cov_trace_pc>
return "\tbl ", " <.__sanitizer_cov_trace_pc>"
// This is only known to occur in the test:
// 838: bl 824 <__sanitizer_cov_trace_pc+0x8>
return [][]byte{[]byte("\tbl ")}, [][]byte{
[]byte("<__sanitizer_cov_trace_pc+0x8>"),
[]byte(" <.__sanitizer_cov_trace_pc>"),
}
case "mips64le":
// ffffffff80100420: jal ffffffff80205880 <__sanitizer_cov_trace_pc>
return "\tjal\t", callName
// This is only known to occur in the test:
// b58: bal b30 <__sanitizer_cov_trace_pc>
return [][]byte{[]byte("\tjal\t"), []byte("\tbal\t")}, callName
default:
panic(fmt.Sprintf("unknown arch %q", arch))
panic(fmt.Sprintf("unknown arch %q", target.Arch))
}
}

152
pkg/cover/report_test.go Normal file
View File

@ -0,0 +1,152 @@
// 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.
// +build linux
package cover
import (
"bytes"
"os"
"path/filepath"
"regexp"
"runtime"
"testing"
"time"
"github.com/google/syzkaller/pkg/osutil"
"github.com/google/syzkaller/pkg/symbolizer"
_ "github.com/google/syzkaller/sys"
"github.com/google/syzkaller/sys/targets"
)
type Test struct {
Name string
CFlags []string
Progs []Prog
Result string
}
func TestReportGenerator(t *testing.T) {
tests := []Test{
{
Name: "no-coverage",
CFlags: []string{"-g"},
Result: `.* doesn't contain coverage callbacks '.*__sanitizer_cov_trace_pc>\]' \(set CONFIG_KCOV=y\)`,
},
{
Name: "no-debug-info",
CFlags: []string{"-fsanitize-coverage=trace-pc"},
Result: `.* doesn't have debug info \(set CONFIG_DEBUG_INFO=y\)`,
},
{
Name: "no-pcs",
CFlags: []string{"-fsanitize-coverage=trace-pc", "-g"},
Result: `no coverage collected so far`,
},
{
Name: "bad-pcs",
CFlags: []string{"-fsanitize-coverage=trace-pc", "-g"},
Progs: []Prog{{Data: "data", PCs: []uint64{0x1, 0x2}}},
Result: `coverage \(2\) doesn't match coverage callbacks`,
},
{
Name: "good",
CFlags: []string{"-fsanitize-coverage=trace-pc", "-g"},
},
}
t.Parallel()
for os, arches := range targets.List {
if os == "test" {
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 {
test := test
t.Run("no-coverage", func(t *testing.T) {
t.Parallel()
testReportGenerator(t, target, test)
})
}
})
}
}
}
func testReportGenerator(t *testing.T, target *targets.Target, test Test) {
rep, 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)
}
_ = rep
}
func generateReport(t *testing.T, target *targets.Target, test Test) ([]byte, error) {
src, err := osutil.TempFile("syz-cover-test-src")
if err != nil {
t.Fatal(err)
}
defer os.Remove(src)
if err := osutil.WriteFile(src, []byte(`
void __sanitizer_cov_trace_pc() {}
int main() {}
`)); err != nil {
t.Fatal(err)
}
bin, err := osutil.TempFile("syz-cover-test-bin")
if err != nil {
t.Fatal(err)
}
defer os.Remove(bin)
flags := append(append([]string{
"-o", bin,
"-x", "c", src,
}, target.CFlags...), test.CFlags...)
if _, err := osutil.RunCmd(time.Hour, "", target.CCompiler, flags...); err != nil {
t.Fatal(err)
}
rg, err := MakeReportGenerator(target, bin, filepath.Dir(src), filepath.Dir(src))
if err != nil {
return nil, err
}
if test.Result == "" {
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]
var pcs []uint64
for off := 0; off < main.Size; off++ {
pcs = append(pcs, main.Addr+uint64(off))
}
test.Progs = append(test.Progs, Prog{Data: "main", PCs: pcs})
}
out := new(bytes.Buffer)
if err := rg.Do(out, test.Progs); err != nil {
return nil, err
}
return out.Bytes(), nil
}

View File

@ -67,6 +67,8 @@ type osCommon struct {
CPP string
// Common CFLAGS for this OS.
cflags []string
// If set, this OS uses $SOURCEDIR to locate the toolchain.
NeedsSourceDir bool
}
func Get(OS, arch string) *Target {
@ -356,6 +358,7 @@ var oses = map[string]osCommon{
ExecutorUsesShmem: true,
ExecutorUsesForkServer: true,
KernelObject: "netbsd.gdb",
NeedsSourceDir: true,
},
"openbsd": {
SyscallNumbers: true,
@ -373,6 +376,7 @@ var oses = map[string]osCommon{
HostFuzzer: true,
SyzExecutorCmd: "syz-executor",
KernelObject: "zircon.elf",
NeedsSourceDir: true,
},
"windows": {
SyscallNumbers: false,
@ -389,6 +393,7 @@ var oses = map[string]osCommon{
ExecutorUsesForkServer: true,
HostFuzzer: true,
KernelObject: "akaros-kernel-64b",
NeedsSourceDir: true,
},
"trusty": {
SyscallNumbers: true,
@ -501,6 +506,9 @@ func (target *Target) lazyInit() {
// On CI we want to fail loudly if cross-compilation breaks.
if _, err := exec.LookPath(target.CCompiler); err != nil {
target.BrokenCompiler = fmt.Sprintf("%v is missing", target.CCompiler)
if target.NeedsSourceDir && os.Getenv("SOURCEDIR") == "" {
target.BrokenCompiler = "SOURCEDIR is not set"
}
return
}
}

View File

@ -15,6 +15,7 @@ import (
"github.com/google/syzkaller/pkg/cover"
"github.com/google/syzkaller/pkg/osutil"
"github.com/google/syzkaller/sys/targets"
)
var (
@ -24,27 +25,27 @@ var (
reportGenerator *cover.ReportGenerator
)
func initCover(kernelObj, kernelObjName, kernelSrc, kernelBuildSrc, arch, OS string) error {
func initCover(target *targets.Target, kernelObj, kernelSrc, kernelBuildSrc string) error {
initCoverOnce.Do(func() {
if kernelObj == "" {
initCoverError = fmt.Errorf("kernel_obj is not specified")
return
}
vmlinux := filepath.Join(kernelObj, kernelObjName)
reportGenerator, initCoverError = cover.MakeReportGenerator(vmlinux, kernelSrc, kernelBuildSrc, arch)
vmlinux := filepath.Join(kernelObj, target.KernelObject)
reportGenerator, initCoverError = cover.MakeReportGenerator(target, vmlinux, kernelSrc, kernelBuildSrc)
if initCoverError != nil {
return
}
initCoverVMOffset, initCoverError = getVMOffset(vmlinux, OS)
initCoverVMOffset, initCoverError = getVMOffset(vmlinux, target.OS)
})
return initCoverError
}
func coverToPCs(cov []uint32, arch string) []uint64 {
func coverToPCs(target *targets.Target, cov []uint32) []uint64 {
pcs := make([]uint64, 0, len(cov))
for _, pc := range cov {
fullPC := cover.RestorePC(pc, initCoverVMOffset)
prevPC := cover.PreviousInstructionPC(arch, fullPC)
prevPC := cover.PreviousInstructionPC(target, fullPC)
pcs = append(pcs, prevPC)
}
return pcs

View File

@ -218,8 +218,7 @@ func (mgr *Manager) httpCover(w http.ResponseWriter, r *http.Request) {
}
// Note: initCover is executed without mgr.mu because it takes very long time
// (but it only reads config and it protected by initCoverOnce).
if err := initCover(mgr.cfg.KernelObj, mgr.sysTarget.KernelObject,
mgr.cfg.KernelSrc, mgr.cfg.KernelBuildSrc, mgr.cfg.TargetVMArch, mgr.cfg.TargetOS); err != nil {
if err := initCover(mgr.sysTarget, mgr.cfg.KernelObj, mgr.cfg.KernelSrc, mgr.cfg.KernelBuildSrc); err != nil {
http.Error(w, fmt.Sprintf("failed to generate coverage profile: %v", err), http.StatusInternalServerError)
return
}
@ -234,7 +233,7 @@ func (mgr *Manager) httpCoverCover(w http.ResponseWriter, r *http.Request) {
inp := mgr.corpus[sig]
progs = append(progs, cover.Prog{
Data: string(inp.Prog),
PCs: coverToPCs(inp.Cover, mgr.cfg.TargetVMArch),
PCs: coverToPCs(mgr.sysTarget, inp.Cover),
})
} else {
call := r.FormValue("call")
@ -244,7 +243,7 @@ func (mgr *Manager) httpCoverCover(w http.ResponseWriter, r *http.Request) {
}
progs = append(progs, cover.Prog{
Data: string(inp.Prog),
PCs: coverToPCs(inp.Cover, mgr.cfg.TargetVMArch),
PCs: coverToPCs(mgr.sysTarget, inp.Cover),
})
}
}
@ -391,8 +390,7 @@ func (mgr *Manager) httpReport(w http.ResponseWriter, r *http.Request) {
func (mgr *Manager) httpRawCover(w http.ResponseWriter, r *http.Request) {
// Note: initCover is executed without mgr.mu because it takes very long time
// (but it only reads config and it protected by initCoverOnce).
if err := initCover(mgr.cfg.KernelObj, mgr.sysTarget.KernelObject, mgr.cfg.KernelSrc,
mgr.cfg.KernelBuildSrc, mgr.cfg.TargetArch, mgr.cfg.TargetOS); err != nil {
if err := initCover(mgr.sysTarget, mgr.cfg.KernelObj, mgr.cfg.KernelSrc, mgr.cfg.KernelBuildSrc); err != nil {
http.Error(w, initCoverError.Error(), http.StatusInternalServerError)
return
}
@ -407,7 +405,7 @@ func (mgr *Manager) httpRawCover(w http.ResponseWriter, r *http.Request) {
for pc := range cov {
covArray = append(covArray, pc)
}
pcs := coverToPCs(covArray, mgr.cfg.TargetVMArch)
pcs := coverToPCs(mgr.sysTarget, covArray)
sort.Slice(pcs, func(i, j int) bool {
return pcs[i] < pcs[j]
})

View File

@ -125,9 +125,9 @@ func main() {
if err != nil {
log.Fatalf("%v", err)
}
sysTarget := targets.Get(cfg.TargetOS, cfg.TargetArch)
sysTarget := targets.Get(cfg.TargetOS, cfg.TargetVMArch)
if sysTarget == nil {
log.Fatalf("unsupported OS/arch: %v/%v", cfg.TargetOS, cfg.TargetArch)
log.Fatalf("unsupported OS/arch: %v/%v", cfg.TargetOS, cfg.TargetVMArch)
}
syscalls, err := mgrconfig.ParseEnabledSyscalls(target, cfg.EnabledSyscalls, cfg.DisabledSyscalls)
if err != nil {

View File

@ -66,7 +66,7 @@ func main() {
failf("%v", err)
}
kernelObj := filepath.Join(*flagKernelObj, target.KernelObject)
rg, err := cover.MakeReportGenerator(kernelObj, *flagKernelSrc, *flagKernelBuildSrc, *flagArch)
rg, err := cover.MakeReportGenerator(target, kernelObj, *flagKernelSrc, *flagKernelBuildSrc)
if err != nil {
failf("%v", err)
}