// Copyright 2022 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 proxyapp import ( "bytes" "fmt" "io" "net/rpc" "net/rpc/jsonrpc" "strings" "testing" "time" "github.com/google/syzkaller/pkg/report" "github.com/google/syzkaller/vm/proxyapp/proxyrpc" "github.com/google/syzkaller/vm/vmimpl" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) var testEnv = &vmimpl.Env{ Config: []byte(` { "cmd": "/path/to/proxyapp_binary", "config": { "internal_values": 123 } } `)} func makeTestParams() *proxyAppParams { return &proxyAppParams{ CommandRunner: osutilCommandContext, InitRetryDelay: 0, LogOutput: io.Discard, } } func makeMockProxyAppProcess(t *testing.T) ( *mockProxyAppInterface, io.WriteCloser, io.ReadCloser, io.ReadCloser) { rStdin, wStdin := io.Pipe() rStdout, wStdout := io.Pipe() rStderr, wStderr := io.Pipe() wStderr.Close() server := rpc.NewServer() handler := makeMockProxyAppInterface(t) server.RegisterName("ProxyVM", struct{ proxyrpc.ProxyAppInterface }{handler}) go server.ServeCodec(jsonrpc.NewServerCodec(stdInOutCloser{ rStdin, wStdout, })) return handler, wStdin, rStdout, rStderr } type nopWriteCloser struct { io.Writer } func (nopWriteCloser) Close() error { return nil } func TestCtor_Ok(t *testing.T) { _, mCmdRunner, params := proxyAppServerFixture(t) p, err := ctor(params, testEnv) assert.Nil(t, err) assert.Equal(t, 2, p.Count()) <-mCmdRunner.onWaitCalled } func TestCtor_ReadBadConfig(t *testing.T) { pool, err := ctor(makeTestParams(), &vmimpl.Env{ Config: []byte(`{"wrong_key": 1}`), }) assert.NotNil(t, err) assert.Nil(t, pool) } func TestCtor_FailedPipes(t *testing.T) { mCmdRunner, params := makeMockCommandRunner(t) mCmdRunner. On("StdinPipe"). Return(nil, fmt.Errorf("stdinpipe error")). Once(). On("StdinPipe"). Return(nopWriteCloser{&bytes.Buffer{}}, nil). On("StdoutPipe"). Return(nil, fmt.Errorf("stdoutpipe error")). Once(). On("StdoutPipe"). Return(io.NopCloser(strings.NewReader("")), nil). On("StderrPipe"). Return(nil, fmt.Errorf("stderrpipe error")). Once(). On("StderrPipe"). Return(io.NopCloser(strings.NewReader("")), nil) for i := 0; i < 3; i++ { p, err := ctor(params, testEnv) assert.NotNil(t, err) assert.Nil(t, p) } } func TestClose_waitDone(t *testing.T) { _, mCmdRunner, params := proxyAppServerFixture(t) mCmdRunner. On("waitDone"). Return(nil) p, _ := ctor(params, testEnv) p.(io.Closer).Close() } func TestCtor_FailedStartProxyApp(t *testing.T) { mCmdRunner, params := makeMockCommandRunner(t) mCmdRunner. On("StdinPipe"). Return(nopWriteCloser{&bytes.Buffer{}}, nil). On("StdoutPipe"). Return(io.NopCloser(strings.NewReader("")), nil). On("StderrPipe"). Return(io.NopCloser(strings.NewReader("")), nil). On("Start"). Return(fmt.Errorf("failed to start program")) p, err := ctor(params, testEnv) assert.NotNil(t, err) assert.Nil(t, p) } // TODO: reuse proxyAppServerFixture() code: func could be called here once Mock.Unset() error // // fixed https://github.com/stretchr/testify/issues/1236 // nolint: dupl func TestCtor_FailedConstructPool(t *testing.T) { mProxyAppServer, stdin, stdout, stderr := makeMockProxyAppProcess(t) mProxyAppServer. On("CreatePool", mock.Anything, mock.Anything). Return(fmt.Errorf("failed to construct pool")). On("PoolLogs", mock.Anything, mock.Anything). Return(nil). Maybe() // on CreatePool error we close logger. This close makes PoolLogs racy. mCmdRunner, params := makeMockCommandRunner(t) mCmdRunner. On("StdinPipe"). Return(stdin, nil). On("StdoutPipe"). Return(stdout, nil). On("StderrPipe"). Return(stderr, nil). On("Start"). Return(nil). On("Wait"). Run(func(args mock.Arguments) { <-mCmdRunner.ctx.Done() }). Return(nil) p, err := ctor(params, testEnv) assert.NotNil(t, err) assert.Nil(t, p) } func initProxyAppServerFixture(mProxyAppServer *mockProxyAppInterface) *mockProxyAppInterface { mProxyAppServer. On("CreatePool", mock.Anything, mock.Anything). Run(func(args mock.Arguments) { out := args.Get(1).(*proxyrpc.CreatePoolResult) out.Count = 2 }). Return(nil). Once(). On("PoolLogs", mock.Anything, mock.Anything). Run(func(args mock.Arguments) { select { case mProxyAppServer.OnLogsReceived <- true: default: } }). Return(nil). // PoolLogs is optional as we can call .closeProxy any time. // If PoolLogs call is expected we are checking for OnLogsReceived. // TODO: refactor it once Mock.Unset() is available. Maybe() return mProxyAppServer } // TODO: to remove duplicate see TestCtor_FailedConstructPool() comment // // nolint: dupl func proxyAppServerFixture(t *testing.T) (*mockProxyAppInterface, *mockCommandRunner, *proxyAppParams) { mProxyAppServer, stdin, stdout, stderr := makeMockProxyAppProcess(t) initProxyAppServerFixture(mProxyAppServer) mCmdRunner, params := makeMockCommandRunner(t) mCmdRunner. On("StdinPipe"). Return(stdin, nil). On("StdoutPipe"). Return(stdout, nil). On("StderrPipe"). Return(stderr, nil). On("Start"). Return(nil). On("Wait"). Run(func(args mock.Arguments) { <-mCmdRunner.ctx.Done() mCmdRunner.MethodCalled("waitDone") }). Return(nil). Maybe() return mProxyAppServer, mCmdRunner, params } func poolFixture(t *testing.T) (*mockProxyAppInterface, *mockCommandRunner, vmimpl.Pool) { mProxyAppServer, mCmdRunner, params := proxyAppServerFixture(t) p, _ := ctor(params, testEnv) return mProxyAppServer, mCmdRunner, p } func TestPool_Create_Ok(t *testing.T) { mockServer, _, p := poolFixture(t) mockServer. On("CreateInstance", mock.Anything, mock.Anything). Return(nil) inst, err := p.Create("workdir", 0) assert.NotNil(t, inst) assert.Nil(t, err) } func TestPool_Logs_Ok(t *testing.T) { mockServer, _, _ := poolFixture(t) <-mockServer.OnLogsReceived } func TestPool_Create_ProxyNilError(t *testing.T) { _, mCmdRunner, p := poolFixture(t) mCmdRunner. On("waitDone"). Return(nil) p.(io.Closer).Close() inst, err := p.Create("workdir", 0) assert.Nil(t, inst) assert.NotNil(t, err) } func TestPool_Create_OutOfPoolError(t *testing.T) { mockServer, _, p := poolFixture(t) mockServer. On("CreateInstance", mock.Anything, mock.Anything). Run(func(args mock.Arguments) { in := args.Get(0).(proxyrpc.CreateInstanceParams) assert.Equal(t, p.Count(), in.Index) }). Return(fmt.Errorf("out of pool size")) inst, err := p.Create("workdir", p.Count()) assert.Nil(t, inst) assert.NotNil(t, err) } func TestPool_Create_ProxyFailure(t *testing.T) { mockServer, _, p := poolFixture(t) mockServer. On("CreateInstance", mock.Anything, mock.Anything). Return(fmt.Errorf("create instance failure")) inst, err := p.Create("workdir", 0) assert.Nil(t, inst) assert.NotNil(t, err) } // nolint: dupl func createInstanceFixture(t *testing.T) (*mock.Mock, vmimpl.Instance) { mockServer, _, p := poolFixture(t) mockServer. On("CreateInstance", mock.Anything, mock.Anything). Run(func(args mock.Arguments) { in := args.Get(0).(proxyrpc.CreateInstanceParams) out := args.Get(1).(*proxyrpc.CreateInstanceResult) out.ID = fmt.Sprintf("instance_id_%v", in.Index) }). Return(nil) inst, err := p.Create("workdir", 0) assert.Nil(t, err) assert.NotNil(t, inst) return &mockServer.Mock, inst } func TestInstance_Close(t *testing.T) { mockInstance, inst := createInstanceFixture(t) mockInstance. On("Close", mock.Anything, mock.Anything). Return(fmt.Errorf("mock error")) inst.Close() } func TestInstance_Diagnose_Ok(t *testing.T) { mockInstance, inst := createInstanceFixture(t) mockInstance. On("Diagnose", mock.Anything, mock.Anything). Run(func(args mock.Arguments) { out := args.Get(1).(*proxyrpc.DiagnoseReply) out.Diagnosis = "diagnostic result" }). Return(nil) diagnosis, wait := inst.Diagnose(nil) assert.NotNil(t, diagnosis) assert.Equal(t, wait, false) diagnosis, wait = inst.Diagnose(&report.Report{}) assert.NotNil(t, diagnosis) assert.Equal(t, wait, false) } func TestInstance_Diagnose_Failure(t *testing.T) { mockInstance, inst := createInstanceFixture(t) mockInstance. On("Diagnose", mock.Anything, mock.Anything). Return(fmt.Errorf("diagnose failed")) diagnosis, wait := inst.Diagnose(&report.Report{}) assert.Nil(t, diagnosis) assert.Equal(t, wait, false) } func TestInstance_Copy_OK(t *testing.T) { mockInstance, inst := createInstanceFixture(t) mockInstance. On("Copy", mock.Anything, mock.Anything). Run(func(args mock.Arguments) { out := args.Get(1).(*proxyrpc.CopyResult) out.VMFileName = "remote_file_path" }). Return(nil) remotePath, err := inst.Copy("host/path") assert.Nil(t, err) assert.NotEmpty(t, remotePath) } // nolint: dupl func TestInstance_Copy_Failure(t *testing.T) { mockInstance, inst := createInstanceFixture(t) mockInstance. On("Copy", mock.Anything, mock.Anything). Return(fmt.Errorf("copy failure")) remotePath, err := inst.Copy("host/path") assert.NotNil(t, err) assert.Empty(t, remotePath) } // nolint: dupl func TestInstance_Forward_OK(t *testing.T) { mockInstance, inst := createInstanceFixture(t) mockInstance. On("Forward", mock.Anything, mock.Anything). Run(func(args mock.Arguments) { in := args.Get(0).(proxyrpc.ForwardParams) out := args.Get(1).(*proxyrpc.ForwardResult) out.ManagerAddress = fmt.Sprintf("manager_address:%v", in.Port) }). Return(nil) remoteAddressToUse, err := inst.Forward(12345) assert.Nil(t, err) assert.Equal(t, "manager_address:12345", remoteAddressToUse) } // nolint: dupl func TestInstance_Forward_Failure(t *testing.T) { mockInstance, inst := createInstanceFixture(t) mockInstance. On("Forward", mock.Anything, mock.Anything). Return(fmt.Errorf("forward failure")) remoteAddressToUse, err := inst.Forward(12345) assert.NotNil(t, err) assert.Empty(t, remoteAddressToUse) } func TestInstance_Run_SimpleOk(t *testing.T) { mockInstance, inst := createInstanceFixture(t) mockInstance. On("RunStart", mock.Anything, mock.Anything). Return(nil). On("RunReadProgress", mock.Anything, mock.Anything). Return(nil). Maybe() outc, errc, err := inst.Run(10*time.Second, make(chan bool), "command") assert.NotNil(t, outc) assert.NotNil(t, errc) assert.Nil(t, err) } func TestInstance_Run_Failure(t *testing.T) { mockInstance, inst := createInstanceFixture(t) mockInstance. On("RunStart", mock.Anything, mock.Anything). Return(fmt.Errorf("run start error")) outc, errc, err := inst.Run(10*time.Second, make(chan bool), "command") assert.Nil(t, outc) assert.Nil(t, errc) assert.NotEmpty(t, err) } func TestInstance_Run_OnTimeout(t *testing.T) { mockInstance, inst := createInstanceFixture(t) mockInstance. On("RunStart", mock.Anything, mock.Anything). Return(nil). On("RunReadProgress", mock.Anything, mock.Anything). Return(nil).Maybe(). On("RunStop", mock.Anything, mock.Anything). Return(nil) _, errc, _ := inst.Run(time.Second, make(chan bool), "command") err := <-errc assert.Equal(t, err, vmimpl.ErrTimeout) } func TestInstance_Run_OnStop(t *testing.T) { mockInstance, inst := createInstanceFixture(t) mockInstance. On("RunStart", mock.Anything, mock.Anything). Return(nil). On("RunReadProgress", mock.Anything, mock.Anything). Return(nil). Maybe(). On("RunStop", mock.Anything, mock.Anything). Return(nil) stop := make(chan bool) _, errc, _ := inst.Run(10*time.Second, stop, "command") stop <- true err := <-errc assert.Equal(t, err, vmimpl.ErrTimeout) } func TestInstance_RunReadProgress_OnErrorReceived(t *testing.T) { mockInstance, inst := createInstanceFixture(t) mockInstance. On("RunStart", mock.Anything, mock.Anything). Return(nil). On("RunReadProgress", mock.Anything, mock.Anything). Return(nil). Times(100). On("RunReadProgress", mock.Anything, mock.Anything). Run(func(args mock.Arguments) { out := args.Get(1).(*proxyrpc.RunReadProgressReply) out.Error = "mock error" }). Return(nil). Once() outc, _, _ := inst.Run(10*time.Second, make(chan bool), "command") output := string(<-outc) assert.Equal(t, "mock error\nSYZFAIL: proxy app plugin error\n", output) } // nolint: dupl func TestInstance_RunReadProgress_OnFinished(t *testing.T) { mockInstance, inst := createInstanceFixture(t) mockInstance. On("RunStart", mock.Anything, mock.Anything). Return(nil). On("RunReadProgress", mock.Anything, mock.Anything). Return(nil).Times(100). On("RunReadProgress", mock.Anything, mock.Anything). Run(func(args mock.Arguments) { out := args.Get(1).(*proxyrpc.RunReadProgressReply) out.Finished = true }). Return(nil). Once() _, errc, _ := inst.Run(10*time.Second, make(chan bool), "command") err := <-errc assert.Equal(t, err, nil) } func TestInstance_RunReadProgress_Failed(t *testing.T) { mockInstance, inst := createInstanceFixture(t) mockInstance. On("RunStart", mock.Anything, mock.Anything). Run(func(args mock.Arguments) { out := args.Get(1).(*proxyrpc.RunStartReply) out.RunID = "test_run_id" }). Return(nil). On("RunReadProgress", mock.Anything, mock.Anything). Return(fmt.Errorf("runreadprogresserror")). Once() outc, _, _ := inst.Run(10*time.Second, make(chan bool), "command") output := string(<-outc) assert.Equal(t, "error reading progress from instance_id_0:test_run_id: runreadprogresserror\nSYZFAIL: proxy app plugin error\n", output, ) } // TODO: test for periodical proxyapp subprocess crashes handling. // [option] check pool size was changed // TODO: test pool.Close() calls plugin API and return error.