aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDmitry Vyukov <dvyukov@google.com>2016-12-19 17:39:03 +0100
committerDmitry Vyukov <dvyukov@google.com>2016-12-19 17:39:03 +0100
commit80b6c954f8daa8d9910698be9eca6d97284a75a0 (patch)
treea367f64eeee88fe93a49d98f5a3bbba60df9de85
parenta074da17a4055352fea94afbd5a15c53d0946653 (diff)
manager: add ability to ignore bugs
Add new config parameter "ignores" which contains list of regexp expressions. If one of the expressions is matched against oops line, crash report is not saved and VM is not restarted.
-rw-r--r--config/config.go82
-rw-r--r--config/config_test.go2
-rw-r--r--report/report.go15
-rw-r--r--report/report_test.go51
-rw-r--r--repro/repro.go4
-rw-r--r--syz-manager/manager.go13
-rw-r--r--tools/syz-crush/crush.go4
-rw-r--r--tools/syz-report/syz-report.go2
-rw-r--r--tools/syz-repro/repro.go2
-rw-r--r--tools/syz-symbolize/symbolize.go2
-rw-r--r--vm/vm.go9
11 files changed, 123 insertions, 63 deletions
diff --git a/config/config.go b/config/config.go
index 33e9c625e..b7492ad68 100644
--- a/config/config.go
+++ b/config/config.go
@@ -58,82 +58,87 @@ type Config struct {
Enable_Syscalls []string
Disable_Syscalls []string
- Suppressions []string
+ Suppressions []string // don't save reports matching these regexps, but reboot VM after them
+ Ignores []string // completely ignore reports matching these regexps (don't save nor reboot)
+
+ // Implementation details beyond this point.
+ ParsedSuppressions []*regexp.Regexp `json:"-"`
+ ParsedIgnores []*regexp.Regexp `json:"-"`
}
-func Parse(filename string) (*Config, map[int]bool, []*regexp.Regexp, error) {
+func Parse(filename string) (*Config, map[int]bool, error) {
if filename == "" {
- return nil, nil, nil, fmt.Errorf("supply config in -config flag")
+ return nil, nil, fmt.Errorf("supply config in -config flag")
}
data, err := ioutil.ReadFile(filename)
if err != nil {
- return nil, nil, nil, fmt.Errorf("failed to read config file: %v", err)
+ return nil, nil, fmt.Errorf("failed to read config file: %v", err)
}
return parse(data)
}
-func parse(data []byte) (*Config, map[int]bool, []*regexp.Regexp, error) {
+func parse(data []byte) (*Config, map[int]bool, error) {
unknown, err := checkUnknownFields(data)
if err != nil {
- return nil, nil, nil, err
+ return nil, nil, err
}
if unknown != "" {
- return nil, nil, nil, fmt.Errorf("unknown field '%v' in config", unknown)
+ return nil, nil, fmt.Errorf("unknown field '%v' in config", unknown)
}
cfg := new(Config)
cfg.Cover = true
cfg.Sandbox = "setuid"
if err := json.Unmarshal(data, cfg); err != nil {
- return nil, nil, nil, fmt.Errorf("failed to parse config file: %v", err)
+ return nil, nil, fmt.Errorf("failed to parse config file: %v", err)
}
if _, err := os.Stat(filepath.Join(cfg.Syzkaller, "bin/syz-fuzzer")); err != nil {
- return nil, nil, nil, fmt.Errorf("bad config syzkaller param: can't find bin/syz-fuzzer")
+ return nil, nil, fmt.Errorf("bad config syzkaller param: can't find bin/syz-fuzzer")
}
if _, err := os.Stat(filepath.Join(cfg.Syzkaller, "bin/syz-executor")); err != nil {
- return nil, nil, nil, fmt.Errorf("bad config syzkaller param: can't find bin/syz-executor")
+ return nil, nil, fmt.Errorf("bad config syzkaller param: can't find bin/syz-executor")
}
if cfg.Http == "" {
- return nil, nil, nil, fmt.Errorf("config param http is empty")
+ return nil, nil, fmt.Errorf("config param http is empty")
}
if cfg.Workdir == "" {
- return nil, nil, nil, fmt.Errorf("config param workdir is empty")
+ return nil, nil, fmt.Errorf("config param workdir is empty")
}
if cfg.Vmlinux == "" {
- return nil, nil, nil, fmt.Errorf("config param vmlinux is empty")
+ return nil, nil, fmt.Errorf("config param vmlinux is empty")
}
if cfg.Type == "" {
- return nil, nil, nil, fmt.Errorf("config param type is empty")
+ return nil, nil, fmt.Errorf("config param type is empty")
}
switch cfg.Type {
case "none":
if cfg.Count != 0 {
- return nil, nil, nil, fmt.Errorf("invalid config param count: %v, type \"none\" does not support param count", cfg.Count)
+ return nil, nil, fmt.Errorf("invalid config param count: %v, type \"none\" does not support param count", cfg.Count)
}
if cfg.Rpc == "" {
- return nil, nil, nil, fmt.Errorf("config param rpc is empty (required for type \"none\")")
+ return nil, nil, fmt.Errorf("config param rpc is empty (required for type \"none\")")
}
if len(cfg.Devices) != 0 {
- return nil, nil, nil, fmt.Errorf("type %v does not support devices param", cfg.Type)
+ return nil, nil, fmt.Errorf("type %v does not support devices param", cfg.Type)
}
case "adb":
if cfg.Count != 0 {
- return nil, nil, nil, fmt.Errorf("don't specify count for adb, instead specify devices")
+ return nil, nil, fmt.Errorf("don't specify count for adb, instead specify devices")
}
if len(cfg.Devices) == 0 {
- return nil, nil, nil, fmt.Errorf("specify at least 1 adb device")
+ return nil, nil, fmt.Errorf("specify at least 1 adb device")
}
cfg.Count = len(cfg.Devices)
case "gce":
if cfg.Machine_Type == "" {
- return nil, nil, nil, fmt.Errorf("machine_type parameter is empty (required for gce)")
+ return nil, nil, fmt.Errorf("machine_type parameter is empty (required for gce)")
}
fallthrough
default:
if cfg.Count <= 0 || cfg.Count > 1000 {
- return nil, nil, nil, fmt.Errorf("invalid config param count: %v, want (1, 1000]", cfg.Count)
+ return nil, nil, fmt.Errorf("invalid config param count: %v, want (1, 1000]", cfg.Count)
}
if len(cfg.Devices) != 0 {
- return nil, nil, nil, fmt.Errorf("type %v does not support devices param", cfg.Type)
+ return nil, nil, fmt.Errorf("type %v does not support devices param", cfg.Type)
}
}
if cfg.Rpc == "" {
@@ -143,7 +148,7 @@ func parse(data []byte) (*Config, map[int]bool, []*regexp.Regexp, error) {
cfg.Procs = 1
}
if cfg.Procs > 32 {
- return nil, nil, nil, fmt.Errorf("config param procs has higher value '%v' then the max supported 32", cfg.Procs)
+ return nil, nil, fmt.Errorf("config param procs has higher value '%v' then the max supported 32", cfg.Procs)
}
if cfg.Output == "" {
if cfg.Type == "local" {
@@ -155,25 +160,24 @@ func parse(data []byte) (*Config, map[int]bool, []*regexp.Regexp, error) {
switch cfg.Output {
case "none", "stdout", "dmesg", "file":
default:
- return nil, nil, nil, fmt.Errorf("config param output must contain one of none/stdout/dmesg/file")
+ return nil, nil, fmt.Errorf("config param output must contain one of none/stdout/dmesg/file")
}
switch cfg.Sandbox {
case "none", "setuid", "namespace":
default:
- return nil, nil, nil, fmt.Errorf("config param sandbox must contain one of none/setuid/namespace")
+ return nil, nil, fmt.Errorf("config param sandbox must contain one of none/setuid/namespace")
}
syscalls, err := parseSyscalls(cfg)
if err != nil {
- return nil, nil, nil, err
+ return nil, nil, err
}
- suppressions, err := parseSuppressions(cfg)
- if err != nil {
- return nil, nil, nil, err
+ if err := parseSuppressions(cfg); err != nil {
+ return nil, nil, err
}
- return cfg, syscalls, suppressions, nil
+ return cfg, syscalls, nil
}
func parseSyscalls(cfg *Config) (map[int]bool, error) {
@@ -225,7 +229,7 @@ func parseSyscalls(cfg *Config) (map[int]bool, error) {
return syscalls, nil
}
-func parseSuppressions(cfg *Config) ([]*regexp.Regexp, error) {
+func parseSuppressions(cfg *Config) error {
// Add some builtin suppressions.
supp := append(cfg.Suppressions, []string{
"panic: failed to start executor binary",
@@ -237,16 +241,21 @@ func parseSuppressions(cfg *Config) ([]*regexp.Regexp, error) {
"lowmemorykiller: Killing 'syz-fuzzer'",
//"INFO: lockdep is turned off", // printed by some sysrq that dumps scheduler state, but also on all lockdep reports
}...)
- var suppressions []*regexp.Regexp
for _, s := range supp {
re, err := regexp.Compile(s)
if err != nil {
- return nil, fmt.Errorf("failed to compile suppression '%v': %v", s, err)
+ return fmt.Errorf("failed to compile suppression '%v': %v", s, err)
}
- suppressions = append(suppressions, re)
+ cfg.ParsedSuppressions = append(cfg.ParsedSuppressions, re)
}
-
- return suppressions, nil
+ for _, ignore := range cfg.Ignores {
+ re, err := regexp.Compile(ignore)
+ if err != nil {
+ return fmt.Errorf("failed to compile ignore '%v': %v", ignore, err)
+ }
+ cfg.ParsedIgnores = append(cfg.ParsedIgnores, re)
+ }
+ return nil
}
func CreateVMConfig(cfg *Config, index int) (*vm.Config, error) {
@@ -313,6 +322,7 @@ func checkUnknownFields(data []byte) (string, error) {
"Enable_Syscalls",
"Disable_Syscalls",
"Suppressions",
+ "Ignores",
"Initrd",
"Machine_Type",
}
diff --git a/config/config_test.go b/config/config_test.go
index 2a465900d..1072587df 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -9,7 +9,7 @@ import (
func TestUnknown(t *testing.T) {
data := `{"foo": "bar"}`
- _, _, _, err := parse([]byte(data))
+ _, _, err := parse([]byte(data))
if err == nil || err.Error() != "unknown field 'foo' in config" {
t.Fatalf("unknown field is not detected (%v)", err)
}
diff --git a/report/report.go b/report/report.go
index 0e5a04bba..b82b9b9a7 100644
--- a/report/report.go
+++ b/report/report.go
@@ -237,7 +237,7 @@ func compile(re string) *regexp.Regexp {
}
// ContainsCrash searches kernel console output for oops messages.
-func ContainsCrash(output []byte) bool {
+func ContainsCrash(output []byte, ignores []*regexp.Regexp) bool {
for pos := 0; pos < len(output); {
next := bytes.IndexByte(output[pos:], '\n')
if next != -1 {
@@ -246,7 +246,7 @@ func ContainsCrash(output []byte) bool {
next = len(output)
}
for _, oops := range oopses {
- match := matchOops(output[pos:next], oops)
+ match := matchOops(output[pos:next], oops, ignores)
if match == -1 {
continue
}
@@ -261,7 +261,7 @@ func ContainsCrash(output []byte) bool {
// Desc contains a representative description of the first oops (empty if no oops found),
// text contains whole oops text,
// start and end denote region of output with oops message(s).
-func Parse(output []byte) (desc string, text []byte, start int, end int) {
+func Parse(output []byte, ignores []*regexp.Regexp) (desc string, text []byte, start int, end int) {
var oops *oops
for pos := 0; pos < len(output); {
next := bytes.IndexByte(output[pos:], '\n')
@@ -271,7 +271,7 @@ func Parse(output []byte) (desc string, text []byte, start int, end int) {
next = len(output)
}
for _, oops1 := range oopses {
- match := matchOops(output[pos:next], oops1)
+ match := matchOops(output[pos:next], oops1, ignores)
if match == -1 {
continue
}
@@ -306,7 +306,7 @@ func Parse(output []byte) (desc string, text []byte, start int, end int) {
return
}
-func matchOops(line []byte, oops *oops) int {
+func matchOops(line []byte, oops *oops, ignores []*regexp.Regexp) int {
match := bytes.Index(line, oops.header)
if match == -1 {
return -1
@@ -316,6 +316,11 @@ func matchOops(line []byte, oops *oops) int {
return -1
}
}
+ for _, ignore := range ignores {
+ if ignore.Match(line) {
+ return -1
+ }
+ }
return match
}
diff --git a/report/report_test.go b/report/report_test.go
index f6fdd7597..b75c23a83 100644
--- a/report/report_test.go
+++ b/report/report_test.go
@@ -5,6 +5,7 @@ package report
import (
"fmt"
+ "regexp"
"strings"
"testing"
@@ -500,7 +501,7 @@ WARNING: /etc/ssh/moduli does not exist, using fixed modulus
tests[strings.Replace(log, "\n", "\r\n", -1)] = crash
}
for log, crash := range tests {
- containsCrash := ContainsCrash([]byte(log))
+ containsCrash := ContainsCrash([]byte(log), nil)
expectCrash := (crash != "")
if expectCrash && !containsCrash {
t.Fatalf("ContainsCrash did not find crash")
@@ -508,7 +509,7 @@ WARNING: /etc/ssh/moduli does not exist, using fixed modulus
if !expectCrash && containsCrash {
t.Fatalf("ContainsCrash found unexpected crash")
}
- desc, _, _, _ := Parse([]byte(log))
+ desc, _, _, _ := Parse([]byte(log), nil)
if desc == "" && crash != "" {
t.Fatalf("did not find crash message '%v' in:\n%v", crash, log)
}
@@ -521,6 +522,52 @@ WARNING: /etc/ssh/moduli does not exist, using fixed modulus
}
}
+func TestIgnores(t *testing.T) {
+ const log = `
+ BUG: bug1
+ BUG: bug2
+ `
+ if !ContainsCrash([]byte(log), nil) {
+ t.Fatalf("no crash")
+ }
+ if desc, _, _, _ := Parse([]byte(log), nil); desc != "BUG: bug1" {
+ t.Fatalf("want `BUG: bug1`, found `%v`", desc)
+ }
+
+ ignores1 := []*regexp.Regexp{
+ regexp.MustCompile("BUG: bug3"),
+ }
+ if !ContainsCrash([]byte(log), ignores1) {
+ t.Fatalf("no crash")
+ }
+ if desc, _, _, _ := Parse([]byte(log), ignores1); desc != "BUG: bug1" {
+ t.Fatalf("want `BUG: bug1`, found `%v`", desc)
+ }
+
+ ignores2 := []*regexp.Regexp{
+ regexp.MustCompile("BUG: bug3"),
+ regexp.MustCompile("BUG: bug1"),
+ }
+ if !ContainsCrash([]byte(log), ignores2) {
+ t.Fatalf("no crash")
+ }
+ if desc, _, _, _ := Parse([]byte(log), ignores2); desc != "BUG: bug2" {
+ t.Fatalf("want `BUG: bug2`, found `%v`", desc)
+ }
+
+ ignores3 := []*regexp.Regexp{
+ regexp.MustCompile("BUG: bug3"),
+ regexp.MustCompile("BUG: bug1"),
+ regexp.MustCompile("BUG: bug2"),
+ }
+ if ContainsCrash([]byte(log), ignores3) {
+ t.Fatalf("found crash, should be ignored")
+ }
+ if desc, _, _, _ := Parse([]byte(log), ignores3); desc != "" {
+ t.Fatalf("found `%v`, should be ignored", desc)
+ }
+}
+
func TestReplace(t *testing.T) {
tests := []struct {
where string
diff --git a/repro/repro.go b/repro/repro.go
index 0b797a084..f6ec8df1e 100644
--- a/repro/repro.go
+++ b/repro/repro.go
@@ -51,7 +51,7 @@ func Run(crashLog []byte, cfg *config.Config, vmIndexes []int) (*Result, error)
if len(entries) == 0 {
return nil, fmt.Errorf("crash log does not contain any programs")
}
- crashDesc, _, crashStart, _ := report.Parse(crashLog)
+ crashDesc, _, crashStart, _ := report.Parse(crashLog, cfg.ParsedIgnores)
if crashDesc == "" {
crashStart = len(crashLog) // assuming VM hanged
crashDesc = "hang"
@@ -325,7 +325,7 @@ func (ctx *context) testImpl(inst vm.Instance, command string, duration time.Dur
if err != nil {
return false, fmt.Errorf("failed to run command in VM: %v", err)
}
- desc, text, output, crashed, timedout := vm.MonitorExecution(outc, errc, false, false)
+ desc, text, output, crashed, timedout := vm.MonitorExecution(outc, errc, false, false, ctx.cfg.ParsedIgnores)
_, _, _ = text, output, timedout
if !crashed {
Logf(2, "reproducing crash '%v': program did not crash", ctx.crashDesc)
diff --git a/syz-manager/manager.go b/syz-manager/manager.go
index 4951172f2..666821ddf 100644
--- a/syz-manager/manager.go
+++ b/syz-manager/manager.go
@@ -14,7 +14,6 @@ import (
"os"
"os/signal"
"path/filepath"
- "regexp"
"sync"
"syscall"
"time"
@@ -57,7 +56,6 @@ type Manager struct {
mu sync.Mutex
enabledSyscalls string
enabledCalls []string // as determined by fuzzer
- suppressions []*regexp.Regexp
candidates [][]byte // untriaged inputs
disabledHashes []string
@@ -85,7 +83,7 @@ type Crash struct {
func main() {
flag.Parse()
EnableLogCaching(1000, 1<<20)
- cfg, syscalls, suppressions, err := config.Parse(*flagConfig)
+ cfg, syscalls, err := config.Parse(*flagConfig)
if err != nil {
Fatalf("%v", err)
}
@@ -94,10 +92,10 @@ func main() {
cfg.Count = 1
}
initAllCover(cfg.Vmlinux)
- RunManager(cfg, syscalls, suppressions)
+ RunManager(cfg, syscalls)
}
-func RunManager(cfg *config.Config, syscalls map[int]bool, suppressions []*regexp.Regexp) {
+func RunManager(cfg *config.Config, syscalls map[int]bool) {
crashdir := filepath.Join(cfg.Workdir, "crashes")
os.MkdirAll(crashdir, 0700)
@@ -117,7 +115,6 @@ func RunManager(cfg *config.Config, syscalls map[int]bool, suppressions []*regex
startTime: time.Now(),
stats: make(map[string]uint64),
enabledSyscalls: enabledSyscalls,
- suppressions: suppressions,
corpusCover: make([]cover.Cover, sys.CallCount),
fuzzers: make(map[string]*Fuzzer),
fresh: true,
@@ -381,7 +378,7 @@ func (mgr *Manager) runInstance(vmCfg *vm.Config, first bool) (*Crash, error) {
return nil, fmt.Errorf("failed to run fuzzer: %v", err)
}
- desc, text, output, crashed, timedout := vm.MonitorExecution(outc, errc, mgr.cfg.Type == "local", true)
+ desc, text, output, crashed, timedout := vm.MonitorExecution(outc, errc, mgr.cfg.Type == "local", true, mgr.cfg.ParsedIgnores)
if timedout {
// This is the only "OK" outcome.
Logf(0, "%v: running for %v, restarting (%v)", vmCfg.Name, time.Since(start), desc)
@@ -395,7 +392,7 @@ func (mgr *Manager) runInstance(vmCfg *vm.Config, first bool) (*Crash, error) {
}
func (mgr *Manager) isSuppressed(crash *Crash) bool {
- for _, re := range mgr.suppressions {
+ for _, re := range mgr.cfg.ParsedSuppressions {
if !re.Match(crash.output) {
continue
}
diff --git a/tools/syz-crush/crush.go b/tools/syz-crush/crush.go
index 32b4a715b..d519107f9 100644
--- a/tools/syz-crush/crush.go
+++ b/tools/syz-crush/crush.go
@@ -33,7 +33,7 @@ var (
func main() {
flag.Parse()
- cfg, _, _, err := config.Parse(*flagConfig)
+ cfg, _, err := config.Parse(*flagConfig)
if err != nil {
Fatalf("%v", err)
}
@@ -113,7 +113,7 @@ func runInstance(cfg *config.Config, vmCfg *vm.Config) {
}
Logf(0, "%v: crushing...", vmCfg.Name)
- desc, _, output, crashed, timedout := vm.MonitorExecution(outc, errc, cfg.Type == "local", true)
+ desc, _, output, crashed, timedout := vm.MonitorExecution(outc, errc, cfg.Type == "local", true, cfg.ParsedIgnores)
if timedout {
// This is the only "OK" outcome.
Logf(0, "%v: running long enough, restarting", vmCfg.Name)
diff --git a/tools/syz-report/syz-report.go b/tools/syz-report/syz-report.go
index aed958b51..eff8d6b8c 100644
--- a/tools/syz-report/syz-report.go
+++ b/tools/syz-report/syz-report.go
@@ -21,7 +21,7 @@ func main() {
fmt.Fprintf(os.Stderr, "failed to read report file: %v\n", err)
os.Exit(1)
}
- desc, text, _, _ := report.Parse(output)
+ desc, text, _, _ := report.Parse(output, nil)
if desc == "" {
fmt.Fprintf(os.Stderr, "report file does not contain a crash\n")
os.Exit(1)
diff --git a/tools/syz-repro/repro.go b/tools/syz-repro/repro.go
index fb9ad0ef6..e2e2f6985 100644
--- a/tools/syz-repro/repro.go
+++ b/tools/syz-repro/repro.go
@@ -30,7 +30,7 @@ var (
func main() {
os.Args = append(append([]string{}, os.Args[0], "-v=10"), os.Args[1:]...)
flag.Parse()
- cfg, _, _, err := config.Parse(*flagConfig)
+ cfg, _, err := config.Parse(*flagConfig)
if err != nil {
Fatalf("%v", err)
}
diff --git a/tools/syz-symbolize/symbolize.go b/tools/syz-symbolize/symbolize.go
index df4f4765f..4e5e8564b 100644
--- a/tools/syz-symbolize/symbolize.go
+++ b/tools/syz-symbolize/symbolize.go
@@ -29,7 +29,7 @@ func main() {
fmt.Fprintf(os.Stderr, "failed to open input file: %v\n", err)
os.Exit(1)
}
- if _, parsed, _, _ := report.Parse(text); len(parsed) != 0 {
+ if _, parsed, _, _ := report.Parse(text, nil); len(parsed) != 0 {
text = parsed
}
text, err = report.Symbolize(filepath.Join(*flagLinux, "vmlinux"), text)
diff --git a/vm/vm.go b/vm/vm.go
index d42ddd4cd..ce4138344 100644
--- a/vm/vm.go
+++ b/vm/vm.go
@@ -9,6 +9,7 @@ import (
"fmt"
"io"
"os"
+ "regexp"
"syscall"
"time"
@@ -86,7 +87,7 @@ func LongPipe() (io.ReadCloser, io.WriteCloser, error) {
var TimeoutErr = errors.New("timeout")
-func MonitorExecution(outc <-chan []byte, errc <-chan error, local, needOutput bool) (desc string, text, output []byte, crashed, timedout bool) {
+func MonitorExecution(outc <-chan []byte, errc <-chan error, local, needOutput bool, ignores []*regexp.Regexp) (desc string, text, output []byte, crashed, timedout bool) {
waitForOutput := func() {
dur := time.Second
if needOutput {
@@ -117,10 +118,10 @@ func MonitorExecution(outc <-chan []byte, errc <-chan error, local, needOutput b
if bytes.Contains(output, []byte("SYZ-FUZZER: PREEMPTED")) {
return "preempted", nil, nil, false, true
}
- if !report.ContainsCrash(output[matchPos:]) {
+ if !report.ContainsCrash(output[matchPos:], ignores) {
return defaultError, nil, output, true, false
}
- desc, text, start, end := report.Parse(output[matchPos:])
+ desc, text, start, end := report.Parse(output[matchPos:], ignores)
start = start + matchPos - beforeContext
if start < 0 {
start = 0
@@ -162,7 +163,7 @@ func MonitorExecution(outc <-chan []byte, errc <-chan error, local, needOutput b
if bytes.Index(output[matchPos:], []byte("executed programs:")) != -1 { // syz-execprog output
lastExecuteTime = time.Now()
}
- if report.ContainsCrash(output[matchPos:]) {
+ if report.ContainsCrash(output[matchPos:], ignores) {
return extractError("")
}
if len(output) > 2*beforeContext {