blob: 9268f733ddd85e68d02a2310a540aa84b1b651c3 [file] [log] [blame]
// Copyright 2018 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 cover
import (
"bufio"
"bytes"
"fmt"
"html"
"html/template"
"io"
"io/ioutil"
"math"
"path/filepath"
"sort"
"strconv"
"strings"
"github.com/google/syzkaller/pkg/osutil"
"github.com/google/syzkaller/pkg/symbolizer"
)
type ReportGenerator struct {
srcDir string
buildDir string
symbols []symbol
pcs map[uint64][]symbolizer.Frame
}
type Prog struct {
Data string
PCs []uint64
}
type symbol struct {
start uint64
end uint64
}
func MakeReportGenerator(vmlinux, srcDir, buildDir, arch string) (*ReportGenerator, error) {
rg := &ReportGenerator{
srcDir: srcDir,
buildDir: buildDir,
pcs: make(map[uint64][]symbolizer.Frame),
}
errc := make(chan error)
go func() {
var err error
rg.symbols, err = readSymbols(vmlinux)
errc <- err
}()
frames, err := objdumpAndSymbolize(vmlinux, arch)
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
}
for _, frame := range frames {
rg.pcs[frame.PC] = append(rg.pcs[frame.PC], frame)
}
return rg, nil
}
type file struct {
lines map[int]line
totalPCs map[uint64]bool
coverPCs map[uint64]bool
totalInline map[int]bool
coverInline map[int]bool
}
type line struct {
count map[int]bool
prog int
uncovered bool
symbolCovered bool
}
func (rg *ReportGenerator) Do(w io.Writer, progs []Prog) error {
coveredPCs := make(map[uint64]bool)
symbols := make(map[uint64]bool)
files := make(map[string]*file)
for progIdx, prog := range progs {
for _, pc := range prog.PCs {
symbols[rg.findSymbol(pc)] = true
frames, ok := rg.pcs[pc]
if !ok {
continue
}
coveredPCs[pc] = true
for _, frame := range frames {
f := getFile(files, frame.File)
ln := f.lines[frame.Line]
if ln.count == nil {
ln.count = make(map[int]bool)
ln.prog = -1
}
ln.count[progIdx] = true
if ln.prog == -1 || len(prog.Data) < len(progs[ln.prog].Data) {
ln.prog = progIdx
}
f.lines[frame.Line] = ln
}
}
}
if len(coveredPCs) == 0 {
return fmt.Errorf("no coverage data available")
}
for pc, frames := range rg.pcs {
covered := coveredPCs[pc]
for _, frame := range frames {
f := getFile(files, frame.File)
if frame.Inline {
f.totalInline[frame.Line] = true
if covered {
f.coverInline[frame.Line] = true
}
} else {
f.totalPCs[pc] = true
if covered {
f.coverPCs[pc] = true
}
}
if !covered {
ln := f.lines[frame.Line]
if !frame.Inline || len(ln.count) == 0 {
ln.uncovered = true
ln.symbolCovered = symbols[rg.findSymbol(pc)]
f.lines[frame.Line] = ln
}
}
}
}
return rg.generate(w, progs, files)
}
func getFile(files map[string]*file, name string) *file {
f := files[name]
if f == nil {
f = &file{
lines: make(map[int]line),
totalPCs: make(map[uint64]bool),
coverPCs: make(map[uint64]bool),
totalInline: make(map[int]bool),
coverInline: make(map[int]bool),
}
files[name] = f
}
return f
}
func (rg *ReportGenerator) generate(w io.Writer, progs []Prog, files map[string]*file) error {
d := &templateData{
Root: new(templateDir),
}
for fname, file := range files {
if !strings.HasPrefix(fname, rg.buildDir) {
return fmt.Errorf("path '%s' doesn't match build dir '%s'", fname, rg.buildDir)
}
// Trim the existing build dir
remain := filepath.Clean(strings.TrimPrefix(fname, rg.buildDir))
// Add the current kernel source dir
fname = filepath.Join(rg.srcDir, remain)
pos := d.Root
path := ""
for {
if path != "" {
path += "/"
}
sep := strings.IndexByte(remain, filepath.Separator)
if sep == -1 {
path += remain
break
}
dir := remain[:sep]
path += dir
if pos.Dirs == nil {
pos.Dirs = make(map[string]*templateDir)
}
if pos.Dirs[dir] == nil {
pos.Dirs[dir] = &templateDir{
templateBase: templateBase{
Path: path,
Name: dir,
},
}
}
pos = pos.Dirs[dir]
remain = remain[sep+1:]
}
f := &templateFile{
templateBase: templateBase{
Path: path,
Name: remain,
Total: len(file.totalPCs) + len(file.totalInline),
Covered: len(file.coverPCs) + len(file.coverInline),
},
}
if f.Total == 0 {
return fmt.Errorf("%v: file does not have any coverage", fname)
}
pos.Files = append(pos.Files, f)
if len(file.lines) == 0 || f.Covered == 0 {
continue
}
lines, err := parseFile(fname)
if err != nil {
return err
}
var buf bytes.Buffer
for i, ln := range lines {
cov, ok := file.lines[i+1]
prog, class, count := "", "", " "
if ok {
if len(cov.count) != 0 {
if cov.prog != -1 {
prog = fmt.Sprintf("onclick='onProgClick(%v)'", cov.prog)
}
count = fmt.Sprintf("% 5v", len(cov.count))
class = "covered"
if cov.uncovered {
class = "both"
}
} else {
class = "weak-uncovered"
if cov.symbolCovered {
class = "uncovered"
}
}
}
buf.WriteString(fmt.Sprintf("<span class='count' %v>%v</span>", prog, count))
if class == "" {
buf.WriteByte(' ')
buf.Write(ln)
buf.WriteByte('\n')
} else {
buf.WriteString(fmt.Sprintf("<span class='%v'> ", class))
buf.Write(ln)
buf.WriteString("</span>\n")
}
}
d.Contents = append(d.Contents, template.HTML(buf.String()))
f.Index = len(d.Contents) - 1
}
for _, prog := range progs {
d.Progs = append(d.Progs, template.HTML(html.EscapeString(prog.Data)))
}
processDir(d.Root)
return coverTemplate.Execute(w, d)
}
func processDir(dir *templateDir) {
for len(dir.Dirs) == 1 && len(dir.Files) == 0 {
for _, child := range dir.Dirs {
dir.Name += "/" + child.Name
dir.Files = child.Files
dir.Dirs = child.Dirs
}
}
sort.Slice(dir.Files, func(i, j int) bool {
return dir.Files[i].Name < dir.Files[j].Name
})
for _, f := range dir.Files {
dir.Total += f.Total
dir.Covered += f.Covered
f.Percent = percent(f.Covered, f.Total)
}
for _, child := range dir.Dirs {
processDir(child)
dir.Total += child.Total
dir.Covered += child.Covered
}
dir.Percent = percent(dir.Covered, dir.Total)
if dir.Covered == 0 {
dir.Dirs = nil
dir.Files = nil
}
}
func percent(covered, total int) int {
f := math.Ceil(float64(covered) / float64(total) * 100)
if f == 100 && covered < total {
f = 99
}
return int(f)
}
func (rg *ReportGenerator) findSymbol(pc uint64) uint64 {
idx := sort.Search(len(rg.symbols), func(i int) bool {
return pc < rg.symbols[i].end
})
if idx == len(rg.symbols) {
return 0
}
s := rg.symbols[idx]
if pc < s.start || pc > s.end {
return 0
}
return s.start
}
func readSymbols(obj string) ([]symbol, error) {
raw, err := symbolizer.ReadSymbols(obj)
if err != nil {
return nil, fmt.Errorf("failed to run nm on %v: %v", obj, err)
}
var symbols []symbol
for _, ss := range raw {
for _, s := range ss {
symbols = append(symbols, symbol{
start: s.Addr,
end: s.Addr + uint64(s.Size),
})
}
}
sort.Slice(symbols, func(i, j int) bool {
return symbols[i].start < symbols[j].start
})
return symbols, nil
}
// 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)
pcchan := make(chan []uint64, 10)
var frames []symbolizer.Frame
go func() {
symb := symbolizer.NewSymbolizer()
defer symb.Close()
var err error
for pcs := range pcchan {
if err != nil {
continue
}
frames1, err1 := symb.SymbolizeArray(obj, pcs)
if err1 != nil {
err = fmt.Errorf("failed to symbolize: %v", err1)
}
frames = append(frames, frames1...)
}
errc <- err
}()
cmd := osutil.Command("objdump", "-d", "--no-show-raw-insn", obj)
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
defer stdout.Close()
if err := cmd.Start(); err != nil {
return nil, fmt.Errorf("failed to run objdump on %v: %v", obj, err)
}
defer func() {
cmd.Process.Kill()
cmd.Wait()
}()
s := bufio.NewScanner(stdout)
callInsnS, traceFuncS := archCallInsn(arch)
callInsn, traceFunc := []byte(callInsnS), []byte(traceFuncS)
var pcs []uint64
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 len(pcs) != 0 {
pcchan <- pcs
}
close(pcchan)
if err := s.Err(); err != nil {
return nil, fmt.Errorf("failed to run objdump output: %v", err)
}
if err := <-errc; err != nil {
return nil, err
}
return frames, nil
}
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 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(fmt.Sprintf("unknown arch %q", arch))
}
}
func archCallInsn(arch string) (string, string) {
const callName = " <__sanitizer_cov_trace_pc>"
switch arch {
case "amd64":
// ffffffff8100206a: callq ffffffff815cc1d0 <__sanitizer_cov_trace_pc>
return "\tcallq ", callName
case "386":
// c1000102: call c10001f0 <__sanitizer_cov_trace_pc>
return "\tcall ", callName
case "arm64":
// ffff0000080d9cc0: bl ffff00000820f478 <__sanitizer_cov_trace_pc>
return "\tbl\t", callName
case "arm":
// 8010252c: bl 801c3280 <__sanitizer_cov_trace_pc>
return "\tbl\t", callName
case "ppc64le":
// c00000000006d904: bl c000000000350780 <.__sanitizer_cov_trace_pc>
return "\tbl ", " <.__sanitizer_cov_trace_pc>"
default:
panic(fmt.Sprintf("unknown arch %q", arch))
}
}
type templateData struct {
Root *templateDir
Contents []template.HTML
Progs []template.HTML
}
type templateBase struct {
Name string
Path string
Total int
Covered int
Percent int
}
type templateDir struct {
templateBase
Dirs map[string]*templateDir
Files []*templateFile
}
type templateFile struct {
templateBase
Index int
}
var coverTemplate = template.Must(template.New("").Parse(`
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<style>
.file {
display: none;
margin: 0;
padding: 0;
}
.count {
font-weight: bold;
border-right: 1px solid #ddd;
padding-right: 4px;
cursor: zoom-in;
}
.split {
height: 100%;
position: fixed;
z-index: 1;
top: 0;
overflow-x: hidden;
}
.tree {
left: 0;
width: 24%;
}
.right {
border-left: 2px solid #444;
right: 0;
width: 76%;
font-family: 'Courier New', Courier, monospace;
color: rgb(80, 80, 80);
}
.cover {
float: right;
width: 120px;
padding-right: 4px;
}
.cover-right {
float: right;
}
.covered {
color: rgb(0, 0, 0);
font-weight: bold;
}
.uncovered {
color: rgb(255, 0, 0);
font-weight: bold;
}
.weak-uncovered {
color: rgb(200, 0, 0);
}
.both {
color: rgb(200, 100, 0);
font-weight: bold;
}
ul, #dir_list {
list-style-type: none;
padding-left: 16px;
}
#dir_list {
margin: 0;
padding: 0;
}
.hover:hover {
background: #ffff99;
}
.caret {
cursor: pointer;
user-select: none;
}
.caret::before {
color: black;
content: "\25B6";
display: inline-block;
margin-right: 3px;
}
.caret-down::before {
transform: rotate(90deg);
}
.nested {
display: none;
}
.active {
display: block;
}
</style>
</head>
<body>
<div class="split tree">
<ul id="dir_list">
{{template "dir" .Root}}
</ul>
</div>
<div id="right_pane" class="split right">
{{range $i, $f := .Contents}}
<pre class="file" id="contents_{{$i}}">{{$f}}</pre>
{{end}}
{{range $i, $p := .Progs}}
<pre class="file" id="prog_{{$i}}">{{$p}}</pre>
{{end}}
</div>
</body>
<script>
(function() {
var toggler = document.getElementsByClassName("caret");
for (var i = 0; i < toggler.length; i++) {
toggler[i].addEventListener("click", function() {
this.parentElement.querySelector(".nested").classList.toggle("active");
this.classList.toggle("caret-down");
});
}
if (window.location.hash) {
var hash = decodeURIComponent(window.location.hash.substring(1)).split("/");
var path = "path";
for (var i = 0; i < hash.length; i++) {
path += "/" + hash[i];
var elem = document.getElementById(path);
if (elem)
elem.click();
}
}
})();
var visible;
function onFileClick(index) {
if (visible)
visible.style.display = 'none';
visible = document.getElementById("contents_" + index);
visible.style.display = 'block';
document.getElementById("right_pane").scrollTo(0, 0);
}
function onProgClick(index) {
if (visible)
visible.style.display = 'none';
visible = document.getElementById("prog_" + index);
visible.style.display = 'block';
document.getElementById("right_pane").scrollTo(0, 0);
}
</script>
</html>
{{define "dir"}}
{{range $dir := .Dirs}}
<li>
<span id="path/{{$dir.Path}}" class="caret hover">
{{$dir.Name}}
<span class="cover hover">
{{if $dir.Covered}}{{$dir.Percent}}%{{else}}---{{end}}
<span class="cover-right">of {{$dir.Total}}</span>
</span>
</span>
<ul class="nested">
{{template "dir" $dir}}
</ul>
</li>
{{end}}
{{range $file := .Files}}
<li><span class="hover">
{{if $file.Covered}}
<a href="#{{$file.Path}}" id="path/{{$file.Path}}" onclick="onFileClick({{$file.Index}})">
{{$file.Name}}<span class="cover hover">
{{$file.Percent}}%
<span class="cover-right">of {{$file.Total}}</span>
</span>
</a>
{{else}}
{{$file.Name}}<span class="cover hover">---<span class="cover-right">
of {{$file.Total}}</span></span>
{{end}}
</span></li>
{{end}}
{{end}}
`))