| // Copyright 2018 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 vm |
| |
| import ( |
| "bytes" |
| "fmt" |
| "io/ioutil" |
| "os" |
| "testing" |
| "time" |
| |
| "github.com/google/syzkaller/pkg/mgrconfig" |
| "github.com/google/syzkaller/pkg/report" |
| "github.com/google/syzkaller/vm/vmimpl" |
| ) |
| |
| type testPool struct { |
| } |
| |
| func (pool *testPool) Count() int { |
| return 1 |
| } |
| |
| func (pool *testPool) Create(workdir string, index int) (vmimpl.Instance, error) { |
| return &testInstance{ |
| outc: make(chan []byte, 10), |
| errc: make(chan error, 1), |
| }, nil |
| } |
| |
| type testInstance struct { |
| outc chan []byte |
| errc chan error |
| diagnoseBug bool |
| diagnoseNoWait bool |
| } |
| |
| func (inst *testInstance) Copy(hostSrc string) (string, error) { |
| return "", nil |
| } |
| |
| func (inst *testInstance) Forward(port int) (string, error) { |
| return "", nil |
| } |
| |
| func (inst *testInstance) Run(timeout time.Duration, stop <-chan bool, command string) ( |
| outc <-chan []byte, errc <-chan error, err error) { |
| return inst.outc, inst.errc, nil |
| } |
| |
| func (inst *testInstance) Diagnose() ([]byte, bool) { |
| var diag []byte |
| if inst.diagnoseBug { |
| diag = []byte("BUG: DIAGNOSE\n") |
| } else { |
| diag = []byte("DIAGNOSE\n") |
| } |
| |
| if inst.diagnoseNoWait { |
| return diag, false |
| } |
| |
| inst.outc <- diag |
| return nil, true |
| } |
| |
| func (inst *testInstance) Close() { |
| } |
| |
| func init() { |
| beforeContext = maxErrorLength + 100 |
| tickerPeriod = 1 * time.Second |
| NoOutputTimeout = 5 * time.Second |
| waitForOutputTimeout = 3 * time.Second |
| |
| ctor := func(env *vmimpl.Env) (vmimpl.Pool, error) { |
| return &testPool{}, nil |
| } |
| vmimpl.Register("test", ctor, false) |
| } |
| |
| type Test struct { |
| Name string |
| Exit ExitCondition |
| DiagnoseBug bool // Diagnose produces output that is detected as kernel crash |
| DiagnoseNoWait bool // Diagnose returns output directly rather than to console |
| Body func(outc chan []byte, errc chan error) |
| Report *report.Report |
| } |
| |
| var tests = []*Test{ |
| { |
| Name: "program-exits-normally", |
| Exit: ExitNormal, |
| Body: func(outc chan []byte, errc chan error) { |
| time.Sleep(time.Second) |
| errc <- nil |
| }, |
| }, |
| { |
| Name: "program-exits-when-it-should-not", |
| Body: func(outc chan []byte, errc chan error) { |
| time.Sleep(time.Second) |
| errc <- nil |
| }, |
| Report: &report.Report{ |
| Title: lostConnectionCrash, |
| }, |
| }, |
| { |
| Name: "#875-diagnose-bugs", |
| Exit: ExitNormal, |
| DiagnoseBug: true, |
| Body: func(outc chan []byte, errc chan error) { |
| errc <- nil |
| }, |
| }, |
| { |
| Name: "#875-diagnose-bugs-2", |
| Body: func(outc chan []byte, errc chan error) { |
| errc <- nil |
| }, |
| Report: &report.Report{ |
| Title: lostConnectionCrash, |
| Output: []byte( |
| "DIAGNOSE\n", |
| ), |
| }, |
| }, |
| { |
| Name: "diagnose-no-wait", |
| Body: func(outc chan []byte, errc chan error) { |
| errc <- nil |
| }, |
| DiagnoseNoWait: true, |
| Report: &report.Report{ |
| Title: lostConnectionCrash, |
| Output: []byte( |
| "DIAGNOSIS:\nDIAGNOSE\n", |
| ), |
| }, |
| }, |
| { |
| Name: "kernel-crashes", |
| Body: func(outc chan []byte, errc chan error) { |
| outc <- []byte("BUG: bad\n") |
| time.Sleep(time.Second) |
| outc <- []byte("other output\n") |
| }, |
| Report: &report.Report{ |
| Title: "BUG: bad", |
| Report: []byte( |
| "BUG: bad\n" + |
| "DIAGNOSE\n" + |
| "other output\n", |
| ), |
| }, |
| }, |
| { |
| Name: "fuzzer-is-preempted", |
| Body: func(outc chan []byte, errc chan error) { |
| outc <- []byte("BUG: bad\n") |
| outc <- []byte(fuzzerPreemptedStr + "\n") |
| }, |
| }, |
| { |
| Name: "program-exits-but-kernel-crashes-afterwards", |
| Exit: ExitNormal, |
| Body: func(outc chan []byte, errc chan error) { |
| errc <- nil |
| time.Sleep(time.Second) |
| outc <- []byte("BUG: bad\n") |
| }, |
| Report: &report.Report{ |
| Title: "BUG: bad", |
| Report: []byte( |
| "BUG: bad\n" + |
| "DIAGNOSE\n", |
| ), |
| }, |
| }, |
| { |
| Name: "timeout", |
| Exit: ExitTimeout, |
| Body: func(outc chan []byte, errc chan error) { |
| errc <- vmimpl.ErrTimeout |
| }, |
| }, |
| { |
| Name: "bad-timeout", |
| Body: func(outc chan []byte, errc chan error) { |
| errc <- vmimpl.ErrTimeout |
| }, |
| Report: &report.Report{ |
| Title: timeoutCrash, |
| }, |
| }, |
| { |
| Name: "program-crashes", |
| Body: func(outc chan []byte, errc chan error) { |
| errc <- fmt.Errorf("error") |
| }, |
| Report: &report.Report{ |
| Title: lostConnectionCrash, |
| }, |
| }, |
| { |
| Name: "program-crashes-expected", |
| Exit: ExitError, |
| Body: func(outc chan []byte, errc chan error) { |
| errc <- fmt.Errorf("error") |
| }, |
| }, |
| { |
| Name: "no-output-1", |
| Body: func(outc chan []byte, errc chan error) { |
| }, |
| Report: &report.Report{ |
| Title: noOutputCrash, |
| }, |
| }, |
| { |
| Name: "no-output-2", |
| Body: func(outc chan []byte, errc chan error) { |
| for i := 0; i < 5; i++ { |
| time.Sleep(time.Second) |
| outc <- []byte("something\n") |
| } |
| }, |
| Report: &report.Report{ |
| Title: noOutputCrash, |
| }, |
| }, |
| { |
| Name: "no-no-output-1", |
| Exit: ExitNormal, |
| Body: func(outc chan []byte, errc chan error) { |
| for i := 0; i < 5; i++ { |
| time.Sleep(time.Second) |
| outc <- []byte(executingProgramStr1 + "\n") |
| } |
| errc <- nil |
| }, |
| }, |
| { |
| Name: "no-no-output-2", |
| Exit: ExitNormal, |
| Body: func(outc chan []byte, errc chan error) { |
| for i := 0; i < 5; i++ { |
| time.Sleep(time.Second) |
| outc <- []byte(executingProgramStr2 + "\n") |
| } |
| errc <- nil |
| }, |
| }, |
| { |
| Name: "outc-closed", |
| Exit: ExitTimeout, |
| Body: func(outc chan []byte, errc chan error) { |
| close(outc) |
| time.Sleep(time.Second) |
| errc <- vmimpl.ErrTimeout |
| }, |
| }, |
| { |
| Name: "lots-of-output", |
| Exit: ExitTimeout, |
| Body: func(outc chan []byte, errc chan error) { |
| for i := 0; i < 100; i++ { |
| outc <- []byte("something\n") |
| } |
| time.Sleep(time.Second) |
| errc <- vmimpl.ErrTimeout |
| }, |
| }, |
| { |
| Name: "split-line", |
| Exit: ExitNormal, |
| Body: func(outc chan []byte, errc chan error) { |
| // "ODEBUG:" lines should be ignored, however the matchPos logic |
| // used to trim the lines so that we could see just "BUG:" later |
| // and detect it as crash. |
| buf := new(bytes.Buffer) |
| for i := 0; i < 50; i++ { |
| buf.WriteString("[ 2886.597572] ODEBUG: Out of memory. ODEBUG disabled\n") |
| buf.Write(bytes.Repeat([]byte{'-'}, i)) |
| buf.WriteByte('\n') |
| } |
| output := buf.Bytes() |
| for i := range output { |
| outc <- output[i : i+1] |
| } |
| errc <- nil |
| }, |
| }, |
| } |
| |
| func TestMonitorExecution(t *testing.T) { |
| for _, test := range tests { |
| test := test |
| t.Run(test.Name, func(t *testing.T) { |
| t.Parallel() |
| testMonitorExecution(t, test) |
| }) |
| } |
| } |
| |
| func testMonitorExecution(t *testing.T, test *Test) { |
| dir, err := ioutil.TempDir("", "syz-vm-test") |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer os.RemoveAll(dir) |
| cfg := &mgrconfig.Config{ |
| Workdir: dir, |
| TargetOS: "linux", |
| TargetArch: "amd64", |
| TargetVMArch: "amd64", |
| Type: "test", |
| } |
| pool, err := Create(cfg, false) |
| if err != nil { |
| t.Fatal(err) |
| } |
| reporter, err := report.NewReporter(cfg) |
| if err != nil { |
| t.Fatal(err) |
| } |
| inst, err := pool.Create(0) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer inst.Close() |
| outc, errc, err := inst.Run(time.Second, nil, "") |
| if err != nil { |
| t.Fatal(err) |
| } |
| testInst := inst.impl.(*testInstance) |
| testInst.diagnoseBug = test.DiagnoseBug |
| testInst.diagnoseNoWait = test.DiagnoseNoWait |
| done := make(chan bool) |
| go func() { |
| test.Body(testInst.outc, testInst.errc) |
| done <- true |
| }() |
| rep := inst.MonitorExecution(outc, errc, reporter, test.Exit) |
| <-done |
| if test.Report != nil && rep == nil { |
| t.Fatalf("got no report") |
| } |
| if test.Report == nil && rep != nil { |
| t.Fatalf("got unexpected report: %v", rep.Title) |
| } |
| if test.Report == nil { |
| return |
| } |
| if test.Report.Title != rep.Title { |
| t.Fatalf("want title %q, got title %q", test.Report.Title, rep.Title) |
| } |
| if !bytes.Equal(test.Report.Report, rep.Report) { |
| t.Fatalf("want report:\n%s\n\ngot report:\n%s\n", test.Report.Report, rep.Report) |
| } |
| if test.Report.Output != nil && !bytes.Equal(test.Report.Output, rep.Output) { |
| t.Fatalf("want output:\n%s\n\ngot output:\n%s\n", test.Report.Output, rep.Output) |
| } |
| } |