blob: 0f199c0c8eeb4f04a5a7a95361347ead5f896681 [file] [log] [blame] [edit]
// 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.