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

     1  // Copyright 2018 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 vmm provides VMs based on OpenBSD vmm virtualization.
     5  package vmm
     6  
     7  import (
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	"os/exec"
    12  	"path/filepath"
    13  	"regexp"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/google/syzkaller/pkg/config"
    18  	"github.com/google/syzkaller/pkg/log"
    19  	"github.com/google/syzkaller/pkg/osutil"
    20  	"github.com/google/syzkaller/pkg/report"
    21  	"github.com/google/syzkaller/vm/vmimpl"
    22  )
    23  
    24  // Locates the VM id which is used for VM address.
    25  var vmctlStatusRegex = regexp.MustCompile(`^\s+([0-9]+)\b.*\brunning`)
    26  
    27  func init() {
    28  	vmimpl.Register("vmm", ctor, true)
    29  }
    30  
    31  type Config struct {
    32  	Count    int    `json:"count"`    // number of VMs to use
    33  	Mem      int    `json:"mem"`      // amount of VM memory in MBs
    34  	Kernel   string `json:"kernel"`   // kernel to boot
    35  	Template string `json:"template"` // vm template
    36  }
    37  
    38  type Pool struct {
    39  	env *vmimpl.Env
    40  	cfg *Config
    41  }
    42  
    43  type instance struct {
    44  	cfg      *Config
    45  	image    string
    46  	debug    bool
    47  	os       string
    48  	sshkey   string
    49  	sshuser  string
    50  	sshhost  string
    51  	sshport  int
    52  	merger   *vmimpl.OutputMerger
    53  	vmName   string
    54  	vmm      *exec.Cmd
    55  	consolew io.WriteCloser
    56  }
    57  
    58  func ctor(env *vmimpl.Env) (vmimpl.Pool, error) {
    59  	cfg := &Config{
    60  		Count: 1,
    61  		Mem:   512,
    62  	}
    63  
    64  	if !osutil.IsExist(env.Image) {
    65  		return nil, fmt.Errorf("image file '%v' does not exist", env.Image)
    66  	}
    67  
    68  	if err := config.LoadData(env.Config, cfg); err != nil {
    69  		return nil, fmt.Errorf("failed to parse vmm vm config: %w", err)
    70  	}
    71  	if cfg.Count < 1 || cfg.Count > 128 {
    72  		return nil, fmt.Errorf("invalid config param count: %v, want [1-128]", cfg.Count)
    73  	}
    74  	if env.Debug && cfg.Count > 1 {
    75  		log.Logf(0, "limiting number of VMs from %v to 1 in debug mode", cfg.Count)
    76  		cfg.Count = 1
    77  	}
    78  	if cfg.Mem < 128 || cfg.Mem > 1048576 {
    79  		return nil, fmt.Errorf("invalid config param mem: %v, want [128-1048576]", cfg.Mem)
    80  	}
    81  	if cfg.Kernel == "" {
    82  		return nil, fmt.Errorf("missing config param kernel")
    83  	}
    84  	if !osutil.IsExist(cfg.Kernel) {
    85  		return nil, fmt.Errorf("kernel '%v' does not exist", cfg.Kernel)
    86  	}
    87  	pool := &Pool{
    88  		cfg: cfg,
    89  		env: env,
    90  	}
    91  
    92  	return pool, nil
    93  }
    94  
    95  func (pool *Pool) Count() int {
    96  	return pool.cfg.Count
    97  }
    98  
    99  func (pool *Pool) Create(workdir string, index int) (vmimpl.Instance, error) {
   100  	var tee io.Writer
   101  	if pool.env.Debug {
   102  		tee = os.Stdout
   103  	}
   104  	inst := &instance{
   105  		cfg:     pool.cfg,
   106  		image:   filepath.Join(workdir, "disk.qcow2"),
   107  		debug:   pool.env.Debug,
   108  		os:      pool.env.OS,
   109  		sshkey:  pool.env.SSHKey,
   110  		sshuser: pool.env.SSHUser,
   111  		sshport: 22,
   112  		vmName:  fmt.Sprintf("%v-%v", pool.env.Name, index),
   113  		merger:  vmimpl.NewOutputMerger(tee),
   114  	}
   115  
   116  	// Stop the instance from the previous run in case it's still running.
   117  	// This is racy even with -w flag, start periodically fails with:
   118  	// vmctl: start vm command failed: Operation already in progress
   119  	// So also sleep for a bit.
   120  	inst.vmctl("stop", "-f", "-w", inst.vmName)
   121  	time.Sleep(3 * time.Second)
   122  
   123  	createArgs := []string{
   124  		"create",
   125  		"-b", pool.env.Image,
   126  		inst.image,
   127  	}
   128  	if _, err := inst.vmctl(createArgs...); err != nil {
   129  		return nil, err
   130  	}
   131  
   132  	if err := inst.Boot(); err != nil {
   133  		// Cleans up if Boot fails.
   134  		inst.Close()
   135  		return nil, err
   136  	}
   137  
   138  	return inst, nil
   139  }
   140  
   141  func (inst *instance) Boot() error {
   142  	outr, outw, err := osutil.LongPipe()
   143  	if err != nil {
   144  		return err
   145  	}
   146  	inr, inw, err := osutil.LongPipe()
   147  	if err != nil {
   148  		outr.Close()
   149  		outw.Close()
   150  		return err
   151  	}
   152  	startArgs := []string{
   153  		"start",
   154  		"-b", inst.cfg.Kernel,
   155  		"-d", inst.image,
   156  		"-m", fmt.Sprintf("%vM", inst.cfg.Mem),
   157  		"-L", // add a local network interface
   158  		"-c", // connect to the console
   159  	}
   160  	if inst.cfg.Template != "" {
   161  		startArgs = append(startArgs, "-t", inst.cfg.Template)
   162  	}
   163  	startArgs = append(startArgs, inst.vmName)
   164  	if inst.debug {
   165  		log.Logf(0, "running command: vmctl %#v", startArgs)
   166  	}
   167  	cmd := osutil.Command("vmctl", startArgs...)
   168  	cmd.Stdin = inr
   169  	cmd.Stdout = outw
   170  	cmd.Stderr = outw
   171  	if err := cmd.Start(); err != nil {
   172  		outr.Close()
   173  		outw.Close()
   174  		inr.Close()
   175  		inw.Close()
   176  		return err
   177  	}
   178  	inst.vmm = cmd
   179  	inst.consolew = inw
   180  	outw.Close()
   181  	inr.Close()
   182  	inst.merger.Add("console", outr)
   183  
   184  	inst.sshhost, err = inst.lookupSSHAddress()
   185  	if err != nil {
   186  		return err
   187  	}
   188  
   189  	if err := vmimpl.WaitForSSH(inst.debug, 20*time.Minute, inst.sshhost,
   190  		inst.sshkey, inst.sshuser, inst.os, inst.sshport, nil, false); err != nil {
   191  		out := <-inst.merger.Output
   192  		return vmimpl.BootError{Title: err.Error(), Output: out}
   193  	}
   194  	return nil
   195  }
   196  
   197  func (inst *instance) lookupSSHAddress() (string, error) {
   198  	out, err := inst.vmctl("status", inst.vmName)
   199  	if err != nil {
   200  		return "", err
   201  	}
   202  	lines := strings.Split(out, "\n")
   203  	if len(lines) < 2 {
   204  		return "", vmimpl.InfraError{
   205  			Title:  "unexpected vmctl status output",
   206  			Output: []byte(out),
   207  		}
   208  	}
   209  	matches := vmctlStatusRegex.FindStringSubmatch(lines[1])
   210  	if len(matches) < 2 {
   211  		return "", vmimpl.InfraError{
   212  			Title:  "unexpected vmctl status output",
   213  			Output: []byte(out),
   214  		}
   215  	}
   216  	return fmt.Sprintf("100.64.%s.3", matches[1]), nil
   217  }
   218  
   219  func (inst *instance) Close() {
   220  	inst.vmctl("stop", "-f", inst.vmName)
   221  	if inst.consolew != nil {
   222  		inst.consolew.Close()
   223  	}
   224  	if inst.vmm != nil {
   225  		inst.vmm.Process.Kill()
   226  		inst.vmm.Wait()
   227  	}
   228  	inst.merger.Wait()
   229  }
   230  
   231  func (inst *instance) Forward(port int) (string, error) {
   232  	octets := strings.Split(inst.sshhost, ".")
   233  	if len(octets) < 3 {
   234  		return "", fmt.Errorf("too few octets in hostname %v", inst.sshhost)
   235  	}
   236  	addr := fmt.Sprintf("%v.%v.%v.2:%v", octets[0], octets[1], octets[2], port)
   237  	return addr, nil
   238  }
   239  
   240  func (inst *instance) Copy(hostSrc string) (string, error) {
   241  	vmDst := filepath.Join("/root", filepath.Base(hostSrc))
   242  	args := append(vmimpl.SCPArgs(inst.debug, inst.sshkey, inst.sshport, false),
   243  		hostSrc, inst.sshuser+"@"+inst.sshhost+":"+vmDst)
   244  	if inst.debug {
   245  		log.Logf(0, "running command: scp %#v", args)
   246  	}
   247  	_, err := osutil.RunCmd(10*time.Minute, "", "scp", args...)
   248  	if err != nil {
   249  		return "", err
   250  	}
   251  	return vmDst, nil
   252  }
   253  
   254  func (inst *instance) Run(timeout time.Duration, stop <-chan bool, command string) (
   255  	<-chan []byte, <-chan error, error) {
   256  	rpipe, wpipe, err := osutil.LongPipe()
   257  	if err != nil {
   258  		return nil, nil, err
   259  	}
   260  	inst.merger.Add("ssh", rpipe)
   261  
   262  	args := append(vmimpl.SSHArgs(inst.debug, inst.sshkey, inst.sshport, false),
   263  		inst.sshuser+"@"+inst.sshhost, command)
   264  	if inst.debug {
   265  		log.Logf(0, "running command: ssh %#v", args)
   266  	}
   267  	cmd := osutil.Command("ssh", args...)
   268  	cmd.Stdout = wpipe
   269  	cmd.Stderr = wpipe
   270  	if err := cmd.Start(); err != nil {
   271  		wpipe.Close()
   272  		return nil, nil, err
   273  	}
   274  	wpipe.Close()
   275  	errc := make(chan error, 1)
   276  	signal := func(err error) {
   277  		select {
   278  		case errc <- err:
   279  		default:
   280  		}
   281  	}
   282  
   283  	go func() {
   284  		select {
   285  		case <-time.After(timeout):
   286  			signal(vmimpl.ErrTimeout)
   287  		case <-stop:
   288  			signal(vmimpl.ErrTimeout)
   289  		case err := <-inst.merger.Err:
   290  			cmd.Process.Kill()
   291  			if cmdErr := cmd.Wait(); cmdErr == nil {
   292  				// If the command exited successfully, we got EOF error from merger.
   293  				// But in this case no error has happened and the EOF is expected.
   294  				err = nil
   295  			}
   296  			signal(err)
   297  			return
   298  		}
   299  		cmd.Process.Kill()
   300  		cmd.Wait()
   301  	}()
   302  	return inst.merger.Output, errc, nil
   303  }
   304  
   305  func (inst *instance) Diagnose(rep *report.Report) ([]byte, bool) {
   306  	return vmimpl.DiagnoseOpenBSD(inst.consolew)
   307  }
   308  
   309  // Run the given vmctl(8) command and wait for it to finish.
   310  func (inst *instance) vmctl(args ...string) (string, error) {
   311  	if inst.debug {
   312  		log.Logf(0, "running command: vmctl %#v", args)
   313  	}
   314  	out, err := osutil.RunCmd(time.Minute, "", "vmctl", args...)
   315  	if err != nil {
   316  		if inst.debug {
   317  			log.Logf(0, "vmctl failed: %v", err)
   318  		}
   319  		return "", err
   320  	}
   321  	if inst.debug {
   322  		log.Logf(0, "vmctl output: %v", string(out))
   323  	}
   324  	return string(out), nil
   325  }