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