diff options
| author | Dmitry Vyukov <dvyukov@google.com> | 2016-12-19 17:39:03 +0100 |
|---|---|---|
| committer | Dmitry Vyukov <dvyukov@google.com> | 2016-12-19 17:39:03 +0100 |
| commit | 80b6c954f8daa8d9910698be9eca6d97284a75a0 (patch) | |
| tree | a367f64eeee88fe93a49d98f5a3bbba60df9de85 | |
| parent | a074da17a4055352fea94afbd5a15c53d0946653 (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.go | 82 | ||||
| -rw-r--r-- | config/config_test.go | 2 | ||||
| -rw-r--r-- | report/report.go | 15 | ||||
| -rw-r--r-- | report/report_test.go | 51 | ||||
| -rw-r--r-- | repro/repro.go | 4 | ||||
| -rw-r--r-- | syz-manager/manager.go | 13 | ||||
| -rw-r--r-- | tools/syz-crush/crush.go | 4 | ||||
| -rw-r--r-- | tools/syz-report/syz-report.go | 2 | ||||
| -rw-r--r-- | tools/syz-repro/repro.go | 2 | ||||
| -rw-r--r-- | tools/syz-symbolize/symbolize.go | 2 | ||||
| -rw-r--r-- | vm/vm.go | 9 |
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) @@ -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 { |
