github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/vm/proxyapp/proxyappclient.go (about)

     1  // Copyright 2022 syzkaller project authors. All rights reserved.
     2  // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
     3  
     4  // Package proxyapp package implements the experimental plugins support.
     5  // We promise interface part will not be stable until documented.
     6  package proxyapp
     7  
     8  import (
     9  	"context"
    10  	"crypto/tls"
    11  	"crypto/x509"
    12  	"fmt"
    13  	"io"
    14  	"io/fs"
    15  	"net"
    16  	"net/rpc"
    17  	"net/rpc/jsonrpc"
    18  	"os"
    19  	"path/filepath"
    20  	"strings"
    21  	"sync"
    22  	"time"
    23  
    24  	"github.com/google/syzkaller/pkg/log"
    25  	"github.com/google/syzkaller/pkg/report"
    26  	"github.com/google/syzkaller/vm/proxyapp/proxyrpc"
    27  	"github.com/google/syzkaller/vm/vmimpl"
    28  )
    29  
    30  func ctor(params *proxyAppParams, env *vmimpl.Env) (vmimpl.Pool, error) {
    31  	subConfig, err := parseConfig(env.Config)
    32  	if err != nil {
    33  		return nil, fmt.Errorf("config parse error: %w", err)
    34  	}
    35  
    36  	p := &pool{
    37  		env:      env,
    38  		close:    make(chan bool, 1),
    39  		onClosed: make(chan error, 1),
    40  	}
    41  
    42  	err = p.init(params, subConfig)
    43  	if err != nil {
    44  		return nil, fmt.Errorf("can't initialize pool: %w", err)
    45  	}
    46  
    47  	go func() {
    48  		var forceReinit <-chan time.Time
    49  		for {
    50  			var onTerminated chan bool
    51  			var onLostConnection chan bool
    52  			p.mu.Lock()
    53  			if p.proxy != nil {
    54  				onTerminated = p.proxy.onTerminated
    55  				onLostConnection = p.proxy.onLostConnection
    56  			}
    57  			p.mu.Unlock()
    58  
    59  			select {
    60  			case <-p.close:
    61  				p.mu.Lock()
    62  				p.closeProxy()
    63  
    64  				p.onClosed <- nil
    65  				p.mu.Unlock()
    66  				return
    67  			case <-onTerminated:
    68  			case <-onLostConnection:
    69  			case <-forceReinit:
    70  			}
    71  			p.mu.Lock()
    72  			p.closeProxy()
    73  			time.Sleep(params.InitRetryDelay)
    74  			forceReinit = nil
    75  			err := p.init(params, subConfig)
    76  			if err != nil {
    77  				forceReinit = time.After(100 * time.Millisecond)
    78  			}
    79  			p.mu.Unlock()
    80  		}
    81  	}()
    82  
    83  	return p, nil
    84  }
    85  
    86  type pool struct {
    87  	mu       sync.Mutex
    88  	env      *vmimpl.Env
    89  	proxy    *ProxyApp
    90  	count    int
    91  	close    chan bool
    92  	onClosed chan error
    93  }
    94  
    95  func (p *pool) init(params *proxyAppParams, cfg *Config) error {
    96  	usePipedRPC := cfg.RPCServerURI == ""
    97  	useTCPRPC := !usePipedRPC
    98  	var err error
    99  	if cfg.Command != "" {
   100  		p.proxy, err = runProxyApp(params, cfg.Command, usePipedRPC)
   101  	} else {
   102  		p.proxy = &ProxyApp{
   103  			transferFileContent: cfg.TransferFileContent,
   104  		}
   105  	}
   106  	if err != nil {
   107  		return fmt.Errorf("failed to run ProxyApp: %w", err)
   108  	}
   109  
   110  	if useTCPRPC {
   111  		p.proxy.onLostConnection = make(chan bool, 1)
   112  		p.proxy.Client, err = initNetworkRPCClient(cfg)
   113  		if err != nil {
   114  			p.closeProxy()
   115  			return fmt.Errorf("failed to connect ProxyApp pipes: %w", err)
   116  		}
   117  	}
   118  
   119  	p.proxy.doLogPooling(params.LogOutput)
   120  
   121  	count, err := p.proxy.CreatePool(cfg, p.env.Image, p.env.Debug)
   122  	if err != nil || count == 0 || (p.count != 0 && p.count != count) {
   123  		if err == nil {
   124  			err = fmt.Errorf("wrong pool size %v, prev was %v", count, p.count)
   125  		}
   126  		p.closeProxy()
   127  		return fmt.Errorf("failed to construct pool: %w", err)
   128  	}
   129  
   130  	if p.count == 0 {
   131  		p.count = count
   132  	}
   133  	return nil
   134  }
   135  
   136  func (p *pool) closeProxy() {
   137  	if p.proxy != nil {
   138  		if p.proxy.stopLogPooling != nil {
   139  			p.proxy.stopLogPooling <- true
   140  			<-p.proxy.logPoolingDone
   141  		}
   142  		if p.proxy.Client != nil {
   143  			p.proxy.Client.Close()
   144  		}
   145  		if p.proxy.terminate != nil {
   146  			p.proxy.terminate()
   147  			<-p.proxy.onTerminated
   148  		}
   149  	}
   150  	p.proxy = nil
   151  }
   152  
   153  func (p *pool) Count() int {
   154  	return p.count
   155  }
   156  
   157  func (p *pool) Create(workdir string, index int) (vmimpl.Instance, error) {
   158  	p.mu.Lock()
   159  	proxy := p.proxy
   160  	p.mu.Unlock()
   161  
   162  	if proxy == nil {
   163  		return nil, fmt.Errorf("can't create instance using nil pool")
   164  	}
   165  
   166  	return proxy.CreateInstance(workdir, p.env.Image, index)
   167  }
   168  
   169  // Close is not used now. Its support require wide code changes.
   170  // TODO: support the pool cleanup on syz-manager level.
   171  func (p *pool) Close() error {
   172  	close(p.close)
   173  	return <-p.onClosed
   174  }
   175  
   176  type ProxyApp struct {
   177  	*rpc.Client
   178  	transferFileContent bool
   179  	terminate           context.CancelFunc
   180  	onTerminated        chan bool
   181  	onLostConnection    chan bool
   182  	stopLogPooling      chan bool
   183  	logPoolingDone      chan bool
   184  }
   185  
   186  func initPipedRPCClient(cmd subProcessCmd) (*rpc.Client, []io.Closer, error) {
   187  	subStdout, err := cmd.StdoutPipe()
   188  	if err != nil {
   189  		return nil, nil, fmt.Errorf("failed to get stdoutpipe: %w", err)
   190  	}
   191  
   192  	subStdin, err := cmd.StdinPipe()
   193  	if err != nil {
   194  		subStdout.Close()
   195  		return nil, nil, fmt.Errorf("failed to get stdinpipe: %w", err)
   196  	}
   197  
   198  	return jsonrpc.NewClient(stdInOutCloser{
   199  			subStdout,
   200  			subStdin,
   201  		}),
   202  		[]io.Closer{subStdin, subStdout},
   203  		nil
   204  }
   205  
   206  func initNetworkRPCClient(cfg *Config) (*rpc.Client, error) {
   207  	var conn io.ReadWriteCloser
   208  
   209  	switch cfg.Security {
   210  	case "none":
   211  		var err error
   212  		conn, err = net.Dial("tcp", cfg.RPCServerURI)
   213  		if err != nil {
   214  			return nil, fmt.Errorf("dial: %w", err)
   215  		}
   216  	case "tls":
   217  		var certPool *x509.CertPool
   218  
   219  		if cfg.ServerTLSCert != "" {
   220  			certPool = x509.NewCertPool()
   221  			b, err := os.ReadFile(cfg.ServerTLSCert)
   222  			if err != nil {
   223  				return nil, fmt.Errorf("read server certificate: %w", err)
   224  			}
   225  			if !certPool.AppendCertsFromPEM(b) {
   226  				return nil, fmt.Errorf("append server certificate to empty pool: %w", err)
   227  			}
   228  		}
   229  
   230  		var err error
   231  		conn, err = tls.Dial("tcp", cfg.RPCServerURI, &tls.Config{RootCAs: certPool})
   232  		if err != nil {
   233  			return nil, fmt.Errorf("dial with tls: %w", err)
   234  		}
   235  	case "mtls":
   236  		return nil, fmt.Errorf("mutual TLS not implemented")
   237  	default:
   238  		return nil, fmt.Errorf("security value is %q, must be 'none', 'tls', or 'mtls'", cfg.Security)
   239  	}
   240  
   241  	return jsonrpc.NewClient(conn), nil
   242  }
   243  
   244  func runProxyApp(params *proxyAppParams, cmd string, initRPClient bool) (*ProxyApp, error) {
   245  	ctx, cancelContext := context.WithCancel(context.Background())
   246  	subProcess := params.CommandRunner(ctx, cmd)
   247  	var toClose []io.Closer
   248  	freeAll := func() {
   249  		for _, closer := range toClose {
   250  			closer.Close()
   251  		}
   252  		cancelContext()
   253  	}
   254  
   255  	var client *rpc.Client
   256  	if initRPClient {
   257  		var err error
   258  		var resources []io.Closer
   259  		client, resources, err = initPipedRPCClient(subProcess)
   260  		if err != nil {
   261  			freeAll()
   262  			return nil, fmt.Errorf("failed to init piped client: %w", err)
   263  		}
   264  		toClose = append(toClose, resources...)
   265  	}
   266  
   267  	subprocessLogs, err := subProcess.StderrPipe()
   268  	if err != nil {
   269  		freeAll()
   270  		return nil, fmt.Errorf("failed to get stderrpipe: %w", err)
   271  	}
   272  	toClose = append(toClose, subprocessLogs)
   273  
   274  	if err := subProcess.Start(); err != nil {
   275  		freeAll()
   276  		return nil, fmt.Errorf("failed to start command %v: %w", cmd, err)
   277  	}
   278  
   279  	onTerminated := make(chan bool, 1)
   280  
   281  	go func() {
   282  		io.Copy(params.LogOutput, subprocessLogs)
   283  		if err := subProcess.Wait(); err != nil {
   284  			log.Logf(0, "failed to Wait() subprocess: %v", err)
   285  		}
   286  		onTerminated <- true
   287  	}()
   288  
   289  	return &ProxyApp{
   290  		Client:       client,
   291  		terminate:    cancelContext,
   292  		onTerminated: onTerminated,
   293  	}, nil
   294  }
   295  
   296  func (proxy *ProxyApp) signalLostConnection() {
   297  	select {
   298  	case proxy.onLostConnection <- true:
   299  	default:
   300  	}
   301  }
   302  
   303  func (proxy *ProxyApp) Call(serviceMethod string, args, reply interface{}) error {
   304  	err := proxy.Client.Call(serviceMethod, args, reply)
   305  	if err == rpc.ErrShutdown {
   306  		proxy.signalLostConnection()
   307  	}
   308  	return err
   309  }
   310  
   311  func (proxy *ProxyApp) doLogPooling(writer io.Writer) {
   312  	proxy.stopLogPooling = make(chan bool, 1)
   313  	proxy.logPoolingDone = make(chan bool, 1)
   314  	go func() {
   315  		defer func() { proxy.logPoolingDone <- true }()
   316  		for {
   317  			var reply proxyrpc.PoolLogsReply
   318  			call := proxy.Go(
   319  				"ProxyVM.PoolLogs",
   320  				&proxyrpc.PoolLogsParam{},
   321  				&reply,
   322  				nil,
   323  			)
   324  			select {
   325  			case <-proxy.stopLogPooling:
   326  				return
   327  			case c := <-call.Done:
   328  				if c.Error != nil {
   329  					// possible errors here are:
   330  					// "unexpected EOF"
   331  					// "read tcp 127.0.0.1:56886->127.0.0.1:34603: use of closed network connection"
   332  					// rpc.ErrShutdown
   333  					log.Logf(0, "error pooling ProxyApp logs: %v", c.Error)
   334  					proxy.signalLostConnection()
   335  					return
   336  				}
   337  				if log.V(reply.Verbosity) {
   338  					writer.Write([]byte(fmt.Sprintf("ProxyAppLog: %v", reply.Log)))
   339  				}
   340  			}
   341  		}
   342  	}()
   343  }
   344  
   345  func (proxy *ProxyApp) CreatePool(config *Config, image string, debug bool) (int, error) {
   346  	var reply proxyrpc.CreatePoolResult
   347  	params := proxyrpc.CreatePoolParams{
   348  		Debug: debug,
   349  		Param: string(config.ProxyAppConfig),
   350  		Image: image,
   351  	}
   352  
   353  	if config.TransferFileContent {
   354  		imageData, err := os.ReadFile(image)
   355  		if err != nil {
   356  			return 0, fmt.Errorf("read image on host: %w", err)
   357  		}
   358  
   359  		params.ImageData = imageData
   360  	}
   361  
   362  	err := proxy.Call(
   363  		"ProxyVM.CreatePool",
   364  		params,
   365  		&reply)
   366  
   367  	if err != nil {
   368  		return 0, err
   369  	}
   370  
   371  	return reply.Count, nil
   372  }
   373  
   374  func (proxy *ProxyApp) CreateInstance(workdir, image string, index int) (vmimpl.Instance, error) {
   375  	var reply proxyrpc.CreateInstanceResult
   376  
   377  	params := proxyrpc.CreateInstanceParams{
   378  		Workdir: workdir,
   379  		Index:   index,
   380  	}
   381  
   382  	if proxy.transferFileContent {
   383  		workdirData := make(map[string][]byte)
   384  
   385  		err := filepath.WalkDir(workdir, func(path string, d fs.DirEntry, e error) error {
   386  			if d.IsDir() {
   387  				return nil
   388  			}
   389  
   390  			name := strings.TrimPrefix(path, workdir)
   391  
   392  			data, err := os.ReadFile(path)
   393  			if err != nil {
   394  				return fmt.Errorf("read file on host: %w", err)
   395  			}
   396  
   397  			workdirData[name] = data
   398  
   399  			return nil
   400  		})
   401  
   402  		if err != nil {
   403  			return nil, fmt.Errorf("failed to walk workdir: %w", err)
   404  		}
   405  
   406  		params.WorkdirData = workdirData
   407  	}
   408  
   409  	err := proxy.Call("ProxyVM.CreateInstance", params, &reply)
   410  	if err != nil {
   411  		return nil, fmt.Errorf("failed to proxy.Call(\"ProxyVM.CreateInstance\"): %w", err)
   412  	}
   413  
   414  	return &instance{
   415  		ProxyApp: proxy,
   416  		ID:       reply.ID,
   417  	}, nil
   418  }
   419  
   420  type instance struct {
   421  	*ProxyApp
   422  	ID string
   423  }
   424  
   425  // Copy copies a hostSrc file into VM and returns file name in VM.
   426  // nolint: dupl
   427  func (inst *instance) Copy(hostSrc string) (string, error) {
   428  	var reply proxyrpc.CopyResult
   429  	params := proxyrpc.CopyParams{
   430  		ID:      inst.ID,
   431  		HostSrc: hostSrc,
   432  	}
   433  
   434  	if inst.ProxyApp.transferFileContent {
   435  		data, err := os.ReadFile(hostSrc)
   436  		if err != nil {
   437  			return "", fmt.Errorf("read file on host: %w", err)
   438  		}
   439  
   440  		params.Data = data
   441  	}
   442  
   443  	err := inst.ProxyApp.Call("ProxyVM.Copy", params, &reply)
   444  	if err != nil {
   445  		return "", err
   446  	}
   447  
   448  	return reply.VMFileName, nil
   449  }
   450  
   451  // Forward sets up forwarding from within VM to the given tcp
   452  // port on the host and returns the address to use in VM.
   453  // nolint: dupl
   454  func (inst *instance) Forward(port int) (string, error) {
   455  	var reply proxyrpc.ForwardResult
   456  	err := inst.ProxyApp.Call(
   457  		"ProxyVM.Forward",
   458  		proxyrpc.ForwardParams{
   459  			ID:   inst.ID,
   460  			Port: port,
   461  		},
   462  		&reply)
   463  	if err != nil {
   464  		return "", err
   465  	}
   466  	return reply.ManagerAddress, nil
   467  }
   468  
   469  func buildMerger(names ...string) (*vmimpl.OutputMerger, []io.Writer) {
   470  	var wPipes []io.Writer
   471  	merger := vmimpl.NewOutputMerger(nil)
   472  	for _, name := range names {
   473  		rpipe, wpipe := io.Pipe()
   474  		wPipes = append(wPipes, wpipe)
   475  		merger.Add(name, rpipe)
   476  	}
   477  	return merger, wPipes
   478  }
   479  
   480  func (inst *instance) Run(
   481  	timeout time.Duration,
   482  	stop <-chan bool,
   483  	command string,
   484  ) (<-chan []byte, <-chan error, error) {
   485  	merger, wPipes := buildMerger("stdout", "stderr", "console")
   486  	receivedStdoutChunks := wPipes[0]
   487  	receivedStderrChunks := wPipes[1]
   488  	receivedConsoleChunks := wPipes[2]
   489  	outc := merger.Output
   490  
   491  	var reply proxyrpc.RunStartReply
   492  	err := inst.ProxyApp.Call(
   493  		"ProxyVM.RunStart",
   494  		proxyrpc.RunStartParams{
   495  			ID:      inst.ID,
   496  			Command: command},
   497  		&reply)
   498  
   499  	if err != nil {
   500  		return nil, nil, fmt.Errorf("error calling ProxyVM.Run with command %v: %w", command, err)
   501  	}
   502  
   503  	runID := reply.RunID
   504  	terminationError := make(chan error, 1)
   505  	timeoutSignal := time.After(timeout)
   506  	signalClientErrorf := clientErrorf(receivedStderrChunks)
   507  
   508  	go func() {
   509  		for {
   510  			var progress proxyrpc.RunReadProgressReply
   511  			readProgressCall := inst.ProxyApp.Go(
   512  				"ProxyVM.RunReadProgress",
   513  				proxyrpc.RunReadProgressParams{
   514  					ID:    inst.ID,
   515  					RunID: runID,
   516  				},
   517  				&progress,
   518  				nil)
   519  			select {
   520  			case <-readProgressCall.Done:
   521  				receivedStdoutChunks.Write([]byte(progress.StdoutChunk))
   522  				receivedStderrChunks.Write([]byte(progress.StderrChunk))
   523  				receivedConsoleChunks.Write([]byte(progress.ConsoleOutChunk))
   524  				if readProgressCall.Error != nil {
   525  					signalClientErrorf("error reading progress from %v:%v: %v",
   526  						inst.ID, runID, readProgressCall.Error)
   527  				} else if progress.Error != "" {
   528  					signalClientErrorf("%v", progress.Error)
   529  				} else if progress.Finished {
   530  					terminationError <- nil
   531  				} else {
   532  					continue
   533  				}
   534  			case <-timeoutSignal:
   535  				// It is the happy path.
   536  				inst.runStop(runID)
   537  				terminationError <- vmimpl.ErrTimeout
   538  			case <-stop:
   539  				inst.runStop(runID)
   540  				terminationError <- vmimpl.ErrTimeout
   541  			}
   542  			break
   543  		}
   544  	}()
   545  	return outc, terminationError, nil
   546  }
   547  
   548  func (inst *instance) runStop(runID string) {
   549  	err := inst.ProxyApp.Call(
   550  		"ProxyVM.RunStop",
   551  		proxyrpc.RunStopParams{
   552  			ID:    inst.ID,
   553  			RunID: runID,
   554  		},
   555  		&proxyrpc.RunStopParams{})
   556  	if err != nil {
   557  		log.Logf(0, "error calling runStop(%v) on %v: %v", runID, inst.ID, err)
   558  	}
   559  }
   560  
   561  func (inst *instance) Diagnose(r *report.Report) (diagnosis []byte, wait bool) {
   562  	var title string
   563  	if r != nil {
   564  		title = r.Title
   565  	}
   566  	var reply proxyrpc.DiagnoseReply
   567  	err := inst.ProxyApp.Call(
   568  		"ProxyVM.Diagnose",
   569  		proxyrpc.DiagnoseParams{
   570  			ID:          inst.ID,
   571  			ReasonTitle: title,
   572  		},
   573  		&reply)
   574  	if err != nil {
   575  		return nil, false
   576  	}
   577  
   578  	return []byte(reply.Diagnosis), false
   579  }
   580  
   581  func (inst *instance) Close() {
   582  	var reply proxyrpc.CloseReply
   583  	err := inst.ProxyApp.Call(
   584  		"ProxyVM.Close",
   585  		proxyrpc.CloseParams{
   586  			ID: inst.ID,
   587  		},
   588  		&reply)
   589  	if err != nil {
   590  		log.Logf(0, "error closing instance %v: %v", inst.ID, err)
   591  	}
   592  }
   593  
   594  type stdInOutCloser struct {
   595  	io.ReadCloser
   596  	io.Writer
   597  }
   598  
   599  func clientErrorf(writer io.Writer) func(fmt string, s ...interface{}) {
   600  	return func(f string, s ...interface{}) {
   601  		writer.Write([]byte(fmt.Sprintf(f, s...)))
   602  		writer.Write([]byte("\nSYZFAIL: proxy app plugin error\n"))
   603  	}
   604  }