blob: 6acc00351443f90c2236eb28197a46e6b88e62b3 [file] [log] [blame]
// 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"
"testing"
"time"
"github.com/google/syzkaller/pkg/mgrconfig"
"github.com/google/syzkaller/pkg/report"
"github.com/google/syzkaller/sys/targets"
"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
}
func (pool *testPool) Close() error {
return 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(rep *report.Report) ([]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() {
beforeContextDefault = maxErrorLength + 100
tickerPeriod = 1 * time.Second
waitForOutputTimeout = 3 * time.Second
ctor := func(env *vmimpl.Env) (vmimpl.Pool, error) {
return &testPool{}, nil
}
vmimpl.Register("test", ctor, false, 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.
InjectOutput string
Body func(outc chan []byte, errc chan error)
Report *report.Report
}
// nolint: goconst // "DIAGNOSE\n", "BUG: bad\n" and "other output\n"
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(
"\n" +
"VM DIAGNOSIS:\n" +
"DIAGNOSE\n",
),
},
},
{
Name: "diagnose-bug-no-wait",
Body: func(outc chan []byte, errc chan error) {
outc <- []byte("BUG: bad\n")
time.Sleep(time.Second)
outc <- []byte("other output\n")
},
DiagnoseNoWait: true,
Report: &report.Report{
Title: "BUG: bad",
Report: []byte(
"BUG: bad\n" +
"other output\n",
),
Output: []byte(
"BUG: bad\n" +
"other output\n" +
"\n" +
"VM DIAGNOSIS:\n" +
"DIAGNOSE\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 <- append(executingProgram1, '\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 <- append(executingProgram2, '\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
},
},
{
Name: "inject-error",
Exit: ExitNormal,
InjectOutput: "BUG: foo\n",
Body: func(outc chan []byte, errc chan error) {
time.Sleep(time.Second)
errc <- nil
},
Report: &report.Report{
Title: "BUG: foo",
Report: []byte("BUG: foo\nDIAGNOSE\n"),
},
},
{
Name: "inject-output",
Exit: ExitNormal,
InjectOutput: "INJECTED\n",
Body: func(outc chan []byte, errc chan error) {
time.Sleep(time.Second)
outc <- []byte("BUG: foo\n")
},
Report: &report.Report{
Title: "BUG: foo",
Report: []byte("INJECTED\nBUG: foo\nDIAGNOSE\n"),
},
},
}
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 := t.TempDir()
cfg := &mgrconfig.Config{
Derived: mgrconfig.Derived{
TargetOS: targets.Linux,
TargetArch: targets.AMD64,
TargetVMArch: targets.AMD64,
Timeouts: targets.Timeouts{
Scale: 1,
Slowdown: 1,
NoOutput: 5 * time.Second,
},
SysTarget: targets.Get(targets.Linux, targets.AMD64),
},
Workdir: dir,
Type: "test",
}
pool, err := Create(cfg, false)
if err != nil {
t.Fatal(err)
}
defer pool.Close()
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()
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
}()
opts := []any{test.Exit}
if test.InjectOutput != "" {
c := make(chan []byte, 1)
c <- []byte(test.InjectOutput)
opts = append(opts, InjectOutput(c))
}
_, rep, err := inst.Run(time.Second, reporter, "", opts...)
if err != nil {
t.Fatal(err)
}
<-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", 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", test.Report.Output, rep.Output)
}
}
func TestVMType(t *testing.T) {
testCases := []struct {
in string
want string
}{
{"gvisor", "gvisor"},
{"proxyapp:android", "proxyapp"},
}
for _, tc := range testCases {
if got := vmType(tc.in); got != tc.want {
t.Errorf("vmType(%q) = %q, want %q", tc.in, got, tc.want)
}
}
}