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

     1  // Copyright 2015 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 qemu
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/json"
     9  	"fmt"
    10  	"io"
    11  	"net"
    12  	"os"
    13  	"os/exec"
    14  	"path/filepath"
    15  	"regexp"
    16  	"strconv"
    17  	"strings"
    18  	"time"
    19  
    20  	"github.com/google/syzkaller/pkg/config"
    21  	"github.com/google/syzkaller/pkg/log"
    22  	"github.com/google/syzkaller/pkg/osutil"
    23  	"github.com/google/syzkaller/pkg/report"
    24  	"github.com/google/syzkaller/sys/targets"
    25  	"github.com/google/syzkaller/vm/vmimpl"
    26  )
    27  
    28  func init() {
    29  	var _ vmimpl.Infoer = (*instance)(nil)
    30  	vmimpl.Register("qemu", ctor, true)
    31  }
    32  
    33  type Config struct {
    34  	// Number of VMs to run in parallel (1 by default).
    35  	Count int `json:"count"`
    36  	// QEMU binary name (optional).
    37  	// If not specified, qemu-system-arch is used by default.
    38  	Qemu string `json:"qemu"`
    39  	// Additional command line arguments for the QEMU binary.
    40  	// If not specified, the default value specifies machine type, cpu and usually contains -enable-kvm.
    41  	// If you provide this parameter, it needs to contain the desired machine, cpu
    42  	// and include -enable-kvm if necessary.
    43  	// "{{INDEX}}" is replaced with 0-based index of the VM (from 0 to Count-1).
    44  	// "{{TEMPLATE}}" is replaced with the path to a copy of workdir/template dir.
    45  	// "{{TCP_PORT}}" is replaced with a random free TCP port
    46  	// "{{FN%8}}" is replaced with PCI BDF (Function#%8) and PCI BDF Dev# += index/8
    47  	QemuArgs string `json:"qemu_args"`
    48  	// Location of the kernel for injected boot (e.g. arch/x86/boot/bzImage, optional).
    49  	// This is passed to QEMU as the -kernel option.
    50  	Kernel string `json:"kernel"`
    51  	// Additional command line options for the booting kernel, for example `root=/dev/sda1`.
    52  	// Can only be specified with kernel.
    53  	Cmdline string `json:"cmdline"`
    54  	// Initial ramdisk, passed via -initrd QEMU flag (optional).
    55  	Initrd string `json:"initrd"`
    56  	// QEMU image device.
    57  	// The default value "hda" is transformed to "-hda image" for QEMU.
    58  	// The modern way of describing QEMU hard disks is supported, so the value
    59  	// "drive index=0,media=disk,file=" is transformed to "-drive index=0,media=disk,file=image" for QEMU.
    60  	ImageDevice string `json:"image_device"`
    61  	// EFI images containing the EFI itself, as well as this VMs EFI variables.
    62  	EfiCodeDevice string `json:"efi_code_device"`
    63  	EfiVarsDevice string `json:"efi_vars_device"`
    64  	// QEMU network device type to use.
    65  	// If not specified, some default per-arch value will be used.
    66  	// See the full list with qemu-system-x86_64 -device help.
    67  	NetDev string `json:"network_device"`
    68  	// Number of VM CPUs (1 by default).
    69  	CPU int `json:"cpu"`
    70  	// Amount of VM memory in MiB (1024 by default).
    71  	Mem int `json:"mem"`
    72  	// For building kernels without -snapshot for pkg/build (true by default).
    73  	Snapshot bool `json:"snapshot"`
    74  	// Magic key used to dongle macOS to the device.
    75  	AppleSmcOsk string `json:"apple_smc_osk"`
    76  }
    77  
    78  type Pool struct {
    79  	env        *vmimpl.Env
    80  	cfg        *Config
    81  	target     *targets.Target
    82  	archConfig *archConfig
    83  	version    string
    84  }
    85  
    86  type instance struct {
    87  	index       int
    88  	cfg         *Config
    89  	target      *targets.Target
    90  	archConfig  *archConfig
    91  	version     string
    92  	args        []string
    93  	image       string
    94  	debug       bool
    95  	os          string
    96  	workdir     string
    97  	sshkey      string
    98  	sshuser     string
    99  	timeouts    targets.Timeouts
   100  	port        int
   101  	monport     int
   102  	forwardPort int
   103  	mon         net.Conn
   104  	monEnc      *json.Encoder
   105  	monDec      *json.Decoder
   106  	rpipe       io.ReadCloser
   107  	wpipe       io.WriteCloser
   108  	qemu        *exec.Cmd
   109  	merger      *vmimpl.OutputMerger
   110  	files       map[string]string
   111  	diagnose    chan bool
   112  }
   113  
   114  type archConfig struct {
   115  	Qemu      string
   116  	QemuArgs  string
   117  	TargetDir string // "/" by default
   118  	NetDev    string // default network device type (see the full list with qemu-system-x86_64 -device help)
   119  	RngDev    string // default rng device (optional)
   120  	// UseNewQemuImageOptions specifies whether the arch uses "new" QEMU image device options.
   121  	UseNewQemuImageOptions bool
   122  	CmdLine                []string
   123  }
   124  
   125  var archConfigs = map[string]*archConfig{
   126  	"linux/amd64": {
   127  		Qemu:     "qemu-system-x86_64",
   128  		QemuArgs: "-enable-kvm -cpu host,migratable=off",
   129  		// e1000e fails on recent Debian distros with:
   130  		// Initialization of device e1000e failed: failed to find romfile "efi-e1000e.rom
   131  		// But other arches don't use e1000e, e.g. arm64 uses virtio by default.
   132  		NetDev: "e1000",
   133  		RngDev: "virtio-rng-pci",
   134  		CmdLine: []string{
   135  			"root=/dev/sda",
   136  			"console=ttyS0",
   137  		},
   138  	},
   139  	"linux/386": {
   140  		Qemu:   "qemu-system-i386",
   141  		NetDev: "e1000",
   142  		RngDev: "virtio-rng-pci",
   143  		CmdLine: []string{
   144  			"root=/dev/sda",
   145  			"console=ttyS0",
   146  		},
   147  	},
   148  	"linux/arm64": {
   149  		Qemu:     "qemu-system-aarch64",
   150  		QemuArgs: "-machine virt,virtualization=on -cpu cortex-a57 -accel tcg,thread=multi",
   151  		NetDev:   "virtio-net-pci",
   152  		RngDev:   "virtio-rng-pci",
   153  		CmdLine: []string{
   154  			"root=/dev/vda",
   155  			"console=ttyAMA0",
   156  		},
   157  	},
   158  	"linux/arm": {
   159  		Qemu:                   "qemu-system-arm",
   160  		QemuArgs:               "-machine vexpress-a15 -cpu max -accel tcg,thread=multi",
   161  		NetDev:                 "virtio-net-device",
   162  		RngDev:                 "virtio-rng-device",
   163  		UseNewQemuImageOptions: true,
   164  		CmdLine: []string{
   165  			"root=/dev/vda",
   166  			"console=ttyAMA0",
   167  		},
   168  	},
   169  	"linux/mips64le": {
   170  		Qemu:     "qemu-system-mips64el",
   171  		QemuArgs: "-M malta -cpu MIPS64R2-generic -nodefaults",
   172  		NetDev:   "e1000",
   173  		RngDev:   "virtio-rng-pci",
   174  		CmdLine: []string{
   175  			"root=/dev/sda",
   176  			"console=ttyS0",
   177  		},
   178  	},
   179  	"linux/ppc64le": {
   180  		Qemu:     "qemu-system-ppc64",
   181  		QemuArgs: "-enable-kvm -vga none",
   182  		NetDev:   "virtio-net-pci",
   183  		RngDev:   "virtio-rng-pci",
   184  	},
   185  	"linux/riscv64": {
   186  		Qemu:                   "qemu-system-riscv64",
   187  		QemuArgs:               "-machine virt",
   188  		NetDev:                 "virtio-net-pci",
   189  		RngDev:                 "virtio-rng-pci",
   190  		UseNewQemuImageOptions: true,
   191  		CmdLine: []string{
   192  			"root=/dev/vda",
   193  			"console=ttyS0",
   194  		},
   195  	},
   196  	"linux/s390x": {
   197  		Qemu:     "qemu-system-s390x",
   198  		QemuArgs: "-M s390-ccw-virtio -cpu max,zpci=on",
   199  		NetDev:   "virtio-net-pci",
   200  		RngDev:   "virtio-rng-ccw",
   201  		CmdLine: []string{
   202  			"root=/dev/vda",
   203  			// The following kernel parameters is a temporary
   204  			// work-around for not having CONFIG_CMDLINE on s390x.
   205  			"net.ifnames=0",
   206  			"biosdevname=0",
   207  		},
   208  	},
   209  	"freebsd/amd64": {
   210  		Qemu:     "qemu-system-x86_64",
   211  		QemuArgs: "-enable-kvm",
   212  		NetDev:   "e1000",
   213  		RngDev:   "virtio-rng-pci",
   214  	},
   215  	"freebsd/riscv64": {
   216  		Qemu:                   "qemu-system-riscv64",
   217  		QemuArgs:               "-machine virt",
   218  		NetDev:                 "virtio-net-pci",
   219  		RngDev:                 "virtio-rng-pci",
   220  		UseNewQemuImageOptions: true,
   221  	},
   222  	"darwin/amd64": {
   223  		Qemu: "qemu-system-x86_64",
   224  		QemuArgs: strings.Join([]string{
   225  			"-accel hvf -machine q35 ",
   226  			"-cpu Penryn,vendor=GenuineIntel,+invtsc,vmware-cpuid-freq=on,",
   227  			"+pcid,+ssse3,+sse4.2,+popcnt,+avx,+aes,+xsave,+xsaveopt,check ",
   228  		}, ""),
   229  		TargetDir: "/tmp",
   230  		NetDev:    "e1000-82545em",
   231  		RngDev:    "virtio-rng-pci",
   232  	},
   233  	"netbsd/amd64": {
   234  		Qemu:     "qemu-system-x86_64",
   235  		QemuArgs: "-enable-kvm",
   236  		NetDev:   "e1000",
   237  		RngDev:   "virtio-rng-pci",
   238  	},
   239  	"fuchsia/amd64": {
   240  		Qemu:      "qemu-system-x86_64",
   241  		QemuArgs:  "-enable-kvm -machine q35 -cpu host,migratable=off",
   242  		TargetDir: "/tmp",
   243  		NetDev:    "e1000",
   244  		RngDev:    "virtio-rng-pci",
   245  		CmdLine: []string{
   246  			"kernel.serial=legacy",
   247  			"kernel.halt-on-panic=true",
   248  			// Set long (300sec) thresholds for kernel lockup detector to
   249  			// prevent false alarms from potentially oversubscribed hosts.
   250  			// (For more context, see fxbug.dev/109612.)
   251  			"kernel.lockup-detector.critical-section-threshold-ms=300000",
   252  			"kernel.lockup-detector.critical-section-fatal-threshold-ms=300000",
   253  			"kernel.lockup-detector.heartbeat-age-threshold-ms=300000",
   254  			"kernel.lockup-detector.heartbeat-age-fatal-threshold-ms=300000",
   255  		},
   256  	},
   257  }
   258  
   259  func ctor(env *vmimpl.Env) (vmimpl.Pool, error) {
   260  	archConfig := archConfigs[env.OS+"/"+env.Arch]
   261  	cfg := &Config{
   262  		Count:       1,
   263  		CPU:         1,
   264  		Mem:         1024,
   265  		ImageDevice: "hda",
   266  		Qemu:        archConfig.Qemu,
   267  		QemuArgs:    archConfig.QemuArgs,
   268  		NetDev:      archConfig.NetDev,
   269  		Snapshot:    true,
   270  	}
   271  	if err := config.LoadData(env.Config, cfg); err != nil {
   272  		return nil, fmt.Errorf("failed to parse qemu vm config: %w", err)
   273  	}
   274  	if cfg.Count < 1 || cfg.Count > 128 {
   275  		return nil, fmt.Errorf("invalid config param count: %v, want [1, 128]", cfg.Count)
   276  	}
   277  	if env.Debug && cfg.Count > 1 {
   278  		log.Logf(0, "limiting number of VMs from %v to 1 in debug mode", cfg.Count)
   279  		cfg.Count = 1
   280  	}
   281  	if _, err := exec.LookPath(cfg.Qemu); err != nil {
   282  		return nil, err
   283  	}
   284  	if env.Image == "9p" {
   285  		if env.OS != targets.Linux {
   286  			return nil, fmt.Errorf("9p image is supported for linux only")
   287  		}
   288  		if cfg.Kernel == "" {
   289  			return nil, fmt.Errorf("9p image requires kernel")
   290  		}
   291  	} else {
   292  		if !osutil.IsExist(env.Image) {
   293  			return nil, fmt.Errorf("image file '%v' does not exist", env.Image)
   294  		}
   295  	}
   296  	if cfg.CPU <= 0 || cfg.CPU > 1024 {
   297  		return nil, fmt.Errorf("bad qemu cpu: %v, want [1-1024]", cfg.CPU)
   298  	}
   299  	if cfg.Mem < 128 || cfg.Mem > 1048576 {
   300  		return nil, fmt.Errorf("bad qemu mem: %v, want [128-1048576]", cfg.Mem)
   301  	}
   302  	cfg.Kernel = osutil.Abs(cfg.Kernel)
   303  	cfg.Initrd = osutil.Abs(cfg.Initrd)
   304  
   305  	output, err := osutil.RunCmd(time.Minute, "", cfg.Qemu, "--version")
   306  	if err != nil {
   307  		return nil, err
   308  	}
   309  	version := string(bytes.Split(output, []byte{'\n'})[0])
   310  
   311  	pool := &Pool{
   312  		env:        env,
   313  		cfg:        cfg,
   314  		version:    version,
   315  		target:     targets.Get(env.OS, env.Arch),
   316  		archConfig: archConfig,
   317  	}
   318  	return pool, nil
   319  }
   320  
   321  func (pool *Pool) Count() int {
   322  	return pool.cfg.Count
   323  }
   324  
   325  func (pool *Pool) Create(workdir string, index int) (vmimpl.Instance, error) {
   326  	sshkey := pool.env.SSHKey
   327  	sshuser := pool.env.SSHUser
   328  	if pool.env.Image == "9p" {
   329  		sshkey = filepath.Join(workdir, "key")
   330  		sshuser = "root"
   331  		if _, err := osutil.RunCmd(10*time.Minute, "", "ssh-keygen", "-t", "rsa", "-b", "2048",
   332  			"-N", "", "-C", "", "-f", sshkey); err != nil {
   333  			return nil, err
   334  		}
   335  		initFile := filepath.Join(workdir, "init.sh")
   336  		if err := osutil.WriteExecFile(initFile, []byte(strings.Replace(initScript, "{{KEY}}", sshkey, -1))); err != nil {
   337  			return nil, fmt.Errorf("failed to create init file: %w", err)
   338  		}
   339  	}
   340  
   341  	for i := 0; ; i++ {
   342  		inst, err := pool.ctor(workdir, sshkey, sshuser, index)
   343  		if err == nil {
   344  			return inst, nil
   345  		}
   346  		// Older qemu prints "could", newer -- "Could".
   347  		if i < 1000 && strings.Contains(err.Error(), "ould not set up host forwarding rule") {
   348  			continue
   349  		}
   350  		if i < 1000 && strings.Contains(err.Error(), "Device or resource busy") {
   351  			continue
   352  		}
   353  		return nil, err
   354  	}
   355  }
   356  
   357  func (pool *Pool) ctor(workdir, sshkey, sshuser string, index int) (vmimpl.Instance, error) {
   358  	inst := &instance{
   359  		index:      index,
   360  		cfg:        pool.cfg,
   361  		target:     pool.target,
   362  		archConfig: pool.archConfig,
   363  		version:    pool.version,
   364  		image:      pool.env.Image,
   365  		debug:      pool.env.Debug,
   366  		os:         pool.env.OS,
   367  		timeouts:   pool.env.Timeouts,
   368  		workdir:    workdir,
   369  		sshkey:     sshkey,
   370  		sshuser:    sshuser,
   371  		diagnose:   make(chan bool, 1),
   372  	}
   373  	if st, err := os.Stat(inst.image); err == nil && st.Size() == 0 {
   374  		// Some kernels may not need an image, however caller may still
   375  		// want to pass us a fake empty image because the rest of syzkaller
   376  		// assumes that an image is mandatory. So if the image is empty, we ignore it.
   377  		inst.image = ""
   378  	}
   379  	closeInst := inst
   380  	defer func() {
   381  		if closeInst != nil {
   382  			closeInst.Close()
   383  		}
   384  	}()
   385  
   386  	var err error
   387  	inst.rpipe, inst.wpipe, err = osutil.LongPipe()
   388  	if err != nil {
   389  		return nil, err
   390  	}
   391  
   392  	if err := inst.boot(); err != nil {
   393  		return nil, err
   394  	}
   395  
   396  	closeInst = nil
   397  	return inst, nil
   398  }
   399  
   400  func (inst *instance) Close() {
   401  	if inst.qemu != nil {
   402  		inst.qemu.Process.Kill()
   403  		inst.qemu.Wait()
   404  	}
   405  	if inst.merger != nil {
   406  		inst.merger.Wait()
   407  	}
   408  	if inst.rpipe != nil {
   409  		inst.rpipe.Close()
   410  	}
   411  	if inst.wpipe != nil {
   412  		inst.wpipe.Close()
   413  	}
   414  	if inst.mon != nil {
   415  		inst.mon.Close()
   416  	}
   417  }
   418  
   419  func (inst *instance) boot() error {
   420  	inst.port = vmimpl.UnusedTCPPort()
   421  	inst.monport = vmimpl.UnusedTCPPort()
   422  	instanceName := fmt.Sprintf("VM-%v", inst.index)
   423  	args := []string{
   424  		"-m", strconv.Itoa(inst.cfg.Mem),
   425  		"-smp", strconv.Itoa(inst.cfg.CPU),
   426  		"-chardev", fmt.Sprintf("socket,id=SOCKSYZ,server=on,wait=off,host=localhost,port=%v", inst.monport),
   427  		"-mon", "chardev=SOCKSYZ,mode=control",
   428  		"-display", "none",
   429  		"-serial", "stdio",
   430  		"-no-reboot",
   431  		"-name", instanceName,
   432  	}
   433  	if inst.archConfig.RngDev != "" {
   434  		args = append(args, "-device", inst.archConfig.RngDev)
   435  	}
   436  	templateDir := filepath.Join(inst.workdir, "template")
   437  	args = append(args, splitArgs(inst.cfg.QemuArgs, templateDir, inst.index)...)
   438  
   439  	forwardedPort := vmimpl.UnusedTCPPort()
   440  	pprofExt := fmt.Sprintf(",hostfwd=tcp::%v-:%v", forwardedPort, vmimpl.PprofPort)
   441  	log.Logf(3, "instance %s's pprof is available at 127.0.0.1:%v", instanceName, forwardedPort)
   442  
   443  	args = append(args,
   444  		"-device", inst.cfg.NetDev+",netdev=net0",
   445  		"-netdev", fmt.Sprintf("user,id=net0,restrict=on,hostfwd=tcp:127.0.0.1:%v-:22%s", inst.port, pprofExt),
   446  	)
   447  	if inst.image == "9p" {
   448  		args = append(args,
   449  			"-fsdev", "local,id=fsdev0,path=/,security_model=none,readonly",
   450  			"-device", "virtio-9p-pci,fsdev=fsdev0,mount_tag=/dev/root",
   451  		)
   452  	} else if inst.image != "" {
   453  		if inst.archConfig.UseNewQemuImageOptions {
   454  			args = append(args,
   455  				"-device", "virtio-blk-device,drive=hd0",
   456  				"-drive", fmt.Sprintf("file=%v,if=none,format=raw,id=hd0", inst.image),
   457  			)
   458  		} else {
   459  			// inst.cfg.ImageDevice can contain spaces
   460  			imgline := strings.Split(inst.cfg.ImageDevice, " ")
   461  			imgline[0] = "-" + imgline[0]
   462  			if strings.HasSuffix(imgline[len(imgline)-1], "file=") {
   463  				imgline[len(imgline)-1] = imgline[len(imgline)-1] + inst.image
   464  			} else {
   465  				imgline = append(imgline, inst.image)
   466  			}
   467  			args = append(args, imgline...)
   468  		}
   469  		if inst.cfg.Snapshot {
   470  			args = append(args, "-snapshot")
   471  		}
   472  	}
   473  	if inst.cfg.Initrd != "" {
   474  		args = append(args,
   475  			"-initrd", inst.cfg.Initrd,
   476  		)
   477  	}
   478  	if inst.cfg.Kernel != "" {
   479  		cmdline := append([]string{}, inst.archConfig.CmdLine...)
   480  		if inst.image == "9p" {
   481  			cmdline = append(cmdline,
   482  				"root=/dev/root",
   483  				"rootfstype=9p",
   484  				"rootflags=trans=virtio,version=9p2000.L,cache=loose",
   485  				"init="+filepath.Join(inst.workdir, "init.sh"),
   486  			)
   487  		}
   488  		cmdline = append(cmdline, inst.cfg.Cmdline)
   489  		args = append(args,
   490  			"-kernel", inst.cfg.Kernel,
   491  			"-append", strings.Join(cmdline, " "),
   492  		)
   493  	}
   494  	if inst.cfg.EfiCodeDevice != "" {
   495  		args = append(args,
   496  			"-drive", "if=pflash,format=raw,readonly=on,file="+inst.cfg.EfiCodeDevice,
   497  		)
   498  	}
   499  	if inst.cfg.EfiVarsDevice != "" {
   500  		args = append(args,
   501  			"-drive", "if=pflash,format=raw,readonly=on,file="+inst.cfg.EfiVarsDevice,
   502  		)
   503  	}
   504  	if inst.cfg.AppleSmcOsk != "" {
   505  		args = append(args,
   506  			"-device", "isa-applesmc,osk="+inst.cfg.AppleSmcOsk,
   507  		)
   508  	}
   509  	if inst.debug {
   510  		log.Logf(0, "running command: %v %#v", inst.cfg.Qemu, args)
   511  	}
   512  	inst.args = args
   513  	qemu := osutil.Command(inst.cfg.Qemu, args...)
   514  	qemu.Stdout = inst.wpipe
   515  	qemu.Stderr = inst.wpipe
   516  	if err := qemu.Start(); err != nil {
   517  		return fmt.Errorf("failed to start %v %+v: %w", inst.cfg.Qemu, args, err)
   518  	}
   519  	inst.wpipe.Close()
   520  	inst.wpipe = nil
   521  	inst.qemu = qemu
   522  	// Qemu has started.
   523  
   524  	// Start output merger.
   525  	var tee io.Writer
   526  	if inst.debug {
   527  		tee = os.Stdout
   528  	}
   529  	inst.merger = vmimpl.NewOutputMerger(tee)
   530  	inst.merger.Add("qemu", inst.rpipe)
   531  	inst.rpipe = nil
   532  
   533  	var bootOutput []byte
   534  	bootOutputStop := make(chan bool)
   535  	go func() {
   536  		for {
   537  			select {
   538  			case out := <-inst.merger.Output:
   539  				bootOutput = append(bootOutput, out...)
   540  			case <-bootOutputStop:
   541  				close(bootOutputStop)
   542  				return
   543  			}
   544  		}
   545  	}()
   546  	if err := vmimpl.WaitForSSH(inst.debug, 10*time.Minute*inst.timeouts.Scale, "localhost",
   547  		inst.sshkey, inst.sshuser, inst.os, inst.port, inst.merger.Err, false); err != nil {
   548  		bootOutputStop <- true
   549  		<-bootOutputStop
   550  		return vmimpl.MakeBootError(err, bootOutput)
   551  	}
   552  	bootOutputStop <- true
   553  	return nil
   554  }
   555  
   556  // "vfio-pci,host=BN:DN.{{FN%8}},addr=0x11".
   557  func handleVfioPciArg(arg string, index int) string {
   558  	if !strings.Contains(arg, "{{FN%8}}") {
   559  		return arg
   560  	}
   561  	if index > 7 {
   562  		re := regexp.MustCompile(`vfio-pci,host=[a-bA-B0-9]+(:[a-bA-B0-9]{1,8}).{{FN%8}},[^:.,]+$`)
   563  		matches := re.FindAllStringSubmatch(arg, -1)
   564  		if len(matches[0]) != 2 {
   565  			return arg
   566  		}
   567  		submatch := matches[0][1]
   568  		dnSubmatch, _ := strconv.ParseInt(submatch[1:], 16, 64)
   569  		devno := dnSubmatch + int64(index/8)
   570  		arg = strings.ReplaceAll(arg, submatch, fmt.Sprintf(":%02x", devno))
   571  	}
   572  	arg = strings.ReplaceAll(arg, "{{FN%8}}", fmt.Sprint(index%8))
   573  	return arg
   574  }
   575  
   576  func splitArgs(str, templateDir string, index int) (args []string) {
   577  	for _, arg := range strings.Split(str, " ") {
   578  		if arg == "" {
   579  			continue
   580  		}
   581  		arg = strings.ReplaceAll(arg, "{{INDEX}}", fmt.Sprint(index))
   582  		arg = strings.ReplaceAll(arg, "{{TEMPLATE}}", templateDir)
   583  		arg = handleVfioPciArg(arg, index)
   584  		const tcpPort = "{{TCP_PORT}}"
   585  		if strings.Contains(arg, tcpPort) {
   586  			arg = strings.ReplaceAll(arg, tcpPort, fmt.Sprint(vmimpl.UnusedTCPPort()))
   587  		}
   588  		args = append(args, arg)
   589  	}
   590  	return
   591  }
   592  
   593  func (inst *instance) Forward(port int) (string, error) {
   594  	if port == 0 {
   595  		return "", fmt.Errorf("vm/qemu: forward port is zero")
   596  	}
   597  	if !inst.target.HostFuzzer {
   598  		if inst.forwardPort != 0 {
   599  			return "", fmt.Errorf("vm/qemu: forward port already set")
   600  		}
   601  		inst.forwardPort = port
   602  	}
   603  	return fmt.Sprintf("localhost:%v", port), nil
   604  }
   605  
   606  func (inst *instance) targetDir() string {
   607  	if inst.image == "9p" {
   608  		return "/tmp"
   609  	}
   610  	if inst.archConfig.TargetDir == "" {
   611  		return "/"
   612  	}
   613  	return inst.archConfig.TargetDir
   614  }
   615  
   616  func (inst *instance) Copy(hostSrc string) (string, error) {
   617  	base := filepath.Base(hostSrc)
   618  	vmDst := filepath.Join(inst.targetDir(), base)
   619  	if inst.target.HostFuzzer {
   620  		if base == "syz-fuzzer" || base == "syz-execprog" {
   621  			return hostSrc, nil // we will run these on host
   622  		}
   623  		if inst.files == nil {
   624  			inst.files = make(map[string]string)
   625  		}
   626  		inst.files[vmDst] = hostSrc
   627  	}
   628  
   629  	args := append(vmimpl.SCPArgs(inst.debug, inst.sshkey, inst.port, false),
   630  		hostSrc, inst.sshuser+"@localhost:"+vmDst)
   631  	if inst.debug {
   632  		log.Logf(0, "running command: scp %#v", args)
   633  	}
   634  	_, err := osutil.RunCmd(10*time.Minute*inst.timeouts.Scale, "", "scp", args...)
   635  	if err != nil {
   636  		return "", err
   637  	}
   638  	return vmDst, nil
   639  }
   640  
   641  func (inst *instance) Run(timeout time.Duration, stop <-chan bool, command string) (
   642  	<-chan []byte, <-chan error, error) {
   643  	rpipe, wpipe, err := osutil.LongPipe()
   644  	if err != nil {
   645  		return nil, nil, err
   646  	}
   647  	inst.merger.Add("ssh", rpipe)
   648  
   649  	sshArgs := vmimpl.SSHArgsForward(inst.debug, inst.sshkey, inst.port, inst.forwardPort, false)
   650  	args := strings.Split(command, " ")
   651  	if bin := filepath.Base(args[0]); inst.target.HostFuzzer &&
   652  		(bin == "syz-fuzzer" || bin == "syz-execprog") {
   653  		// Weird mode for Fuchsia.
   654  		// Fuzzer and execprog are on host (we did not copy them), so we will run them as is,
   655  		// but we will also wrap executor with ssh invocation.
   656  		for i, arg := range args {
   657  			if strings.HasPrefix(arg, "-executor=") {
   658  				args[i] = "-executor=" + "/usr/bin/ssh " + strings.Join(sshArgs, " ") +
   659  					" " + inst.sshuser + "@localhost " + arg[len("-executor="):]
   660  			}
   661  			if host := inst.files[arg]; host != "" {
   662  				args[i] = host
   663  			}
   664  		}
   665  	} else {
   666  		args = []string{"ssh"}
   667  		args = append(args, sshArgs...)
   668  		args = append(args, inst.sshuser+"@localhost", "cd "+inst.targetDir()+" && "+command)
   669  	}
   670  	if inst.debug {
   671  		log.Logf(0, "running command: %#v", args)
   672  	}
   673  	cmd := osutil.Command(args[0], args[1:]...)
   674  	cmd.Dir = inst.workdir
   675  	cmd.Stdout = wpipe
   676  	cmd.Stderr = wpipe
   677  	if err := cmd.Start(); err != nil {
   678  		wpipe.Close()
   679  		return nil, nil, err
   680  	}
   681  	wpipe.Close()
   682  	errc := make(chan error, 1)
   683  	signal := func(err error) {
   684  		select {
   685  		case errc <- err:
   686  		default:
   687  		}
   688  	}
   689  
   690  	go func() {
   691  	retry:
   692  		select {
   693  		case <-time.After(timeout):
   694  			signal(vmimpl.ErrTimeout)
   695  		case <-stop:
   696  			signal(vmimpl.ErrTimeout)
   697  		case <-inst.diagnose:
   698  			cmd.Process.Kill()
   699  			goto retry
   700  		case err := <-inst.merger.Err:
   701  			cmd.Process.Kill()
   702  			if cmdErr := cmd.Wait(); cmdErr == nil {
   703  				// If the command exited successfully, we got EOF error from merger.
   704  				// But in this case no error has happened and the EOF is expected.
   705  				err = nil
   706  			}
   707  			signal(err)
   708  			return
   709  		}
   710  		cmd.Process.Kill()
   711  		cmd.Wait()
   712  	}()
   713  	return inst.merger.Output, errc, nil
   714  }
   715  
   716  func (inst *instance) Info() ([]byte, error) {
   717  	info := fmt.Sprintf("%v\n%v %q\n", inst.version, inst.cfg.Qemu, inst.args)
   718  	return []byte(info), nil
   719  }
   720  
   721  func (inst *instance) Diagnose(rep *report.Report) ([]byte, bool) {
   722  	if inst.target.OS == targets.Linux {
   723  		if output, wait, handled := vmimpl.DiagnoseLinux(rep, inst.ssh); handled {
   724  			return output, wait
   725  		}
   726  	}
   727  	// TODO: we don't need registers on all reports. Probably only relevant for "crashes"
   728  	// (NULL derefs, paging faults, etc), but is not useful for WARNING/BUG/HANG (?).
   729  	ret := []byte(fmt.Sprintf("%s Registers:\n", time.Now().Format("15:04:05 ")))
   730  	for cpu := 0; cpu < inst.cfg.CPU; cpu++ {
   731  		regs, err := inst.hmp("info registers", cpu)
   732  		if err == nil {
   733  			ret = append(ret, []byte(fmt.Sprintf("info registers vcpu %v\n", cpu))...)
   734  			ret = append(ret, []byte(regs)...)
   735  		} else {
   736  			log.Logf(0, "VM-%v failed reading regs: %v", inst.index, err)
   737  			ret = append(ret, []byte(fmt.Sprintf("Failed reading regs: %v\n", err))...)
   738  		}
   739  	}
   740  	return ret, false
   741  }
   742  
   743  func (inst *instance) ssh(args ...string) ([]byte, error) {
   744  	return osutil.RunCmd(time.Minute*inst.timeouts.Scale, "", "ssh", inst.sshArgs(args...)...)
   745  }
   746  
   747  func (inst *instance) sshArgs(args ...string) []string {
   748  	sshArgs := append(vmimpl.SSHArgs(inst.debug, inst.sshkey, inst.port, false), inst.sshuser+"@localhost")
   749  	return append(sshArgs, args...)
   750  }
   751  
   752  // nolint: lll
   753  const initScript = `#! /bin/bash
   754  set -eux
   755  mount -t proc none /proc
   756  mount -t sysfs none /sys
   757  mount -t debugfs nodev /sys/kernel/debug/
   758  mount -t tmpfs none /tmp
   759  mount -t tmpfs none /var
   760  mount -t tmpfs none /run
   761  mount -t tmpfs none /etc
   762  mount -t tmpfs none /root
   763  touch /etc/fstab
   764  mkdir /etc/network
   765  mkdir /run/network
   766  printf 'auto lo\niface lo inet loopback\n\n' >> /etc/network/interfaces
   767  printf 'auto eth0\niface eth0 inet static\naddress 10.0.2.15\nnetmask 255.255.255.0\nnetwork 10.0.2.0\ngateway 10.0.2.1\nbroadcast 10.0.2.255\n\n' >> /etc/network/interfaces
   768  printf 'auto eth0\niface eth0 inet6 static\naddress fe80::5054:ff:fe12:3456/64\ngateway 2000:da8:203:612:0:3:0:1\n\n' >> /etc/network/interfaces
   769  mkdir -p /etc/network/if-pre-up.d
   770  mkdir -p /etc/network/if-up.d
   771  ifup lo
   772  ifup eth0 || true
   773  echo "root::0:0:root:/root:/bin/bash" > /etc/passwd
   774  mkdir -p /etc/ssh
   775  cp {{KEY}}.pub /root/
   776  chmod 0700 /root
   777  chmod 0600 /root/key.pub
   778  mkdir -p /var/run/sshd/
   779  chmod 700 /var/run/sshd
   780  groupadd -g 33 sshd
   781  useradd -u 33 -g 33 -c sshd -d / sshd
   782  cat > /etc/ssh/sshd_config <<EOF
   783            Port 22
   784            Protocol 2
   785            UsePrivilegeSeparation no
   786            HostKey {{KEY}}
   787            PermitRootLogin yes
   788            AuthenticationMethods publickey
   789            ChallengeResponseAuthentication no
   790            AuthorizedKeysFile /root/key.pub
   791            IgnoreUserKnownHosts yes
   792            AllowUsers root
   793            LogLevel INFO
   794            TCPKeepAlive yes
   795            RSAAuthentication yes
   796            PubkeyAuthentication yes
   797  EOF
   798  /usr/sbin/sshd -e -D
   799  /sbin/halt -f
   800  `