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  }