syzkaller/pkg/report/report.go
Dmitry Vyukov d9ec038e02 pkg/report: refactor argument passing
We now pass 5 arguments through a bunch of functions,
this is quite inconvinient when the set of arguments changes.
Incapsulate all arguments in a struct and pass/store it as a whole.
2019-07-23 09:15:43 +02:00

589 lines
16 KiB
Go

// Copyright 2016 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 report contains functions that process kernel output,
// detect/extract crash messages, symbolize them, etc.
package report
import (
"bufio"
"bytes"
"fmt"
"regexp"
"strings"
"github.com/google/syzkaller/pkg/mgrconfig"
"github.com/google/syzkaller/sys/targets"
)
type Reporter interface {
// ContainsCrash searches kernel console output for oops messages.
ContainsCrash(output []byte) bool
// Parse extracts information about oops from console output.
// Returns nil if no oops found.
Parse(output []byte) *Report
// Symbolize symbolizes rep.Report and fills in Maintainers.
Symbolize(rep *Report) error
}
type Report struct {
// Title contains a representative description of the first oops.
Title string
// Bug type (e.g. hang, memory leak, etc).
Type Type
// The indicative function name.
Frame string
// Report contains whole oops text.
Report []byte
// Output contains whole raw console output as passed to Reporter.Parse.
Output []byte
// StartPos/EndPos denote region of output with oops message(s).
StartPos int
EndPos int
// Suppressed indicates whether the report should not be reported to user.
Suppressed bool
// Corrupted indicates whether the report is truncated of corrupted in some other way.
Corrupted bool
// CorruptedReason contains reason why the report is marked as corrupted.
CorruptedReason string
// Maintainers is list of maintainer emails (filled in by Symbolize).
Maintainers []string
// guiltyFile is the source file that we think is to blame for the crash (filled in by Symbolize).
guiltyFile string
// reportPrefixLen is length of additional prefix lines that we added before actual crash report.
reportPrefixLen int
}
type Type int
const (
Unknown Type = iota
Hang
MemoryLeak
UnexpectedReboot
)
func (t Type) String() string {
switch t {
case Unknown:
return "UNKNOWN"
case Hang:
return "HANG"
case MemoryLeak:
return "LEAK"
case UnexpectedReboot:
return "REBOOT"
default:
panic("unknown report type")
}
}
// NewReporter creates reporter for the specified OS/Type.
func NewReporter(cfg *mgrconfig.Config) (Reporter, error) {
typ := cfg.TargetOS
if cfg.Type == "gvisor" {
typ = cfg.Type
}
ctor := ctors[typ]
if ctor == nil {
return nil, fmt.Errorf("unknown OS: %v", typ)
}
ignores, err := compileRegexps(cfg.Ignores)
if err != nil {
return nil, err
}
target := targets.Get(cfg.TargetOS, cfg.TargetArch)
if target == nil && typ != "gvisor" {
return nil, fmt.Errorf("unknown target %v/%v", cfg.TargetOS, cfg.TargetArch)
}
config := &config{
target: target,
kernelSrc: cfg.KernelSrc,
kernelBuildSrc: cfg.KernelBuildSrc,
kernelObj: cfg.KernelObj,
ignores: ignores,
}
rep, suppressions, err := ctor(config)
if err != nil {
return nil, err
}
supps, err := compileRegexps(append(suppressions, cfg.Suppressions...))
if err != nil {
return nil, err
}
return &reporterWrapper{rep, supps, typ}, nil
}
const (
unexpectedKernelReboot = "unexpected kernel reboot"
memoryLeakPrefix = "memory leak in "
)
var ctors = map[string]fn{
"akaros": ctorAkaros,
"linux": ctorLinux,
"gvisor": ctorGvisor,
"freebsd": ctorFreebsd,
"netbsd": ctorNetbsd,
"openbsd": ctorOpenbsd,
"fuchsia": ctorFuchsia,
"windows": ctorStub,
}
type config struct {
target *targets.Target
kernelSrc string
kernelBuildSrc string
kernelObj string
ignores []*regexp.Regexp
}
type fn func(cfg *config) (Reporter, []string, error)
func compileRegexps(list []string) ([]*regexp.Regexp, error) {
compiled := make([]*regexp.Regexp, len(list))
for i, str := range list {
re, err := regexp.Compile(str)
if err != nil {
return nil, fmt.Errorf("failed to compile %q: %v", str, err)
}
compiled[i] = re
}
return compiled, nil
}
type reporterWrapper struct {
Reporter
suppressions []*regexp.Regexp
typ string
}
func (wrap *reporterWrapper) Parse(output []byte) *Report {
rep := wrap.Reporter.Parse(output)
if rep == nil {
return nil
}
rep.Title = sanitizeTitle(replaceTable(dynamicTitleReplacement, rep.Title))
rep.Suppressed = matchesAny(rep.Output, wrap.suppressions)
if bytes.Contains(rep.Output, gceConsoleHangup) {
rep.Corrupted = true
}
rep.Type = extractReportType(rep)
if match := reportFrameRe.FindStringSubmatch(rep.Title); match != nil {
rep.Frame = match[1]
}
return rep
}
func extractReportType(rep *Report) Type {
// Type/frame extraction logic should be integrated with oops types.
// But for now we do this more ad-hoc analysis here to at least isolate
// the rest of the code base from report parsing.
if rep.Title == unexpectedKernelReboot {
return UnexpectedReboot
}
if strings.HasPrefix(rep.Title, memoryLeakPrefix) {
return MemoryLeak
}
if strings.HasPrefix(rep.Title, "INFO: rcu detected stall") ||
strings.HasPrefix(rep.Title, "INFO: task hung") ||
strings.HasPrefix(rep.Title, "BUG: soft lockup") {
return Hang
}
return Unknown
}
func IsSuppressed(reporter Reporter, output []byte) bool {
return matchesAny(output, reporter.(*reporterWrapper).suppressions) ||
bytes.Contains(output, gceConsoleHangup)
}
// GCE console connection sometimes fails with this message.
// The message frequently happens right after a kernel panic.
// So if we see it in output where we recognized a crash, we mark the report as corrupted
// because the crash message is usually truncated (maybe we don't even have the title line).
// If we see it in no output/lost connection reports then we mark them as suppressed instead
// because the crash itself may have been caused by the console connection error.
var gceConsoleHangup = []byte("serialport: VM disconnected.")
type replacement struct {
match *regexp.Regexp
replacement string
}
func replaceTable(replacements []replacement, str string) string {
for _, repl := range replacements {
str = repl.match.ReplaceAllString(str, repl.replacement)
}
return str
}
var dynamicTitleReplacement = []replacement{
{
// Executor PIDs are not interesting.
regexp.MustCompile(`syz-executor\.?[0-9]+((/|:)[0-9]+)?`),
"syz-executor",
},
{
// syzkaller binaries are coming from repro.
regexp.MustCompile(`syzkaller[0-9]+((/|:)[0-9]+)?`),
"syzkaller",
},
{
// Replace that everything looks like an address with "ADDR",
// addresses in descriptions can't be good regardless of the oops regexps.
regexp.MustCompile(`([^a-zA-Z])(?:0x)?[0-9a-f]{6,}`),
"${1}ADDR",
},
{
// Replace that everything looks like a decimal number with "NUM".
regexp.MustCompile(`([^a-zA-Z])[0-9]{5,}`),
"${1}NUM",
},
{
// Replace that everything looks like a file line number with "LINE".
regexp.MustCompile(`(:[0-9]+)+`),
":LINE",
},
{
// Replace all raw references to runctions (e.g. "ip6_fragment+0x1052/0x2d80")
// with just function name ("ip6_fragment"). Offsets and sizes are not stable.
regexp.MustCompile(`([a-zA-Z][a-zA-Z0-9_.]+)\+0x[0-9a-z]+/0x[0-9a-z]+`),
"${1}",
},
{
// CPU numbers are not interesting.
regexp.MustCompile(`CPU#[0-9]+`),
"CPU",
},
}
func sanitizeTitle(title string) string {
const maxTitleLen = 120 // Corrupted/intermixed lines can be very long.
res := make([]byte, 0, len(title))
prev := byte(' ')
for i := 0; i < len(title) && i < maxTitleLen; i++ {
ch := title[i]
switch {
case ch == '\t':
ch = ' '
case ch < 0x20 || ch >= 0x7f:
continue
}
if ch == ' ' && prev == ' ' {
continue
}
res = append(res, ch)
prev = ch
}
return strings.TrimSpace(string(res))
}
type oops struct {
header []byte
formats []oopsFormat
suppressions []*regexp.Regexp
}
type oopsFormat struct {
title *regexp.Regexp
// If title is matched but report is not, the report is considered corrupted.
report *regexp.Regexp
// Format string to create report title.
// Strings captured by title (or by report if present) are passed as input.
// If stack is not nil, extracted function name is passed as an additional last argument.
fmt string
// If not nil, a function name is extracted from the report and passed to fmt.
// If not nil but frame extraction fails, the report is considered corrupted.
stack *stackFmt
noStackTrace bool
corrupted bool
}
type stackFmt struct {
// parts describe how guilty stack frame must be extracted from the report.
// parts are matched consecutively potentially capturing frames.
// parts can be of 3 types:
// - non-capturing regexp, matched against report and advances current position
// - capturing regexp, same as above, but also yields a frame
// - special value parseStackTrace means that a stack trace must be parsed
// starting from current position
parts []*regexp.Regexp
// If parts2 is present it is tried when parts matching fails.
parts2 []*regexp.Regexp
// Skip these functions in stack traces (matched as substring).
skip []string
// Custom frame extractor (optional).
// Accepts set of all frames, returns guilty frame and corruption reason.
extractor frameExtractor
}
type frameExtractor func(frames []string) (frame string, corrupted string)
var parseStackTrace *regexp.Regexp
func compile(re string) *regexp.Regexp {
re = strings.Replace(re, "{{ADDR}}", "0x[0-9a-f]+", -1)
re = strings.Replace(re, "{{PC}}", "\\[\\<?(?:0x)?[0-9a-f]+\\>?\\]", -1)
re = strings.Replace(re, "{{FUNC}}", "([a-zA-Z0-9_]+)(?:\\.|\\+)", -1)
re = strings.Replace(re, "{{SRC}}", "([a-zA-Z0-9-_/.]+\\.[a-z]+:[0-9]+)", -1)
return regexp.MustCompile(re)
}
func containsCrash(output []byte, oopses []*oops, ignores []*regexp.Regexp) bool {
for pos := 0; pos < len(output); {
next := bytes.IndexByte(output[pos:], '\n')
if next != -1 {
next += pos
} else {
next = len(output)
}
for _, oops := range oopses {
if matchOops(output[pos:next], oops, ignores) {
return true
}
}
pos = next + 1
}
return false
}
func matchOops(line []byte, oops *oops, ignores []*regexp.Regexp) bool {
match := bytes.Index(line, oops.header)
if match == -1 {
return false
}
if matchesAny(line, oops.suppressions) {
return false
}
if matchesAny(line, ignores) {
return false
}
return true
}
func extractDescription(output []byte, oops *oops, params *stackParams) (
desc string, corrupted string, format oopsFormat) {
startPos := len(output)
matchedTitle := false
for _, f := range oops.formats {
match := f.title.FindSubmatchIndex(output)
if match == nil || match[0] > startPos {
continue
}
if match[0] == startPos && desc != "" {
continue
}
if match[0] < startPos {
desc = ""
format = oopsFormat{}
startPos = match[0]
}
matchedTitle = true
if f.report != nil {
match = f.report.FindSubmatchIndex(output)
if match == nil {
continue
}
}
var args []interface{}
for i := 2; i < len(match); i += 2 {
args = append(args, string(output[match[i]:match[i+1]]))
}
corrupted = ""
if f.stack != nil {
frame := ""
frame, corrupted = extractStackFrame(params, f.stack, output[match[0]:])
if frame == "" {
frame = "corrupted"
if corrupted == "" {
corrupted = "extracted no stack frame"
}
}
args = append(args, frame)
}
desc = fmt.Sprintf(f.fmt, args...)
format = f
}
if desc == "" {
// If we are here and matchedTitle is set, it means that we've matched
// a title of an oops but not full report regexp or stack trace,
// which means the report was corrupted.
if matchedTitle {
corrupted = "matched title but not report regexp"
}
pos := bytes.Index(output, oops.header)
if pos == -1 {
return
}
end := bytes.IndexByte(output[pos:], '\n')
if end == -1 {
end = len(output)
} else {
end += pos
}
desc = string(output[pos:end])
}
if corrupted == "" && format.corrupted {
corrupted = "report format is marked as corrupted"
}
return
}
type stackParams struct {
// stackStartRes matches start of stack traces.
stackStartRes []*regexp.Regexp
// frameRes match different formats of lines containing kernel frames (capture function name).
frameRes []*regexp.Regexp
// skipPatterns match functions that must be unconditionally skipped.
skipPatterns []string
// If we looked at any lines that match corruptedLines during report analysis,
// then the report is marked as corrupted.
corruptedLines []*regexp.Regexp
}
func extractStackFrame(params *stackParams, stack *stackFmt, output []byte) (string, string) {
skip := append([]string{}, params.skipPatterns...)
skip = append(skip, stack.skip...)
var skipRe *regexp.Regexp
if len(skip) != 0 {
skipRe = regexp.MustCompile(strings.Join(skip, "|"))
}
extractor := stack.extractor
if extractor == nil {
extractor = func(frames []string) (string, string) {
return frames[0], ""
}
}
frame, corrupted := extractStackFrameImpl(params, output, skipRe, stack.parts, extractor)
if frame != "" || len(stack.parts2) == 0 {
return frame, corrupted
}
return extractStackFrameImpl(params, output, skipRe, stack.parts2, extractor)
}
func extractStackFrameImpl(params *stackParams, output []byte, skipRe *regexp.Regexp,
parts []*regexp.Regexp, extractor frameExtractor) (string, string) {
s := bufio.NewScanner(bytes.NewReader(output))
var frames []string
nextPart:
for _, part := range parts {
if part == parseStackTrace {
for s.Scan() {
ln := bytes.Trim(s.Bytes(), "\r")
if matchesAny(ln, params.corruptedLines) {
break nextPart
}
if matchesAny(ln, params.stackStartRes) {
continue nextPart
}
var match [][]byte
for _, re := range params.frameRes {
match = re.FindSubmatch(ln)
if match != nil {
break
}
}
frames = appendStackFrame(frames, match, skipRe)
}
} else {
for s.Scan() {
ln := bytes.Trim(s.Bytes(), "\r")
if matchesAny(ln, params.corruptedLines) {
break nextPart
}
match := part.FindSubmatch(ln)
if match == nil {
continue
}
frames = appendStackFrame(frames, match, skipRe)
break
}
}
}
if len(frames) == 0 {
return "", "extracted no frames"
}
return extractor(frames)
}
func appendStackFrame(frames []string, match [][]byte, skipRe *regexp.Regexp) []string {
if len(match) < 2 {
return frames
}
for _, frame := range match[1:] {
if frame != nil && (skipRe == nil || !skipRe.Match(frame)) {
frames = append(frames, string(frame))
break
}
}
return frames
}
func simpleLineParser(output []byte, oopses []*oops, params *stackParams, ignores []*regexp.Regexp) *Report {
rep := &Report{
Output: output,
}
var oops *oops
for pos := 0; pos < len(output); {
next := bytes.IndexByte(output[pos:], '\n')
if next != -1 {
next += pos
} else {
next = len(output)
}
line := output[pos:next]
for _, oops1 := range oopses {
if matchOops(line, oops1, ignores) {
oops = oops1
rep.StartPos = pos
rep.EndPos = next
break
}
}
if oops != nil {
break
}
pos = next + 1
}
if oops == nil {
return nil
}
title, corrupted, _ := extractDescription(output[rep.StartPos:], oops, params)
rep.Title = title
rep.Report = output[rep.StartPos:]
rep.Corrupted = corrupted != ""
rep.CorruptedReason = corrupted
return rep
}
func matchesAny(line []byte, res []*regexp.Regexp) bool {
for _, re := range res {
if re.Match(line) {
return true
}
}
return false
}
// replace replaces [start:end] in where with what, inplace.
func replace(where []byte, start, end int, what []byte) []byte {
if len(what) >= end-start {
where = append(where, what[end-start:]...)
copy(where[start+len(what):], where[end:])
copy(where[start:], what)
} else {
copy(where[start+len(what):], where[end:])
where = where[:len(where)-(end-start-len(what))]
copy(where[start:], what)
}
return where
}
var (
filenameRe = regexp.MustCompile(`[a-zA-Z0-9_\-\./]*[a-zA-Z0-9_\-]+\.(c|h):[0-9]+`)
reportFrameRe = regexp.MustCompile(`.* in ([a-zA-Z0-9_]+)`)
)