mirror of
https://github.com/reactos/syzkaller.git
synced 2024-11-24 20:09:49 +00:00
f69c5fcd76
If vmlinux built from different tree from kernelSrc, cover page will failed to show. So match only the path without prefix.
491 lines
12 KiB
Go
491 lines
12 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, kernelSrc, 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) {
|
|
remain := strings.TrimPrefix(f, prefix)
|
|
if kernelSrc != "" {
|
|
f = filepath.Join(kernelSrc, remain)
|
|
}
|
|
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(remain)
|
|
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(">", ">", "<", "<", "&", "&", "\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>
|
|
`))
|