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

     1  // Copyright 2019 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 bhyve
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"os/exec"
    11  	"path/filepath"
    12  	"regexp"
    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  	vmimpl.Register("bhyve", ctor, true)
    25  }
    26  
    27  type Config struct {
    28  	Bridge  string `json:"bridge"`  // name of network bridge device
    29  	Count   int    `json:"count"`   // number of VMs to use
    30  	CPU     int    `json:"cpu"`     // number of VM vCPU
    31  	HostIP  string `json:"hostip"`  // VM host IP address
    32  	Mem     string `json:"mem"`     // amount of VM memory
    33  	Dataset string `json:"dataset"` // ZFS dataset containing VM image
    34  }
    35  
    36  type Pool struct {
    37  	env *vmimpl.Env
    38  	cfg *Config
    39  }
    40  
    41  type instance struct {
    42  	cfg      *Config
    43  	snapshot string
    44  	tapdev   string
    45  	image    string
    46  	debug    bool
    47  	os       string
    48  	sshkey   string
    49  	sshuser  string
    50  	sshhost  string
    51  	merger   *vmimpl.OutputMerger
    52  	vmName   string
    53  	bhyve    *exec.Cmd
    54  	consolew io.WriteCloser
    55  }
    56  
    57  var ipRegex = regexp.MustCompile(`bound to (([0-9]+\.){3}[0-9]+) `)
    58  var tapRegex = regexp.MustCompile(`^tap[0-9]+`)
    59  
    60  func ctor(env *vmimpl.Env) (vmimpl.Pool, error) {
    61  	cfg := &Config{
    62  		Count: 1,
    63  		CPU:   1,
    64  		Mem:   "512M",
    65  	}
    66  	if err := config.LoadData(env.Config, cfg); err != nil {
    67  		return nil, fmt.Errorf("failed to parse bhyve vm config: %w", err)
    68  	}
    69  	if cfg.Count < 1 || cfg.Count > 128 {
    70  		return nil, fmt.Errorf("invalid config param count: %v, want [1-128]", cfg.Count)
    71  	}
    72  	if env.Debug && cfg.Count > 1 {
    73  		log.Logf(0, "limiting number of VMs from %v to 1 in debug mode", cfg.Count)
    74  		cfg.Count = 1
    75  	}
    76  	pool := &Pool{
    77  		cfg: cfg,
    78  		env: env,
    79  	}
    80  	return pool, nil
    81  }
    82  
    83  func (pool *Pool) Count() int {
    84  	return pool.cfg.Count
    85  }
    86  
    87  func (pool *Pool) Create(workdir string, index int) (vmimpl.Instance, error) {
    88  	inst := &instance{
    89  		cfg:     pool.cfg,
    90  		debug:   pool.env.Debug,
    91  		os:      pool.env.OS,
    92  		sshkey:  pool.env.SSHKey,
    93  		sshuser: pool.env.SSHUser,
    94  		vmName:  fmt.Sprintf("syzkaller-%v-%v", pool.env.Name, index),
    95  	}
    96  
    97  	dataset := inst.cfg.Dataset
    98  	mountpoint, err := osutil.RunCmd(time.Minute, "", "zfs", "get", "-H", "-o", "value", "mountpoint", dataset)
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  
   103  	snapshot := fmt.Sprintf("%v@bhyve-%v", dataset, inst.vmName)
   104  	clone := fmt.Sprintf("%v/bhyve-%v", dataset, inst.vmName)
   105  
   106  	prefix := strings.TrimSuffix(string(mountpoint), "\n") + "/"
   107  	image := strings.TrimPrefix(pool.env.Image, prefix)
   108  	if image == pool.env.Image {
   109  		return nil, fmt.Errorf("image file %v not contained in dataset %v", image, prefix)
   110  	}
   111  	inst.image = prefix + fmt.Sprintf("bhyve-%v", inst.vmName) + "/" + image
   112  
   113  	// Stop the instance from a previous run in case it's still running.
   114  	osutil.RunCmd(time.Minute, "", "bhyvectl", "--destroy", fmt.Sprintf("--vm=%v", inst.vmName))
   115  	// Destroy a lingering snapshot and clone.
   116  	osutil.RunCmd(time.Minute, "", "zfs", "destroy", "-R", snapshot)
   117  
   118  	// Create a snapshot of the data set containing the VM image.
   119  	// bhyve will use a clone of the snapshot, which gets recreated every time the VM
   120  	// is restarted. This is all to work around bhyve's current lack of an
   121  	// image snapshot facility.
   122  	if _, err := osutil.RunCmd(time.Minute, "", "zfs", "snapshot", snapshot); err != nil {
   123  		inst.Close()
   124  		return nil, err
   125  	}
   126  	inst.snapshot = snapshot
   127  	if _, err := osutil.RunCmd(time.Minute, "", "zfs", "clone", snapshot, clone); err != nil {
   128  		inst.Close()
   129  		return nil, err
   130  	}
   131  
   132  	tapdev, err := osutil.RunCmd(time.Minute, "", "ifconfig", "tap", "create")
   133  	if err != nil {
   134  		inst.Close()
   135  		return nil, err
   136  	}
   137  	inst.tapdev = tapRegex.FindString(string(tapdev))
   138  	if _, err := osutil.RunCmd(time.Minute, "", "ifconfig", inst.cfg.Bridge, "addm", inst.tapdev); err != nil {
   139  		inst.Close()
   140  		return nil, err
   141  	}
   142  
   143  	if err := inst.Boot(); err != nil {
   144  		inst.Close()
   145  		return nil, err
   146  	}
   147  
   148  	return inst, nil
   149  }
   150  
   151  func (inst *instance) Boot() error {
   152  	loaderArgs := []string{
   153  		"-c", "stdio",
   154  		"-m", inst.cfg.Mem,
   155  		"-d", inst.image,
   156  		"-e", "autoboot_delay=0",
   157  		inst.vmName,
   158  	}
   159  
   160  	// Stop the instance from the previous run in case it's still running.
   161  	osutil.RunCmd(time.Minute, "", "bhyvectl", "--destroy", fmt.Sprintf("--vm=%v", inst.vmName))
   162  
   163  	_, err := osutil.RunCmd(time.Minute, "", "bhyveload", loaderArgs...)
   164  	if err != nil {
   165  		return err
   166  	}
   167  
   168  	bhyveArgs := []string{
   169  		"-H", "-A", "-P",
   170  		"-c", fmt.Sprintf("%d", inst.cfg.CPU),
   171  		"-m", inst.cfg.Mem,
   172  		"-s", "0:0,hostbridge",
   173  		"-s", "1:0,lpc",
   174  		"-s", fmt.Sprintf("2:0,virtio-net,%v", inst.tapdev),
   175  		"-s", fmt.Sprintf("3:0,virtio-blk,%v", inst.image),
   176  		"-l", "com1,stdio",
   177  		inst.vmName,
   178  	}
   179  
   180  	outr, outw, err := osutil.LongPipe()
   181  	if err != nil {
   182  		return err
   183  	}
   184  	inr, inw, err := osutil.LongPipe()
   185  	if err != nil {
   186  		outr.Close()
   187  		outw.Close()
   188  		return err
   189  	}
   190  
   191  	bhyve := osutil.Command("bhyve", bhyveArgs...)
   192  	bhyve.Stdin = inr
   193  	bhyve.Stdout = outw
   194  	bhyve.Stderr = outw
   195  	if err := bhyve.Start(); err != nil {
   196  		outr.Close()
   197  		outw.Close()
   198  		inr.Close()
   199  		inw.Close()
   200  		return err
   201  	}
   202  	outw.Close()
   203  	outw = nil
   204  	inst.consolew = inw
   205  	inr.Close()
   206  	inst.bhyve = bhyve
   207  
   208  	var tee io.Writer
   209  	if inst.debug {
   210  		tee = os.Stdout
   211  	}
   212  	inst.merger = vmimpl.NewOutputMerger(tee)
   213  	inst.merger.Add("console", outr)
   214  	outr = nil
   215  
   216  	var bootOutput []byte
   217  	bootOutputStop := make(chan bool)
   218  	ipch := make(chan string, 1)
   219  	go func() {
   220  		gotip := false
   221  		for {
   222  			select {
   223  			case out := <-inst.merger.Output:
   224  				bootOutput = append(bootOutput, out...)
   225  			case <-bootOutputStop:
   226  				close(bootOutputStop)
   227  				return
   228  			}
   229  			if gotip {
   230  				continue
   231  			}
   232  			if ip := parseIP(bootOutput); ip != "" {
   233  				ipch <- ip
   234  				gotip = true
   235  			}
   236  		}
   237  	}()
   238  
   239  	select {
   240  	case ip := <-ipch:
   241  		inst.sshhost = ip
   242  	case <-inst.merger.Err:
   243  		bootOutputStop <- true
   244  		<-bootOutputStop
   245  		return vmimpl.BootError{Title: "bhyve exited", Output: bootOutput}
   246  	case <-time.After(10 * time.Minute):
   247  		bootOutputStop <- true
   248  		<-bootOutputStop
   249  		return vmimpl.BootError{Title: "no IP found", Output: bootOutput}
   250  	}
   251  
   252  	if err := vmimpl.WaitForSSH(inst.debug, 10*time.Minute, inst.sshhost,
   253  		inst.sshkey, inst.sshuser, inst.os, 22, nil, false); err != nil {
   254  		bootOutputStop <- true
   255  		<-bootOutputStop
   256  		return vmimpl.MakeBootError(err, bootOutput)
   257  	}
   258  	bootOutputStop <- true
   259  	return nil
   260  }
   261  
   262  func (inst *instance) Close() {
   263  	if inst.consolew != nil {
   264  		inst.consolew.Close()
   265  	}
   266  	if inst.bhyve != nil {
   267  		inst.bhyve.Process.Kill()
   268  		inst.bhyve.Wait()
   269  		osutil.RunCmd(time.Minute, "", "bhyvectl", fmt.Sprintf("--vm=%v", inst.vmName), "--destroy")
   270  		inst.bhyve = nil
   271  	}
   272  	if inst.snapshot != "" {
   273  		osutil.RunCmd(time.Minute, "", "zfs", "destroy", "-R", inst.snapshot)
   274  		inst.snapshot = ""
   275  	}
   276  	if inst.tapdev != "" {
   277  		osutil.RunCmd(time.Minute, "", "ifconfig", inst.tapdev, "destroy")
   278  		inst.tapdev = ""
   279  	}
   280  }
   281  
   282  func (inst *instance) Forward(port int) (string, error) {
   283  	return fmt.Sprintf("%v:%v", inst.cfg.HostIP, port), nil
   284  }
   285  
   286  func (inst *instance) Copy(hostSrc string) (string, error) {
   287  	vmDst := filepath.Join("/root", filepath.Base(hostSrc))
   288  	args := append(vmimpl.SCPArgs(inst.debug, inst.sshkey, 22, false),
   289  		hostSrc, inst.sshuser+"@"+inst.sshhost+":"+vmDst)
   290  	if inst.debug {
   291  		log.Logf(0, "running command: scp %#v", args)
   292  	}
   293  	_, err := osutil.RunCmd(10*time.Minute, "", "scp", args...)
   294  	if err != nil {
   295  		return "", err
   296  	}
   297  	return vmDst, nil
   298  }
   299  
   300  func (inst *instance) Run(timeout time.Duration, stop <-chan bool, command string) (
   301  	<-chan []byte, <-chan error, error) {
   302  	rpipe, wpipe, err := osutil.LongPipe()
   303  	if err != nil {
   304  		return nil, nil, err
   305  	}
   306  	inst.merger.Add("ssh", rpipe)
   307  
   308  	args := append(vmimpl.SSHArgs(inst.debug, inst.sshkey, 22, false),
   309  		inst.sshuser+"@"+inst.sshhost, command)
   310  	if inst.debug {
   311  		log.Logf(0, "running command: ssh %#v", args)
   312  	}
   313  	cmd := osutil.Command("ssh", args...)
   314  	cmd.Stdout = wpipe
   315  	cmd.Stderr = wpipe
   316  	if err := cmd.Start(); err != nil {
   317  		wpipe.Close()
   318  		return nil, nil, err
   319  	}
   320  	wpipe.Close()
   321  	errc := make(chan error, 1)
   322  	signal := func(err error) {
   323  		select {
   324  		case errc <- err:
   325  		default:
   326  		}
   327  	}
   328  
   329  	go func() {
   330  		select {
   331  		case <-time.After(timeout):
   332  			signal(vmimpl.ErrTimeout)
   333  		case <-stop:
   334  			signal(vmimpl.ErrTimeout)
   335  		case err := <-inst.merger.Err:
   336  			cmd.Process.Kill()
   337  			if cmdErr := cmd.Wait(); cmdErr == nil {
   338  				// If the command exited successfully, we got EOF error from merger.
   339  				// But in this case no error has happened and the EOF is expected.
   340  				err = nil
   341  			}
   342  			signal(err)
   343  			return
   344  		}
   345  		cmd.Process.Kill()
   346  		cmd.Wait()
   347  	}()
   348  	return inst.merger.Output, errc, nil
   349  }
   350  
   351  func (inst *instance) Diagnose(rep *report.Report) ([]byte, bool) {
   352  	return vmimpl.DiagnoseFreeBSD(inst.consolew)
   353  }
   354  
   355  func parseIP(output []byte) string {
   356  	matches := ipRegex.FindSubmatch(output)
   357  	if len(matches) < 2 {
   358  		return ""
   359  	}
   360  	return string(matches[1])
   361  }