syzkaller/vm/vm_test.go
Dmitry Vyukov dfc7d235f5 vm: fix spurious crash detection caused by trimmed lines
We've got a case when "ODEBUG:" was incorrectly detected as crash.
That was caused by a flaw in matchPos logic. Fix that.
See the added test for details.
2019-06-24 10:14:58 +02:00

369 lines
7.8 KiB
Go

// 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)
}
}