github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/vm/virtualbox/virtualbox.go (about)

     1  // Copyright 2025 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 virtualbox
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"io"
    10  	"net"
    11  	"os"
    12  	"os/exec"
    13  	"path/filepath"
    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/sys/targets"
    21  	"github.com/google/syzkaller/vm/vmimpl"
    22  )
    23  
    24  func init() {
    25  	vmimpl.Register("virtualbox", vmimpl.Type{Ctor: ctor})
    26  }
    27  
    28  type Config struct {
    29  	BaseVM string `json:"base_vm_name"` // name of the base VM
    30  	Count  int    `json:"count"`        // number of VMs to run in parallel
    31  }
    32  
    33  type Pool struct {
    34  	env *vmimpl.Env
    35  	cfg *Config
    36  }
    37  
    38  type instance struct {
    39  	cfg        *Config
    40  	debug      bool
    41  	baseVM     string
    42  	vmName     string
    43  	rpcPort    int
    44  	timeouts   targets.Timeouts
    45  	serialPath string
    46  	closed     chan bool
    47  	os         string
    48  	merger     *vmimpl.OutputMerger
    49  	rpipe      io.ReadCloser
    50  	wpipe      io.WriteCloser
    51  	uartConn   net.Conn
    52  	vmimpl.SSHOptions
    53  }
    54  
    55  func ctor(env *vmimpl.Env) (vmimpl.Pool, error) {
    56  	cfg := &Config{}
    57  	if err := config.LoadData(env.Config, cfg); err != nil {
    58  		return nil, err
    59  	}
    60  	if cfg.BaseVM == "" {
    61  		return nil, fmt.Errorf("config param base_vm is empty")
    62  	}
    63  	if cfg.Count < 1 || cfg.Count > 128 {
    64  		return nil, fmt.Errorf("invalid config param count: %v, want [1,128]", cfg.Count)
    65  	}
    66  	if _, err := exec.LookPath("VBoxManage"); err != nil {
    67  		return nil, fmt.Errorf("cannot find VBoxManage")
    68  	}
    69  	return &Pool{cfg: cfg, env: env}, nil
    70  }
    71  
    72  func (pool *Pool) Count() int { return pool.cfg.Count }
    73  
    74  func (pool *Pool) Create(_ context.Context, workdir string, index int) (vmimpl.Instance, error) {
    75  	serialPath := filepath.Join(workdir, "serial")
    76  	vmName := fmt.Sprintf("syzkaller_vm_%d", index)
    77  	inst := &instance{
    78  		cfg:        pool.cfg,
    79  		debug:      pool.env.Debug,
    80  		baseVM:     pool.cfg.BaseVM,
    81  		vmName:     vmName,
    82  		os:         pool.env.OS,
    83  		timeouts:   pool.env.Timeouts,
    84  		serialPath: serialPath,
    85  		closed:     make(chan bool),
    86  		SSHOptions: vmimpl.SSHOptions{
    87  			Addr: "localhost",
    88  			Port: 0,
    89  			Key:  pool.env.SSHKey,
    90  			User: pool.env.SSHUser,
    91  		},
    92  	}
    93  	rp, wp, err := osutil.LongPipe()
    94  	if err != nil {
    95  		return nil, err
    96  	}
    97  	inst.rpipe, inst.wpipe = rp, wp
    98  	if err := inst.clone(); err != nil {
    99  		return nil, err
   100  	}
   101  	if err := inst.boot(); err != nil {
   102  		return nil, err
   103  	}
   104  	return inst, nil
   105  }
   106  
   107  // We avoid immutable disks and reset the image manually, as this proved more reliable when VMs fail to restart cleanly.
   108  func (inst *instance) clone() error {
   109  	if inst.debug {
   110  		log.Logf(0, "cloning VM %q to %q", inst.baseVM, inst.vmName)
   111  	}
   112  	if _, err := osutil.RunCmd(2*time.Minute, "", "VBoxManage", "clonevm", inst.baseVM,
   113  		"--name", inst.vmName, "--register"); err != nil {
   114  		if inst.debug {
   115  			log.Logf(0, "clone failed for VM %q -> %q: %v", inst.baseVM, inst.vmName, err)
   116  		}
   117  		return err
   118  	}
   119  	inst.SSHOptions.Port = vmimpl.UnusedTCPPort()
   120  	rule := fmt.Sprintf("syzkaller_pf_%d", inst.SSHOptions.Port)
   121  	natArg := fmt.Sprintf("%s,tcp,,%d,,22", rule, inst.SSHOptions.Port)
   122  	if inst.debug {
   123  		log.Logf(0, "setting NAT rule %q", natArg)
   124  	}
   125  	if _, err := osutil.RunCmd(2*time.Minute, "", "VBoxManage",
   126  		"modifyvm", inst.vmName, "--natpf1", natArg); err != nil {
   127  		if inst.debug {
   128  			log.Logf(0, "VBoxManage modifyvm --natpf1 failed: %v", err)
   129  		}
   130  		return err
   131  	}
   132  	if inst.debug {
   133  		log.Logf(0, "SSH NAT forwarding: host 127.0.0.1:%d -> guest:22", inst.SSHOptions.Port)
   134  	}
   135  
   136  	serialDir := filepath.Dir(inst.serialPath)
   137  	if inst.debug {
   138  		log.Logf(0, "ensuring serial parent directory exists: %s", serialDir)
   139  	}
   140  	if err := os.MkdirAll(serialDir, 0755); err != nil {
   141  		return fmt.Errorf("failed to create serial directory %s: %w", serialDir, err)
   142  	}
   143  	if inst.debug {
   144  		log.Logf(0, "enabling UART on VM %q (0x3F8/IRQ4) and piping to %s", inst.vmName, inst.serialPath)
   145  	}
   146  	if _, err := osutil.RunCmd(2*time.Minute, "", "VBoxManage",
   147  		"modifyvm", inst.vmName, "--uart1", "0x3F8", "4"); err != nil {
   148  		if inst.debug {
   149  			log.Logf(0, "VBoxManage modifyvm --uart1 failed: %v", err)
   150  		}
   151  		return err
   152  	}
   153  	if _, err := osutil.RunCmd(2*time.Minute, "", "VBoxManage",
   154  		"modifyvm", inst.vmName, "--uart-mode1", "server", inst.serialPath); err != nil {
   155  		if inst.debug {
   156  			log.Logf(0, "VBoxManage modifyvm --uart-mode1 failed: %v", err)
   157  		}
   158  		return err
   159  	}
   160  
   161  	return nil
   162  }
   163  
   164  func (inst *instance) boot() error {
   165  	if inst.debug {
   166  		log.Logf(0, "booting VM %q (headless)", inst.vmName)
   167  	}
   168  	if _, err := osutil.RunCmd(2*time.Minute, "", "VBoxManage",
   169  		"startvm", inst.vmName, "--type", "headless"); err != nil {
   170  		if inst.debug {
   171  			log.Logf(0, "VBoxManage startvm failed: %v", err)
   172  		}
   173  		return err
   174  	}
   175  
   176  	var tee io.Writer
   177  	if inst.debug {
   178  		tee = os.Stdout
   179  	}
   180  	inst.merger = vmimpl.NewOutputMerger(tee)
   181  	inst.merger.Add("virtualbox", inst.rpipe)
   182  	inst.rpipe = nil
   183  
   184  	// Connect to the serial console and add it to the merger.
   185  	var err error
   186  	inst.uartConn, err = net.Dial("unix", inst.serialPath)
   187  	if err != nil {
   188  		if inst.debug {
   189  			log.Logf(0, "failed to connect to serial socket %s: %v", inst.serialPath, err)
   190  		}
   191  		return err
   192  	}
   193  	inst.merger.Add("dmesg", inst.uartConn)
   194  
   195  	var bootOutput []byte
   196  	bootOutputStop := make(chan bool)
   197  	go func() {
   198  		for {
   199  			select {
   200  			case out := <-inst.merger.Output:
   201  				bootOutput = append(bootOutput, out...)
   202  			case <-bootOutputStop:
   203  				close(bootOutputStop)
   204  				return
   205  			}
   206  		}
   207  	}()
   208  	if err := vmimpl.WaitForSSH(10*time.Minute*inst.timeouts.Scale, inst.SSHOptions,
   209  		inst.os, inst.merger.Err, false, inst.debug); err != nil {
   210  		bootOutputStop <- true
   211  		<-bootOutputStop
   212  		return vmimpl.MakeBootError(err, bootOutput)
   213  	}
   214  	bootOutputStop <- true
   215  
   216  	return nil
   217  }
   218  
   219  func (inst *instance) Forward(port int) (string, error) {
   220  	if inst.rpcPort != 0 {
   221  		return "", fmt.Errorf("isolated: Forward port already set")
   222  	}
   223  	if port == 0 {
   224  		return "", fmt.Errorf("isolated: Forward port is zero")
   225  	}
   226  	inst.rpcPort = port
   227  	return fmt.Sprintf("127.0.0.1:%d", port), nil
   228  }
   229  
   230  func (inst *instance) Close() error {
   231  	if inst.debug {
   232  		log.Logf(0, "stopping %v", inst.vmName)
   233  	}
   234  	osutil.RunCmd(2*time.Minute, "", "VBoxManage", "controlvm", inst.vmName, "poweroff")
   235  	if inst.debug {
   236  		log.Logf(0, "deleting %v", inst.vmName)
   237  	}
   238  	osutil.RunCmd(2*time.Minute, "", "VBoxManage", "unregistervm", inst.vmName, "--delete")
   239  	close(inst.closed)
   240  	if inst.rpipe != nil {
   241  		inst.rpipe.Close()
   242  	}
   243  	if inst.wpipe != nil {
   244  		inst.wpipe.Close()
   245  	}
   246  	return nil
   247  }
   248  
   249  func (inst *instance) Copy(hostSrc string) (string, error) {
   250  	base := filepath.Base(hostSrc)
   251  	vmDest := "/" + base
   252  
   253  	args := vmimpl.SCPArgs(inst.debug, inst.SSHOptions.Key, inst.SSHOptions.Port, false)
   254  	args = append(args, hostSrc, fmt.Sprintf("%v@127.0.0.1:%v", inst.SSHOptions.User, vmDest))
   255  
   256  	if inst.debug {
   257  		log.Logf(0, "running command: scp %#v", args)
   258  	}
   259  
   260  	if _, err := osutil.RunCmd(3*time.Minute, "", "scp", args...); err != nil {
   261  		return "", err
   262  	}
   263  	return vmDest, nil
   264  }
   265  
   266  func (inst *instance) Run(ctx context.Context, command string) (
   267  	<-chan []byte, <-chan error, error) {
   268  	if inst.uartConn == nil {
   269  		if inst.debug {
   270  			log.Logf(0, "serial console not available; returning an error")
   271  		}
   272  		return nil, nil, fmt.Errorf("serial console not available")
   273  	}
   274  	args := vmimpl.SSHArgs(inst.debug, inst.SSHOptions.Key, inst.SSHOptions.Port, false)
   275  	if inst.rpcPort != 0 {
   276  		proxy := fmt.Sprintf("%d:127.0.0.1:%d", inst.rpcPort, inst.rpcPort)
   277  		args = append(args, "-R", proxy)
   278  	}
   279  
   280  	args = append(args, fmt.Sprintf("%v@127.0.0.1", inst.SSHOptions.User), fmt.Sprintf("cd / && exec %v", command))
   281  	if inst.debug {
   282  		log.Logf(0, "running command: ssh %#v", args)
   283  	}
   284  	cmd := osutil.Command("ssh", args...)
   285  	rpipe, wpipe, err := osutil.LongPipe()
   286  	if err != nil {
   287  		if inst.debug {
   288  			log.Logf(0, "LongPipe failed: %v", err)
   289  		}
   290  		if inst.uartConn != nil {
   291  			inst.uartConn.Close()
   292  		}
   293  		return nil, nil, err
   294  	}
   295  	cmd.Stdout = wpipe
   296  	cmd.Stderr = wpipe
   297  	if err := cmd.Start(); err != nil {
   298  		wpipe.Close()
   299  		rpipe.Close()
   300  		if inst.uartConn != nil {
   301  			inst.uartConn.Close()
   302  		}
   303  		return nil, nil, err
   304  	}
   305  	wpipe.Close()
   306  
   307  	inst.merger.Add("ssh", rpipe)
   308  
   309  	return vmimpl.Multiplex(ctx, cmd, inst.merger, vmimpl.MultiplexConfig{
   310  		Console: inst.uartConn,
   311  		Close:   inst.closed,
   312  		Debug:   inst.debug,
   313  		Scale:   inst.timeouts.Scale,
   314  	})
   315  }
   316  
   317  func (inst *instance) Diagnose(rep *report.Report) ([]byte, bool) {
   318  	return nil, false
   319  }