| // 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. |