blob: d486ca3d608a499b6346ccbd2717b80b4c76679f [file] [log] [blame]
package data
import (
"fmt"
"regexp"
"sort"
"strings"
"go.skia.org/infra/fuzzer/go/common"
"go.skia.org/infra/go/util"
)
// Represents the metadata about a crash, hopefully easing debugging.
type FuzzResult struct {
Configs map[string]BuildData // maps config name (e.g. DEBUG_ASAN) -> BuildData
}
// BuildData represents the results of parsing a given skia build's output.
type BuildData struct {
OutputFiles
StackTrace StackTrace
Flags FuzzFlag
}
// OutputFiles are the files output by the analysis
type OutputFiles struct {
Key string // An optional key, typically used for the analysis config name
Content map[string]string // maps file name/descriptor -> content
}
// GCSPackage is a struct containing all the pieces of a fuzz that exist in Google Storage.
type GCSPackage struct {
Name string
FuzzCategory string
FuzzArchitecture string
// maps config (e.g. DEBUG_ASAN) to output files created by running that fuzz through the
// config (e.g. stdout and stderr)
Files map[string]OutputFiles
}
// A bit mask representing what happened when a fuzz ran against Skia.
type FuzzFlag int
const (
TerminatedGracefully FuzzFlag = 1 << iota
ClangCrashed
ASANCrashed
AssertionViolated
BadAlloc
NoStackTrace
SKAbortHit
TimedOut
Other
ASAN_GlobalBufferOverflow
ASAN_HeapBufferOverflow
ASAN_StackBufferOverflow
ASAN_HeapUseAfterFree
)
// BadAlloc means Out of Memory, which is not a thing fuzzing cares about.
var _GREY_FLAGS = TerminatedGracefully | TimedOut | BadAlloc
var _FLAG_NAMES = []string{
"TerminatedGracefully",
"ClangCrashed",
"ASANCrashed",
"AssertionViolated",
"BadAlloc",
"NoStackTrace",
"SKAbortHit",
"TimedOut",
"Other",
"ASAN_global-buffer-overflow",
"ASAN_heap-buffer-overflow",
"ASAN_stack-buffer-overflow",
"ASAN_heap-use-after-free",
}
// ToHumanReadableFlags creates a sorted slice of strings that represents the flags. The slice
// is sorted by unicode points, as per sort.Strings().
func (f FuzzFlag) ToHumanReadableFlags() []string {
flags := make([]string, 0)
i := 0
for mask := 1; mask < (2 << uint(len(_FLAG_NAMES))); mask *= 2 {
if int(f)&mask != 0 {
flags = append(flags, _FLAG_NAMES[i])
}
i++
}
// Front end filtering logic will expect the flags to be in alphabetical order.
sort.Strings(flags)
return flags
}
func (f FuzzFlag) String() string {
return fmt.Sprintf("FuzzFlag: %016b (%d) [%s]", f, f, strings.Join(f.ToHumanReadableFlags(), " | "))
}
// IsGrey returns true if the fuzz should be considered grey, that is, is not a real crash.
func (r *FuzzResult) IsGrey() bool {
for _, c := range r.Configs {
if !isGrey(c.Flags) {
return false
}
}
return true
}
// isGrey returns true if the fuzz should be considered grey, that is, is not a real crash.
func isGrey(flags FuzzFlag) bool {
// If the only flags are in the _GREY_FLAGS slice, then we should ignore this.
// TODO(kjlubick): Possibly change this to be a full-blown, user-editable blacklist
// as in skbug.com/5191
// 2^n-1 for a mask like 111111111111111111
// then XOR it with the grey flags to get a bitmask that removes the
// grey flags from the debug/release flags
badFlags := (2<<uint(len(_FLAG_NAMES)) - 1) ^ _GREY_FLAGS
return flags&badFlags == 0
}
// ParseGCSPackage parses the results of analysis of a fuzz and creates a FuzzResult with it.
// This includes parsing the stacktraces and computing the flags about the fuzz.
func ParseGCSPackage(g GCSPackage) FuzzResult {
result := FuzzResult{}
result.Configs = map[string]BuildData{}
for _, c := range common.ANALYSIS_TYPES {
s := StackTrace{}
if strings.Contains(c, "ASAN") {
s = parseASANStackTrace(g.Files[c].Content["stderr"])
if s.IsEmpty() {
s = parseASANSummary(g.Files[c].Content["stderr"])
}
} else if strings.Contains(c, "CLANG") {
s = parseCatchsegvStackTrace(g.Files[c].Content["stdout"])
}
cfg := BuildData{
OutputFiles: g.Files[c],
StackTrace: s,
}
cfg.Flags = parseAll(g.FuzzCategory, &cfg)
result.Configs[c] = cfg
}
return result
}
// parseAll looks at the three input files and parses the results, based on the category. The
// category allows for specialized flags, like SKPICTURE_DuringRendering.
func parseAll(category string, data *BuildData) FuzzFlag {
// SkDebugf (the main source of printing) writes to stderr
stderr := data.Content["stderr"]
// stdout is generally blank, except if catchsegv (used for Clang builds) catches a crash
stdout := data.Content["stdout"]
// Check for SKAbort message
if strings.Contains(stderr, "fatal error") {
if data.StackTrace.IsEmpty() {
data.StackTrace = extractSkAbortTrace(stderr)
}
return SKAbortHit
}
if strings.Contains(data.Key, "ASAN") {
if !asanCrashed(stderr) {
if strings.Contains(stderr, "[terminated]") || strings.Contains(stderr, "Signal boring") {
return TerminatedGracefully
}
return TimedOut
}
}
if strings.Contains(data.Key, "CLANG") && !clangDumped(stdout) {
if strings.Contains(stderr, "[terminated]") || strings.Contains(stderr, "Signal boring") {
return TerminatedGracefully
}
if strings.Contains(stderr, "std::bad_alloc") {
return BadAlloc
}
return TimedOut
}
// We know there was a crash
f := FuzzFlag(0)
if strings.Contains(data.Key, "ASAN") {
f |= parseAsan(category, stderr)
} else if strings.Contains(data.Key, "CLANG") {
f |= parseCatchsegv(category, stdout, stderr)
}
if f == 0 {
// I don't know what this means (yet).
return Other
}
if data.StackTrace.IsEmpty() {
f |= NoStackTrace
}
return f
}
// parseAsan returns the flags discovered while looking through the AddressSanitizer output. This
// includes things like ASAN_GlobalBufferOverflow.
func parseAsan(category, asan string) FuzzFlag {
f := FuzzFlag(0)
if strings.Contains(asan, "AddressSanitizer failed to allocate") ||
strings.Contains(asan, "exceeds maximum supported size of") {
return BadAlloc
}
f |= ASANCrashed
if strings.Contains(asan, "failed assertion") {
f |= AssertionViolated
}
if strings.Contains(asan, "global-buffer-overflow") {
f |= ASAN_GlobalBufferOverflow
}
if strings.Contains(asan, "heap-buffer-overflow") {
f |= ASAN_HeapBufferOverflow
}
if strings.Contains(asan, "stack-buffer-overflow") {
f |= ASAN_StackBufferOverflow
}
if strings.Contains(asan, "heap-use-after-free") {
f |= ASAN_HeapUseAfterFree
}
// Split off the stderr that happened before the crash.
errs := strings.Split(asan, "=================")
if len(errs) > 0 {
// err := errs[0]
// An example on doing a category specific parsing
// if category == "skpicture" && strings.Contains(err, "Rendering") {
// f |= SKPICTURE_DuringRendering
// }
}
return f
}
// asanCrashed returns true if the asan output is consistent with a crash.
func asanCrashed(asan string) bool {
return strings.Contains(asan, "ERROR: AddressSanitizer:") || strings.Contains(asan, "runtime error:")
}
// parseAsan returns the flags discovered while looking through the Clang dump and standard error.
// This includes things like
func parseCatchsegv(category, dump, err string) FuzzFlag {
f := FuzzFlag(0)
if !clangDumped(dump) && strings.Contains(err, "[terminated]") {
return f
}
f |= ClangCrashed
if strings.Contains(err, "failed assertion") {
f |= AssertionViolated
}
return f
}
// clangDumped returns true if the clang output is consistent with a crash, that is, non empty.
func clangDumped(dump string) bool {
return strings.Contains(dump, "Register dump")
}
var skAbortStackTraceLine = regexp.MustCompile(`(?:\.\./)+(?P<package>(?:\w+/)+)(?P<file>.+):(?P<line>\d+): fatal error`)
// extractSkAbortTrace looks for the fatal error string indicative of the SKAbort termination
// and tries to pull out the stacktrace frame on which it happened.
func extractSkAbortTrace(err string) StackTrace {
st := StackTrace{}
if match := skAbortStackTraceLine.FindStringSubmatch(err); match != nil {
st.Frames = append(st.Frames, FullStackFrame(match[1], match[2], common.UNKNOWN_FUNCTION, util.SafeAtoi(match[3])))
}
return st
}