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 }