syzkaller/syz-manager/cover.go
Dmitry Vyukov 2a075d57ab pkg/report: allow to specify suppressions per OS
Currently all (linux-specific) suppressions are hardcoded in mgrconfig.
This is very wrong. Move them to pkg/report and allow to specify per OS.
Add gvisor-specific suppressions.
This required a bit of refactoring. Introduce mgrconfig.KernelObj finally.
Make report.NewReporter and vm.Create accept mgrconfig directly
instead of passing it as multiple scattered args.
Remove tools/syz-parse and it always did the same as tools/syz-symbolize.
Simplify global vars in syz-manager/cover.go.
Create reporter eagerly in manager. Use sort.Slice more.
Overall -90 lines removed.
2018-06-22 16:40:45 +02:00

487 lines
11 KiB
Go

// Copyright 2015 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.
package main
import (
"bufio"
"bytes"
"fmt"
"html/template"
"io"
"io/ioutil"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/google/syzkaller/pkg/cover"
"github.com/google/syzkaller/pkg/hash"
"github.com/google/syzkaller/pkg/osutil"
"github.com/google/syzkaller/pkg/symbolizer"
)
type symbol struct {
start uint64
end uint64
name string
}
type coverage struct {
line int
covered bool
}
var (
initCoverOnce sync.Once
initCoverError error
initCoverSymbols []symbol
initCoverPCs []uint64
initCoverVMOffset uint32
)
func initCover(kernelObj, arch string) error {
if kernelObj == "" {
return fmt.Errorf("kernel_obj is not specified")
}
vmlinux := filepath.Join(kernelObj, "vmlinux")
symbols, err := symbolizer.ReadSymbols(vmlinux)
if err != nil {
return fmt.Errorf("failed to run nm on %v: %v", vmlinux, err)
}
for name, ss := range symbols {
for _, s := range ss {
initCoverSymbols = append(initCoverSymbols, symbol{s.Addr, s.Addr + uint64(s.Size), name})
}
}
sort.Slice(initCoverSymbols, func(i, j int) bool {
return initCoverSymbols[i].start < initCoverSymbols[j].start
})
initCoverPCs, err = coveredPCs(arch, vmlinux)
if err != nil {
return fmt.Errorf("failed to run objdump on %v: %v", vmlinux, err)
}
sort.Slice(initCoverPCs, func(i, j int) bool {
return initCoverPCs[i] < initCoverPCs[j]
})
initCoverVMOffset, err = getVMOffset(vmlinux)
return err
}
func generateCoverHTML(w io.Writer, kernelObj, arch string, cov cover.Cover) error {
if len(cov) == 0 {
return fmt.Errorf("no coverage data available")
}
initCoverOnce.Do(func() { initCoverError = initCover(kernelObj, arch) })
if initCoverError != nil {
return initCoverError
}
pcs := make([]uint64, 0, len(cov))
for pc := range cov {
fullPC := cover.RestorePC(pc, initCoverVMOffset)
prevPC := previousInstructionPC(arch, fullPC)
pcs = append(pcs, prevPC)
}
vmlinux := filepath.Join(kernelObj, "vmlinux")
uncovered, err := uncoveredPcsInFuncs(vmlinux, pcs)
if err != nil {
return err
}
coveredFrames, _, err := symbolize(vmlinux, pcs)
if err != nil {
return err
}
if len(coveredFrames) == 0 {
return fmt.Errorf("'%s' does not have debug info (set CONFIG_DEBUG_INFO=y)", vmlinux)
}
uncoveredFrames, prefix, err := symbolize(vmlinux, uncovered)
if err != nil {
return err
}
var d templateData
for f, covered := range fileSet(coveredFrames, uncoveredFrames) {
lines, err := parseFile(f)
if err != nil {
return err
}
coverage := 0
var buf bytes.Buffer
for i, ln := range lines {
if len(covered) > 0 && covered[0].line == i+1 {
if covered[0].covered {
buf.Write([]byte("<span id='covered'>"))
buf.Write(ln)
buf.Write([]byte("</span> /*covered*/\n"))
coverage++
} else {
buf.Write([]byte("<span id='uncovered'>"))
buf.Write(ln)
buf.Write([]byte("</span>\n"))
}
covered = covered[1:]
} else {
buf.Write(ln)
buf.Write([]byte{'\n'})
}
}
f = filepath.Clean(strings.TrimPrefix(f, prefix))
d.Files = append(d.Files, &templateFile{
ID: hash.String([]byte(f)),
Name: f,
Body: template.HTML(buf.String()),
Coverage: coverage,
})
}
sort.Sort(templateFileArray(d.Files))
return coverTemplate.Execute(w, d)
}
func fileSet(covered, uncovered []symbolizer.Frame) map[string][]coverage {
files := make(map[string]map[int]bool)
funcs := make(map[string]bool)
for _, frame := range covered {
if files[frame.File] == nil {
files[frame.File] = make(map[int]bool)
}
files[frame.File][frame.Line] = true
funcs[frame.Func] = true
}
for _, frame := range uncovered {
if !funcs[frame.Func] {
continue
}
if files[frame.File] == nil {
files[frame.File] = make(map[int]bool)
}
if !files[frame.File][frame.Line] {
files[frame.File][frame.Line] = false
}
}
res := make(map[string][]coverage)
for f, lines := range files {
sorted := make([]coverage, 0, len(lines))
for ln, covered := range lines {
sorted = append(sorted, coverage{ln, covered})
}
sort.Slice(sorted, func(i, j int) bool {
return sorted[i].line < sorted[j].line
})
res[f] = sorted
}
return res
}
func parseFile(fn string) ([][]byte, error) {
data, err := ioutil.ReadFile(fn)
if err != nil {
return nil, err
}
htmlReplacer := strings.NewReplacer(">", "&gt;", "<", "&lt;", "&", "&amp;", "\t", " ")
var lines [][]byte
for {
idx := bytes.IndexByte(data, '\n')
if idx == -1 {
break
}
lines = append(lines, []byte(htmlReplacer.Replace(string(data[:idx]))))
data = data[idx+1:]
}
if len(data) != 0 {
lines = append(lines, data)
}
return lines, nil
}
func getVMOffset(vmlinux string) (uint32, error) {
out, err := osutil.RunCmd(time.Hour, "", "readelf", "-SW", vmlinux)
if err != nil {
return 0, err
}
s := bufio.NewScanner(bytes.NewReader(out))
var addr uint32
for s.Scan() {
ln := s.Text()
pieces := strings.Fields(ln)
for i := 0; i < len(pieces); i++ {
if pieces[i] != "PROGBITS" {
continue
}
v, err := strconv.ParseUint("0x"+pieces[i+1], 0, 64)
if err != nil {
return 0, fmt.Errorf("failed to parse addr in readelf output: %v", err)
}
if v == 0 {
continue
}
v32 := (uint32)(v >> 32)
if addr == 0 {
addr = v32
}
if addr != v32 {
return 0, fmt.Errorf("different section offsets in a single binary")
}
}
}
return addr, nil
}
// uncoveredPcsInFuncs returns uncovered PCs with __sanitizer_cov_trace_pc calls in functions containing pcs.
func uncoveredPcsInFuncs(vmlinux string, pcs []uint64) ([]uint64, error) {
handledFuncs := make(map[uint64]bool)
uncovered := make(map[uint64]bool)
for _, pc := range pcs {
idx := sort.Search(len(initCoverSymbols), func(i int) bool {
return pc < initCoverSymbols[i].end
})
if idx == len(initCoverSymbols) {
continue
}
s := initCoverSymbols[idx]
if pc < s.start || pc > s.end {
continue
}
if !handledFuncs[s.start] {
handledFuncs[s.start] = true
startPC := sort.Search(len(initCoverPCs), func(i int) bool {
return s.start <= initCoverPCs[i]
})
endPC := sort.Search(len(initCoverPCs), func(i int) bool {
return s.end < initCoverPCs[i]
})
for _, pc1 := range initCoverPCs[startPC:endPC] {
uncovered[pc1] = true
}
}
delete(uncovered, pc)
}
uncoveredPCs := make([]uint64, 0, len(uncovered))
for pc := range uncovered {
uncoveredPCs = append(uncoveredPCs, pc)
}
return uncoveredPCs, nil
}
// coveredPCs returns list of PCs of __sanitizer_cov_trace_pc calls in binary bin.
func coveredPCs(arch, bin string) ([]uint64, error) {
cmd := osutil.Command("objdump", "-d", "--no-show-raw-insn", bin)
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
defer stdout.Close()
if err := cmd.Start(); err != nil {
return nil, err
}
defer cmd.Wait()
var pcs []uint64
s := bufio.NewScanner(stdout)
traceFunc := []byte(" <__sanitizer_cov_trace_pc>")
var callInsn []byte
switch arch {
case "amd64":
// ffffffff8100206a: callq ffffffff815cc1d0 <__sanitizer_cov_trace_pc>
callInsn = []byte("\tcallq ")
case "386":
// c1000102: call c10001f0 <__sanitizer_cov_trace_pc>
callInsn = []byte("\tcall ")
case "arm64":
// ffff0000080d9cc0: bl ffff00000820f478 <__sanitizer_cov_trace_pc>
callInsn = []byte("\tbl\t")
case "arm":
// 8010252c: bl 801c3280 <__sanitizer_cov_trace_pc>
callInsn = []byte("\tbl\t")
case "ppc64le":
// c00000000006d904: bl c000000000350780 <.__sanitizer_cov_trace_pc>
callInsn = []byte("\tbl ")
traceFunc = []byte(" <.__sanitizer_cov_trace_pc>")
default:
panic("unknown arch")
}
for s.Scan() {
ln := s.Bytes()
if pos := bytes.Index(ln, callInsn); pos == -1 {
continue
} else if !bytes.Contains(ln[pos:], traceFunc) {
continue
}
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 err := s.Err(); err != nil {
return nil, err
}
return pcs, nil
}
func symbolize(vmlinux string, pcs []uint64) ([]symbolizer.Frame, string, error) {
symb := symbolizer.NewSymbolizer()
defer symb.Close()
frames, err := symb.SymbolizeArray(vmlinux, pcs)
if err != nil {
return nil, "", err
}
prefix := ""
for i := range frames {
frame := &frames[i]
frame.PC--
if prefix == "" {
prefix = frame.File
} else {
i := 0
for ; i < len(prefix) && i < len(frame.File); i++ {
if prefix[i] != frame.File[i] {
break
}
}
prefix = prefix[:i]
}
}
return frames, prefix, nil
}
func previousInstructionPC(arch string, pc uint64) uint64 {
switch arch {
case "amd64":
return pc - 5
case "386":
return pc - 1
case "arm64":
return pc - 4
case "arm":
// THUMB instructions are 2 or 4 bytes with low bit set.
// ARM instructions are always 4 bytes.
return (pc - 3) & ^uint64(1)
case "ppc64le":
return pc - 4
default:
panic("unknown arch")
}
}
type templateData struct {
Files []*templateFile
}
type templateFile struct {
ID string
Name string
Body template.HTML
Coverage int
}
type templateFileArray []*templateFile
func (a templateFileArray) Len() int { return len(a) }
func (a templateFileArray) Less(i, j int) bool {
n1 := a[i].Name
n2 := a[j].Name
// Move include files to the bottom.
if len(n1) != 0 && len(n2) != 0 {
if n1[0] != '.' && n2[0] == '.' {
return true
}
if n1[0] == '.' && n2[0] != '.' {
return false
}
}
return n1 < n2
}
func (a templateFileArray) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
var coverTemplate = template.Must(template.New("").Parse(`
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<style>
body {
background: white;
}
#topbar {
background: black;
position: fixed;
top: 0; left: 0; right: 0;
height: 42px;
border-bottom: 1px solid rgb(70, 70, 70);
}
#nav {
float: left;
margin-left: 10px;
margin-top: 10px;
}
#content {
font-family: 'Courier New', Courier, monospace;
color: rgb(70, 70, 70);
margin-top: 50px;
}
#covered {
color: rgb(0, 0, 0);
font-weight: bold;
}
#uncovered {
color: rgb(255, 0, 0);
font-weight: bold;
}
</style>
</head>
<body>
<div id="topbar">
<div id="nav">
<select id="files">
{{range $f := .Files}}
<option value="{{$f.ID}}">{{$f.Name}} ({{$f.Coverage}})</option>
{{end}}
</select>
</div>
</div>
<div id="content">
{{range $i, $f := .Files}}
<pre class="file" id="{{$f.ID}}" {{if $i}}style="display: none;"{{end}}>{{$f.Body}}</pre>{{end}}
</div>
</body>
<script>
(function() {
var files = document.getElementById('files');
var visible = document.getElementById(files.value);
if (window.location.hash) {
var hash = window.location.hash.substring(1);
for (var i = 0; i < files.options.length; i++) {
if (files.options[i].value === hash) {
files.selectedIndex = i;
visible.style.display = 'none';
visible = document.getElementById(files.value);
visible.style.display = 'block';
break;
}
}
}
files.addEventListener('change', onChange, false);
function onChange() {
visible.style.display = 'none';
visible = document.getElementById(files.value);
visible.style.display = 'block';
window.scrollTo(0, 0);
window.location.hash = files.value;
}
})();
</script>
</html>
`))