// 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> `))