github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/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(_ context.Context, 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  					fmt.Fprintf(writer, "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(ctx context.Context, command string) (<-chan []byte, <-chan error, error) {
   481  	merger, wPipes := buildMerger("stdout", "stderr", "console")
   482  	receivedStdoutChunks := wPipes[0]
   483  	receivedStderrChunks := wPipes[1]
   484  	receivedConsoleChunks := wPipes[2]
   485  	outc := merger.Output
   486  
   487  	var reply proxyrpc.RunStartReply
   488  	err := inst.ProxyApp.Call(
   489  		"ProxyVM.RunStart",
   490  		proxyrpc.RunStartParams{
   491  			ID:      inst.ID,
   492  			Command: command},
   493  		&reply)
   494  
   495  	if err != nil {
   496  		return nil, nil, fmt.Errorf("error calling ProxyVM.Run with command %v: %w", command, err)
   497  	}
   498  
   499  	runID := reply.RunID
   500  	terminationError := make(chan error, 1)
   501  	signalClientErrorf := clientErrorf(receivedStderrChunks)
   502  
   503  	go func() {
   504  		for {
   505  			var progress proxyrpc.RunReadProgressReply
   506  			readProgressCall := inst.ProxyApp.Go(
   507  				"ProxyVM.RunReadProgress",
   508  				proxyrpc.RunReadProgressParams{
   509  					ID:    inst.ID,
   510  					RunID: runID,
   511  				},
   512  				&progress,
   513  				nil)
   514  			select {
   515  			case <-readProgressCall.Done:
   516  				receivedStdoutChunks.Write([]byte(progress.StdoutChunk))
   517  				receivedStderrChunks.Write([]byte(progress.StderrChunk))
   518  				receivedConsoleChunks.Write([]byte(progress.ConsoleOutChunk))
   519  				if readProgressCall.Error != nil {
   520  					signalClientErrorf("error reading progress from %v:%v: %v",
   521  						inst.ID, runID, readProgressCall.Error)
   522  				} else if progress.Error != "" {
   523  					signalClientErrorf("%v", progress.Error)
   524  				} else if progress.Finished {
   525  					terminationError <- nil
   526  				} else {
   527  					continue
   528  				}
   529  			case <-ctx.Done():
   530  				// It is the happy path.
   531  				inst.runStop(runID)
   532  				terminationError <- vmimpl.ErrTimeout
   533  			}
   534  			break
   535  		}
   536  	}()
   537  	return outc, terminationError, nil
   538  }
   539  
   540  func (inst *instance) runStop(runID string) {
   541  	err := inst.ProxyApp.Call(
   542  		"ProxyVM.RunStop",
   543  		proxyrpc.RunStopParams{
   544  			ID:    inst.ID,
   545  			RunID: runID,
   546  		},
   547  		&proxyrpc.RunStopParams{})
   548  	if err != nil {
   549  		log.Logf(0, "error calling runStop(%v) on %v: %v", runID, inst.ID, err)
   550  	}
   551  }
   552  
   553  func (inst *instance) Diagnose(r *report.Report) (diagnosis []byte, wait bool) {
   554  	var title string
   555  	if r != nil {
   556  		title = r.Title
   557  	}
   558  	var reply proxyrpc.DiagnoseReply
   559  	err := inst.ProxyApp.Call(
   560  		"ProxyVM.Diagnose",
   561  		proxyrpc.DiagnoseParams{
   562  			ID:          inst.ID,
   563  			ReasonTitle: title,
   564  		},
   565  		&reply)
   566  	if err != nil {
   567  		return nil, false
   568  	}
   569  
   570  	return []byte(reply.Diagnosis), false
   571  }
   572  
   573  func (inst *instance) Close() error {
   574  	var reply proxyrpc.CloseReply
   575  	err := inst.ProxyApp.Call(
   576  		"ProxyVM.Close",
   577  		proxyrpc.CloseParams{
   578  			ID: inst.ID,
   579  		},
   580  		&reply)
   581  	if err != nil {
   582  		log.Logf(0, "error closing instance %v: %v", inst.ID, err)
   583  	}
   584  	return err
   585  }
   586  
   587  type stdInOutCloser struct {
   588  	io.ReadCloser
   589  	io.Writer
   590  }
   591  
   592  func clientErrorf(writer io.Writer) func(fmt string, s ...interface{}) {
   593  	return func(f string, s ...interface{}) {
   594  		fmt.Fprintf(writer, f, s...)
   595  		writer.Write([]byte("\nSYZFAIL: proxy app plugin error\n"))
   596  	}
   597  }