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