github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/vm/starnix/starnix.go (about) 1 // Copyright 2023 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 starnix 5 6 import ( 7 "encoding/json" 8 "fmt" 9 "io" 10 "os" 11 "os/exec" 12 "path/filepath" 13 "strings" 14 "time" 15 16 "github.com/google/syzkaller/pkg/config" 17 "github.com/google/syzkaller/pkg/log" 18 "github.com/google/syzkaller/pkg/osutil" 19 "github.com/google/syzkaller/pkg/report" 20 "github.com/google/syzkaller/vm/vmimpl" 21 ) 22 23 func init() { 24 var _ vmimpl.Infoer = (*instance)(nil) 25 vmimpl.Register("starnix", ctor, true) 26 } 27 28 type Config struct { 29 // Number of VMs to run in parallel (1 by default). 30 Count int `json:"count"` 31 } 32 33 type Pool struct { 34 count int 35 env *vmimpl.Env 36 cfg *Config 37 } 38 39 type instance struct { 40 fuchsiaDirectory string 41 ffxBinary string 42 name string 43 index int 44 cfg *Config 45 version string 46 debug bool 47 workdir string 48 port int 49 rpipe io.ReadCloser 50 wpipe io.WriteCloser 51 fuchsiaLogs *exec.Cmd 52 adb *exec.Cmd 53 adbTimeout time.Duration 54 adbRetryWait time.Duration 55 executor string 56 merger *vmimpl.OutputMerger 57 diagnose chan bool 58 } 59 60 const targetDir = "/tmp" 61 62 func ctor(env *vmimpl.Env) (vmimpl.Pool, error) { 63 cfg := &Config{} 64 if err := config.LoadData(env.Config, cfg); err != nil { 65 return nil, fmt.Errorf("failed to parse starnix vm config: %w", err) 66 } 67 if cfg.Count < 1 || cfg.Count > 128 { 68 return nil, fmt.Errorf("invalid config param count: %v, want [1, 128]", cfg.Count) 69 } 70 if _, err := exec.LookPath("adb"); err != nil { 71 return nil, err 72 } 73 74 pool := &Pool{ 75 count: cfg.Count, 76 env: env, 77 cfg: cfg, 78 } 79 return pool, nil 80 } 81 82 func (pool *Pool) Count() int { 83 return pool.count 84 } 85 86 func (pool *Pool) Create(workdir string, index int) (vmimpl.Instance, error) { 87 inst := &instance{ 88 fuchsiaDirectory: pool.env.KernelSrc, 89 name: fmt.Sprintf("VM-%v", index), 90 index: index, 91 cfg: pool.cfg, 92 debug: pool.env.Debug, 93 workdir: workdir, 94 // This file is auto-generated inside createAdbScript. 95 executor: filepath.Join(workdir, "adb_executor.sh"), 96 } 97 closeInst := inst 98 defer func() { 99 if closeInst != nil { 100 closeInst.Close() 101 } 102 }() 103 104 var err error 105 inst.ffxBinary, err = getToolPath(inst.fuchsiaDirectory, "ffx") 106 if err != nil { 107 return nil, err 108 } 109 110 inst.rpipe, inst.wpipe, err = osutil.LongPipe() 111 if err != nil { 112 return nil, err 113 } 114 115 if err := inst.setFuchsiaVersion(); err != nil { 116 return nil, fmt.Errorf( 117 "there is an error running ffx commands in the Fuchsia checkout (%q): %w", 118 inst.fuchsiaDirectory, 119 err) 120 } 121 122 if err := inst.boot(); err != nil { 123 return nil, err 124 } 125 126 closeInst = nil 127 return inst, nil 128 } 129 130 func (inst *instance) boot() error { 131 inst.port = vmimpl.UnusedTCPPort() 132 // Start output merger. 133 inst.merger = vmimpl.NewOutputMerger(nil) 134 135 inst.ffx("doctor", "--restart-daemon") 136 137 inst.ffx("emu", "stop", inst.name) 138 139 if err := inst.startFuchsiaVM(); err != nil { 140 return fmt.Errorf("instance %s: could not start Fuchsia VM: %w", inst.name, err) 141 } 142 if err := inst.startAdbServerAndConnection(2*time.Minute, 3*time.Second); err != nil { 143 return fmt.Errorf("instance %s: could not start and connect to the adb server: %w", inst.name, err) 144 } 145 if inst.debug { 146 log.Logf(0, "instance %s: setting up...", inst.name) 147 } 148 if err := inst.restartAdbAsRoot(); err != nil { 149 return fmt.Errorf("instance %s: could not restart adb with root access: %w", inst.name, err) 150 } 151 152 if err := inst.createAdbScript(); err != nil { 153 return fmt.Errorf("instance %s: could not create adb script: %w", inst.name, err) 154 } 155 156 err := inst.startFuchsiaLogs() 157 if err != nil { 158 return fmt.Errorf("instance %s: could not start fuchsia logs: %w", inst.name, err) 159 } 160 if inst.debug { 161 log.Logf(0, "instance %s: booted successfully", inst.name) 162 } 163 return nil 164 } 165 166 func (inst *instance) Close() { 167 inst.ffx("emu", "stop", inst.name) 168 if inst.fuchsiaLogs != nil { 169 inst.fuchsiaLogs.Process.Kill() 170 inst.fuchsiaLogs.Wait() 171 } 172 if inst.adb != nil { 173 inst.adb.Process.Kill() 174 inst.adb.Wait() 175 } 176 if inst.merger != nil { 177 inst.merger.Wait() 178 } 179 if inst.rpipe != nil { 180 inst.rpipe.Close() 181 } 182 if inst.wpipe != nil { 183 inst.wpipe.Close() 184 } 185 } 186 187 func (inst *instance) startFuchsiaVM() error { 188 err := inst.ffx("emu", "start", "--headless", "--name", inst.name, "--net", "user") 189 if err != nil { 190 return err 191 } 192 return nil 193 } 194 195 func (inst *instance) startFuchsiaLogs() error { 196 // `ffx log` outputs some buffered logs by default, and logs from early boot 197 // trigger a false positive from the unexpected reboot check. To avoid this, 198 // only request logs from now on. 199 cmd := osutil.Command(inst.ffxBinary, "--target", inst.name, "log", "--since", "now", 200 "--show-metadata", "--show-full-moniker", "--no-color") 201 cmd.Dir = inst.fuchsiaDirectory 202 cmd.Stdout = inst.wpipe 203 cmd.Stderr = inst.wpipe 204 inst.merger.Add("fuchsia", inst.rpipe) 205 if err := cmd.Start(); err != nil { 206 return err 207 } 208 inst.fuchsiaLogs = cmd 209 inst.wpipe.Close() 210 inst.wpipe = nil 211 return nil 212 } 213 214 func (inst *instance) startAdbServerAndConnection(timeout, retry time.Duration) error { 215 cmd := osutil.Command(inst.ffxBinary, "--target", inst.name, "starnix", "adb", 216 "-p", fmt.Sprintf("%d", inst.port)) 217 cmd.Dir = inst.fuchsiaDirectory 218 if err := cmd.Start(); err != nil { 219 return err 220 } 221 if inst.debug { 222 log.Logf(0, fmt.Sprintf("instance %s: the adb bridge is listening on 127.0.0.1:%d", inst.name, inst.port)) 223 } 224 inst.adb = cmd 225 inst.adbTimeout = timeout 226 inst.adbRetryWait = retry 227 return inst.connectToAdb() 228 } 229 230 func (inst *instance) connectToAdb() error { 231 startTime := time.Now() 232 for { 233 if inst.debug { 234 log.Logf(1, "instance %s: attempting to connect to adb", inst.name) 235 } 236 connectOutput, err := osutil.RunCmd( 237 2*time.Minute, 238 inst.fuchsiaDirectory, 239 "adb", 240 "connect", 241 fmt.Sprintf("127.0.0.1:%d", inst.port)) 242 if err == nil && strings.HasPrefix(string(connectOutput), "connected to") { 243 if inst.debug { 244 log.Logf(0, "instance %s: connected to adb server", inst.name) 245 } 246 return nil 247 } 248 inst.runCommand("adb", "disconnect", fmt.Sprintf("127.0.0.1:%d", inst.port)) 249 if inst.debug { 250 log.Logf(1, "instance %s: adb connect failed", inst.name) 251 } 252 if time.Since(startTime) > (inst.adbTimeout - inst.adbRetryWait) { 253 return fmt.Errorf("instance %s: can't connect to adb server", inst.name) 254 } 255 vmimpl.SleepInterruptible(inst.adbRetryWait) 256 } 257 } 258 259 func (inst *instance) restartAdbAsRoot() error { 260 startTime := time.Now() 261 for { 262 if inst.debug { 263 log.Logf(1, "instance %s: attempting to restart adbd with root access", inst.name) 264 } 265 err := inst.runCommand( 266 "adb", 267 "-s", 268 fmt.Sprintf("127.0.0.1:%d", inst.port), 269 "root", 270 ) 271 if err == nil { 272 return nil 273 } 274 if inst.debug { 275 log.Logf(1, "instance %s: adb root failed", inst.name) 276 } 277 if time.Since(startTime) > (inst.adbTimeout - inst.adbRetryWait) { 278 return fmt.Errorf("instance %s: can't restart adbd with root access", inst.name) 279 } 280 vmimpl.SleepInterruptible(inst.adbRetryWait) 281 } 282 } 283 284 // Script for telling syz-fuzzer how to connect to syz-executor. 285 func (inst *instance) createAdbScript() error { 286 adbScript := fmt.Sprintf( 287 `#!/usr/bin/env bash 288 adb_port=$1 289 fuzzer_args=${@:2} 290 exec adb -s 127.0.0.1:$adb_port shell "cd %s; ./syz-executor $fuzzer_args"`, targetDir) 291 return os.WriteFile(inst.executor, []byte(adbScript), 0777) 292 } 293 294 func (inst *instance) ffx(args ...string) error { 295 return inst.runCommand(inst.ffxBinary, args...) 296 } 297 298 // Runs a command inside the fuchsia directory. 299 func (inst *instance) runCommand(cmd string, args ...string) error { 300 if inst.debug { 301 log.Logf(1, "instance %s: running command: %s %q", inst.name, cmd, args) 302 } 303 output, err := osutil.RunCmd(5*time.Minute, inst.fuchsiaDirectory, cmd, args...) 304 if inst.debug { 305 log.Logf(1, "instance %s: %s", inst.name, output) 306 } 307 return err 308 } 309 310 func (inst *instance) Forward(port int) (string, error) { 311 if port == 0 { 312 return "", fmt.Errorf("vm/starnix: forward port is zero") 313 } 314 return fmt.Sprintf("localhost:%v", port), nil 315 } 316 317 func (inst *instance) Copy(hostSrc string) (string, error) { 318 startTime := time.Now() 319 base := filepath.Base(hostSrc) 320 if base == "syz-fuzzer" || base == "syz-execprog" { 321 return hostSrc, nil // we will run these on host. 322 } 323 vmDst := filepath.Join(targetDir, base) 324 325 for { 326 if inst.debug { 327 log.Logf(1, "instance %s: attempting to push executor binary over ADB", inst.name) 328 } 329 output, err := osutil.RunCmd( 330 1*time.Minute, 331 inst.fuchsiaDirectory, 332 "adb", 333 "-s", 334 fmt.Sprintf("127.0.0.1:%d", inst.port), 335 "push", 336 hostSrc, 337 vmDst) 338 if err == nil { 339 err = inst.Chmod(vmDst) 340 return vmDst, err 341 } 342 // Retryable connection errors usually look like "adb: error: ... : device offline" 343 // or "adb: error: ... closed" 344 if !strings.HasPrefix(string(output), "adb: error:") { 345 log.Logf(0, "instance %s: adb push failed: %s", inst.name, string(output)) 346 return vmDst, err 347 } 348 if inst.debug { 349 log.Logf(1, "instance %s: adb push failed: %s", inst.name, string(output)) 350 } 351 if time.Since(startTime) > (inst.adbTimeout - inst.adbRetryWait) { 352 return vmDst, fmt.Errorf("instance %s: can't push executor binary to VM", inst.name) 353 } 354 vmimpl.SleepInterruptible(inst.adbRetryWait) 355 } 356 } 357 358 func (inst *instance) Chmod(vmDst string) error { 359 startTime := time.Now() 360 for { 361 if inst.debug { 362 log.Logf(1, "instance %s: attempting to chmod executor script over ADB", inst.name) 363 } 364 output, err := osutil.RunCmd( 365 1*time.Minute, 366 inst.fuchsiaDirectory, 367 "adb", 368 "-s", 369 fmt.Sprintf("127.0.0.1:%d", inst.port), 370 "shell", 371 fmt.Sprintf("chmod +x %s", vmDst)) 372 if err == nil { 373 return nil 374 } 375 // Retryable connection errors usually look like "adb: error: ... : device offline" 376 // or "adb: error: ... closed" 377 if !strings.HasPrefix(string(output), "adb: error:") { 378 log.Logf(0, "instance %s: adb shell command failed: %s", inst.name, string(output)) 379 return err 380 } 381 if inst.debug { 382 log.Logf(1, "instance %s: adb shell command failed: %s", inst.name, string(output)) 383 } 384 if time.Since(startTime) > (inst.adbTimeout - inst.adbRetryWait) { 385 return fmt.Errorf("instance %s: can't chmod executor script for VM", inst.name) 386 } 387 vmimpl.SleepInterruptible(inst.adbRetryWait) 388 } 389 } 390 391 func (inst *instance) Run(timeout time.Duration, stop <-chan bool, command string) ( 392 <-chan []byte, <-chan error, error) { 393 rpipe, wpipe, err := osutil.LongPipe() 394 if err != nil { 395 return nil, nil, err 396 } 397 inst.merger.Add("adb", rpipe) 398 399 args := strings.Split(command, " ") 400 if bin := filepath.Base(args[0]); bin == "syz-fuzzer" || bin == "syz-execprog" { 401 for i, arg := range args { 402 if strings.HasPrefix(arg, "-executor=") { 403 args[i] = fmt.Sprintf("-executor=%s %d", inst.executor, inst.port) 404 // TODO(fxbug.dev/120202): reenable threaded mode once clone3 is fixed. 405 args = append(args, "-threaded=0") 406 } 407 } 408 } 409 if inst.debug { 410 log.Logf(1, "instance %s: running command: %#v", inst.name, args) 411 } 412 cmd := osutil.Command(args[0], args[1:]...) 413 cmd.Dir = inst.workdir 414 cmd.Stdout = wpipe 415 cmd.Stderr = wpipe 416 if err := cmd.Start(); err != nil { 417 wpipe.Close() 418 return nil, nil, err 419 } 420 wpipe.Close() 421 errc := make(chan error, 1) 422 signal := func(err error) { 423 select { 424 case errc <- err: 425 default: 426 } 427 } 428 429 go func() { 430 retry: 431 select { 432 case <-time.After(timeout): 433 signal(vmimpl.ErrTimeout) 434 case <-stop: 435 signal(vmimpl.ErrTimeout) 436 case <-inst.diagnose: 437 cmd.Process.Kill() 438 goto retry 439 case err := <-inst.merger.Err: 440 cmd.Process.Kill() 441 if cmdErr := cmd.Wait(); cmdErr == nil { 442 // If the command exited successfully, we got EOF error from merger. 443 // But in this case no error has happened and the EOF is expected. 444 err = nil 445 } 446 signal(err) 447 return 448 } 449 cmd.Process.Kill() 450 cmd.Wait() 451 }() 452 return inst.merger.Output, errc, nil 453 } 454 455 func (inst *instance) Info() ([]byte, error) { 456 info := fmt.Sprintf("%v\n%v", inst.version, "ffx") 457 return []byte(info), nil 458 } 459 460 func (inst *instance) Diagnose(rep *report.Report) ([]byte, bool) { 461 return nil, false 462 } 463 464 func (inst *instance) setFuchsiaVersion() error { 465 version, err := osutil.RunCmd(1*time.Minute, inst.fuchsiaDirectory, inst.ffxBinary, "version") 466 if err != nil { 467 return err 468 } 469 inst.version = string(version) 470 return nil 471 } 472 473 // Get the currently-selected build dir in a Fuchsia checkout. 474 func getFuchsiaBuildDir(fuchsiaDir string) (string, error) { 475 fxBuildDir := filepath.Join(fuchsiaDir, ".fx-build-dir") 476 contents, err := os.ReadFile(fxBuildDir) 477 if err != nil { 478 return "", fmt.Errorf("failed to read %q: %w", fxBuildDir, err) 479 } 480 481 buildDir := strings.TrimSpace(string(contents)) 482 if !filepath.IsAbs(buildDir) { 483 buildDir = filepath.Join(fuchsiaDir, buildDir) 484 } 485 486 return buildDir, nil 487 } 488 489 // Subset of data format used in tool_paths.json. 490 type toolMetadata struct { 491 Name string 492 Path string 493 } 494 495 // Resolve a tool by name using tool_paths.json in the build dir. 496 func getToolPath(fuchsiaDir, toolName string) (string, error) { 497 buildDir, err := getFuchsiaBuildDir(fuchsiaDir) 498 if err != nil { 499 return "", err 500 } 501 502 jsonPath := filepath.Join(buildDir, "tool_paths.json") 503 jsonBlob, err := os.ReadFile(jsonPath) 504 if err != nil { 505 return "", fmt.Errorf("failed to read %q: %w", jsonPath, err) 506 } 507 var metadataList []toolMetadata 508 if err := json.Unmarshal(jsonBlob, &metadataList); err != nil { 509 return "", fmt.Errorf("failed to parse %q: %w", jsonPath, err) 510 } 511 512 for _, metadata := range metadataList { 513 if metadata.Name == toolName { 514 return filepath.Join(buildDir, metadata.Path), nil 515 } 516 } 517 518 return "", fmt.Errorf("no path found for tool %q in %q", toolName, jsonPath) 519 }