From f7988ea4935e2627f33bc73cf235f2c1942dce76 Mon Sep 17 00:00:00 2001 From: Kuzey Arda Bulut Date: Thu, 6 Nov 2025 15:12:01 +0200 Subject: vm: implement the VM interface for VirtualBox This change adds VirtualBox support to syzkaller. It implements the VM interface for VirtualBox and provides: - full VM lifecycle operations (create, boot, stop, snapshot restore) - serial console hookup and integration with the output merger - proper boot wait logic similar to qemu, using SSH readiness - boot-time crash capture using collected console output --- CONTRIBUTORS | 1 + docs/linux/setup.md | 1 + ...etup_ubuntu-host_virtualbox-vm_x86-64-kernel.md | 106 +++++++ vm/virtualbox/virtualbox.go | 319 +++++++++++++++++++++ vm/vm.go | 1 + 5 files changed, 428 insertions(+) create mode 100644 docs/linux/setup_ubuntu-host_virtualbox-vm_x86-64-kernel.md create mode 100644 vm/virtualbox/virtualbox.go diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 0cb36092b..4e7c2caed 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -142,3 +142,4 @@ Rivos Inc. Jeongjun Park Nikita Zhandarovich Jiacheng Xu +Kuzey Arda Bulut diff --git a/docs/linux/setup.md b/docs/linux/setup.md index cf7f2c273..c1c61690e 100644 --- a/docs/linux/setup.md +++ b/docs/linux/setup.md @@ -13,6 +13,7 @@ Instructions for a particular VM type or kernel architecture can be found on the - [Setup: Linux host, Android virtual device, x86-64 kernel](setup_linux-host_android-virtual-device_x86-64-kernel.md) - [Setup: Linux isolated host](setup_linux-host_isolated.md) - [Setup: Ubuntu host, VMware vm, x86-64 kernel](setup_ubuntu-host_vmware-vm_x86-64-kernel.md) +- [Setup: Ubuntu host, VirtualBox vm, x86-64 kernel](setup_ubuntu-host_virtualbox-vm_x86-64-kernel.md) ## Install diff --git a/docs/linux/setup_ubuntu-host_virtualbox-vm_x86-64-kernel.md b/docs/linux/setup_ubuntu-host_virtualbox-vm_x86-64-kernel.md new file mode 100644 index 000000000..f5cde0fd5 --- /dev/null +++ b/docs/linux/setup_ubuntu-host_virtualbox-vm_x86-64-kernel.md @@ -0,0 +1,106 @@ +# Setup: Ubuntu host, VirtualBox vm, x86-64 kernel + +These are the instructions on how to fuzz the x86-64 kernel in VirtualBox with Ubuntu on the host machine and Debian Bullseye in the virtual machines. + +In the instructions below, the `$VAR` notation (e.g. `$GCC`, `$KERNEL`, etc.) is used to denote paths to directories that are either created when executing the instructions (e.g. when unpacking GCC archive, a directory will be created), or that you have to create yourself before running the instructions. Substitute the values for those variables manually. + +## GCC and Kernel + +You can follow the same [instructions](/docs/linux/setup_ubuntu-host_qemu-vm_x86-64-kernel.md) for obtaining GCC and building the Linux kernel as when using QEMU. + +## Image + +Install debootstrap: + +``` bash +sudo apt-get install debootstrap +``` + +To create a Debian Bullseye Linux user space in the $USERSPACE dir do: +``` +sudo mkdir -p $USERSPACE +sudo debootstrap --include=openssh-server,curl,tar,gcc,libc6-dev,time,strace,sudo,less,psmisc,selinux-utils,policycoreutils,checkpolicy,selinux-policy-default,firmware-atheros,open-vm-tools --components=main,contrib,non-free bullseye $USERSPACE +``` + +Note: it is important to include the `open-vm-tools` package in the user space as it provides better VM management. + +To create a Debian Bullseye Linux VMDK do: + +``` +wget https://raw.githubusercontent.com/google/syzkaller/master/tools/create-gce-image.sh -O create-gce-image.sh +chmod +x create-gce-image.sh +./create-gce-image.sh $USERSPACE $KERNEL/arch/x86/boot/bzImage +qemu-img convert -f raw -O vdi disk.raw disk.vdi +``` + +The result should be `disk.vdi` for the disk image. You can delete `disk.raw` if you want. + +## VirtualBox + +Open VirtualBox and start the New Virtual Machine Wizard. +Assuming you want to create the new VM in `$VMPATH`, complete the wizard as follows: + +* Create New Virtual Machine +* Virtual Machine Name and Location: select `$VMPATH` as location and "debian" as name +* Guest OS type: Debian 64-bit +* Disk: select "Use an existing virtual disk" +* Import the `disk.vdi` file, and select the imported `.vdi` file as an Hard Disk File. + +When you complete the wizard, you should have `$VMPATH/debian.vbox`. From this point onward, you no longer need the VirtualBox UI. + +To test the fuzzing environment before getting started, follow the instructions below: +Forwarding port 2222 on your host machine to port 22: +``` bash +VBoxManage modifyvm debian --natpf1 "test,tcp,,2222,,22" +``` + +Starting the Debian VM (headless): +``` bash +VBoxManage startvm debian --type headless +``` + +SSH into the VM: +``` bash +ssh -p 2222 root@127.0.0.1 +``` + +Stopping the VM: +``` bash +VBoxManage controlvm debian poweroff +``` + +If all of the above `VBoxManage` commands work, then you can proceed to running syzkaller. + +## syzkaller + +Create a manager config like the following, replacing the environment variables $GOPATH, $KERNEL and $VMPATH with their actual values. + +``` +{ + "target": "linux/amd64", + "http": "127.0.0.1:56741", + "workdir": "$GOPATH/src/github.com/google/syzkaller/workdir", + "kernel_obj": "$KERNEL", + "sshkey": "$IMAGE/key", + "syzkaller": "$GOPATH/src/github.com/google/syzkaller", + "procs": 8, + "type": "virtualbox", + "vm": { + "count": 4, + "base_vm_name": "debian" + } +} +``` + +Run syzkaller manager: + +``` bash +mkdir workdir +./bin/syz-manager -config=my.cfg +``` + +Syzkaller will create full clone VMs from the `debian` VM and then use ssh to copy and execute programs in them. +The `debian` VM will not be started and its disk will remain unmodified. + +If you get issues after `syz-manager` starts, consider running it with the `-debug` flag. +Also see [this page](/docs/troubleshooting.md) for troubleshooting tips. \ No newline at end of file diff --git a/vm/virtualbox/virtualbox.go b/vm/virtualbox/virtualbox.go new file mode 100644 index 000000000..9383d9d8c --- /dev/null +++ b/vm/virtualbox/virtualbox.go @@ -0,0 +1,319 @@ +// Copyright 2025 syzkaller project authors. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +package virtualbox + +import ( + "context" + "fmt" + "io" + "net" + "os" + "os/exec" + "path/filepath" + "time" + + "github.com/google/syzkaller/pkg/config" + "github.com/google/syzkaller/pkg/log" + "github.com/google/syzkaller/pkg/osutil" + "github.com/google/syzkaller/pkg/report" + "github.com/google/syzkaller/sys/targets" + "github.com/google/syzkaller/vm/vmimpl" +) + +func init() { + vmimpl.Register("virtualbox", vmimpl.Type{Ctor: ctor}) +} + +type Config struct { + BaseVM string `json:"base_vm_name"` // name of the base VM + Count int `json:"count"` // number of VMs to run in parallel +} + +type Pool struct { + env *vmimpl.Env + cfg *Config +} + +type instance struct { + cfg *Config + debug bool + baseVM string + vmName string + rpcPort int + timeouts targets.Timeouts + serialPath string + closed chan bool + os string + merger *vmimpl.OutputMerger + rpipe io.ReadCloser + wpipe io.WriteCloser + uartConn net.Conn + vmimpl.SSHOptions +} + +func ctor(env *vmimpl.Env) (vmimpl.Pool, error) { + cfg := &Config{} + if err := config.LoadData(env.Config, cfg); err != nil { + return nil, err + } + if cfg.BaseVM == "" { + return nil, fmt.Errorf("config param base_vm is empty") + } + if cfg.Count < 1 || cfg.Count > 128 { + return nil, fmt.Errorf("invalid config param count: %v, want [1,128]", cfg.Count) + } + if _, err := exec.LookPath("VBoxManage"); err != nil { + return nil, fmt.Errorf("cannot find VBoxManage") + } + return &Pool{cfg: cfg, env: env}, nil +} + +func (pool *Pool) Count() int { return pool.cfg.Count } + +func (pool *Pool) Create(_ context.Context, workdir string, index int) (vmimpl.Instance, error) { + serialPath := filepath.Join(workdir, "serial") + vmName := fmt.Sprintf("syzkaller_vm_%d", index) + inst := &instance{ + cfg: pool.cfg, + debug: pool.env.Debug, + baseVM: pool.cfg.BaseVM, + vmName: vmName, + os: pool.env.OS, + timeouts: pool.env.Timeouts, + serialPath: serialPath, + closed: make(chan bool), + SSHOptions: vmimpl.SSHOptions{ + Addr: "localhost", + Port: 0, + Key: pool.env.SSHKey, + User: pool.env.SSHUser, + }, + } + rp, wp, err := osutil.LongPipe() + if err != nil { + return nil, err + } + inst.rpipe, inst.wpipe = rp, wp + if err := inst.clone(); err != nil { + return nil, err + } + if err := inst.boot(); err != nil { + return nil, err + } + return inst, nil +} + +// We avoid immutable disks and reset the image manually, as this proved more reliable when VMs fail to restart cleanly. +func (inst *instance) clone() error { + if inst.debug { + log.Logf(0, "cloning VM %q to %q", inst.baseVM, inst.vmName) + } + if _, err := osutil.RunCmd(2*time.Minute, "", "VBoxManage", "clonevm", inst.baseVM, + "--name", inst.vmName, "--register"); err != nil { + if inst.debug { + log.Logf(0, "clone failed for VM %q -> %q: %v", inst.baseVM, inst.vmName, err) + } + return err + } + inst.SSHOptions.Port = vmimpl.UnusedTCPPort() + rule := fmt.Sprintf("syzkaller_pf_%d", inst.SSHOptions.Port) + natArg := fmt.Sprintf("%s,tcp,,%d,,22", rule, inst.SSHOptions.Port) + if inst.debug { + log.Logf(0, "setting NAT rule %q", natArg) + } + if _, err := osutil.RunCmd(2*time.Minute, "", "VBoxManage", + "modifyvm", inst.vmName, "--natpf1", natArg); err != nil { + if inst.debug { + log.Logf(0, "VBoxManage modifyvm --natpf1 failed: %v", err) + } + return err + } + if inst.debug { + log.Logf(0, "SSH NAT forwarding: host 127.0.0.1:%d -> guest:22", inst.SSHOptions.Port) + } + + serialDir := filepath.Dir(inst.serialPath) + if inst.debug { + log.Logf(0, "ensuring serial parent directory exists: %s", serialDir) + } + if err := os.MkdirAll(serialDir, 0755); err != nil { + return fmt.Errorf("failed to create serial directory %s: %w", serialDir, err) + } + if inst.debug { + log.Logf(0, "enabling UART on VM %q (0x3F8/IRQ4) and piping to %s", inst.vmName, inst.serialPath) + } + if _, err := osutil.RunCmd(2*time.Minute, "", "VBoxManage", + "modifyvm", inst.vmName, "--uart1", "0x3F8", "4"); err != nil { + if inst.debug { + log.Logf(0, "VBoxManage modifyvm --uart1 failed: %v", err) + } + return err + } + if _, err := osutil.RunCmd(2*time.Minute, "", "VBoxManage", + "modifyvm", inst.vmName, "--uart-mode1", "server", inst.serialPath); err != nil { + if inst.debug { + log.Logf(0, "VBoxManage modifyvm --uart-mode1 failed: %v", err) + } + return err + } + + return nil +} + +func (inst *instance) boot() error { + if inst.debug { + log.Logf(0, "booting VM %q (headless)", inst.vmName) + } + if _, err := osutil.RunCmd(2*time.Minute, "", "VBoxManage", + "startvm", inst.vmName, "--type", "headless"); err != nil { + if inst.debug { + log.Logf(0, "VBoxManage startvm failed: %v", err) + } + return err + } + + var tee io.Writer + if inst.debug { + tee = os.Stdout + } + inst.merger = vmimpl.NewOutputMerger(tee) + inst.merger.Add("virtualbox", inst.rpipe) + inst.rpipe = nil + + // Connect to the serial console and add it to the merger. + var err error + inst.uartConn, err = net.Dial("unix", inst.serialPath) + if err != nil { + if inst.debug { + log.Logf(0, "failed to connect to serial socket %s: %v", inst.serialPath, err) + } + return err + } + inst.merger.Add("dmesg", inst.uartConn) + + var bootOutput []byte + bootOutputStop := make(chan bool) + go func() { + for { + select { + case out := <-inst.merger.Output: + bootOutput = append(bootOutput, out...) + case <-bootOutputStop: + close(bootOutputStop) + return + } + } + }() + if err := vmimpl.WaitForSSH(10*time.Minute*inst.timeouts.Scale, inst.SSHOptions, + inst.os, inst.merger.Err, false, inst.debug); err != nil { + bootOutputStop <- true + <-bootOutputStop + return vmimpl.MakeBootError(err, bootOutput) + } + bootOutputStop <- true + + return nil +} + +func (inst *instance) Forward(port int) (string, error) { + if inst.rpcPort != 0 { + return "", fmt.Errorf("isolated: Forward port already set") + } + if port == 0 { + return "", fmt.Errorf("isolated: Forward port is zero") + } + inst.rpcPort = port + return fmt.Sprintf("127.0.0.1:%d", port), nil +} + +func (inst *instance) Close() error { + if inst.debug { + log.Logf(0, "stopping %v", inst.vmName) + } + osutil.RunCmd(2*time.Minute, "", "VBoxManage", "controlvm", inst.vmName, "poweroff") + if inst.debug { + log.Logf(0, "deleting %v", inst.vmName) + } + osutil.RunCmd(2*time.Minute, "", "VBoxManage", "unregistervm", inst.vmName, "--delete") + close(inst.closed) + if inst.rpipe != nil { + inst.rpipe.Close() + } + if inst.wpipe != nil { + inst.wpipe.Close() + } + return nil +} + +func (inst *instance) Copy(hostSrc string) (string, error) { + base := filepath.Base(hostSrc) + vmDest := "/" + base + + args := vmimpl.SCPArgs(inst.debug, inst.SSHOptions.Key, inst.SSHOptions.Port, false) + args = append(args, hostSrc, fmt.Sprintf("%v@127.0.0.1:%v", inst.SSHOptions.User, vmDest)) + + if inst.debug { + log.Logf(0, "running command: scp %#v", args) + } + + if _, err := osutil.RunCmd(3*time.Minute, "", "scp", args...); err != nil { + return "", err + } + return vmDest, nil +} + +func (inst *instance) Run(ctx context.Context, command string) ( + <-chan []byte, <-chan error, error) { + if inst.uartConn == nil { + if inst.debug { + log.Logf(0, "serial console not available; returning an error") + } + return nil, nil, fmt.Errorf("serial console not available") + } + args := vmimpl.SSHArgs(inst.debug, inst.SSHOptions.Key, inst.SSHOptions.Port, false) + if inst.rpcPort != 0 { + proxy := fmt.Sprintf("%d:127.0.0.1:%d", inst.rpcPort, inst.rpcPort) + args = append(args, "-R", proxy) + } + + args = append(args, fmt.Sprintf("%v@127.0.0.1", inst.SSHOptions.User), fmt.Sprintf("cd / && exec %v", command)) + if inst.debug { + log.Logf(0, "running command: ssh %#v", args) + } + cmd := osutil.Command("ssh", args...) + rpipe, wpipe, err := osutil.LongPipe() + if err != nil { + if inst.debug { + log.Logf(0, "LongPipe failed: %v", err) + } + if inst.uartConn != nil { + inst.uartConn.Close() + } + return nil, nil, err + } + cmd.Stdout = wpipe + cmd.Stderr = wpipe + if err := cmd.Start(); err != nil { + wpipe.Close() + rpipe.Close() + if inst.uartConn != nil { + inst.uartConn.Close() + } + return nil, nil, err + } + wpipe.Close() + + inst.merger.Add("ssh", rpipe) + + return vmimpl.Multiplex(ctx, cmd, inst.merger, vmimpl.MultiplexConfig{ + Console: inst.uartConn, + Close: inst.closed, + Debug: inst.debug, + Scale: inst.timeouts.Scale, + }) +} + +func (inst *instance) Diagnose(rep *report.Report) ([]byte, bool) { + return nil, false +} diff --git a/vm/vm.go b/vm/vm.go index 42d758517..b800532d0 100644 --- a/vm/vm.go +++ b/vm/vm.go @@ -40,6 +40,7 @@ import ( _ "github.com/google/syzkaller/vm/proxyapp" _ "github.com/google/syzkaller/vm/qemu" _ "github.com/google/syzkaller/vm/starnix" + _ "github.com/google/syzkaller/vm/virtualbox" _ "github.com/google/syzkaller/vm/vmm" _ "github.com/google/syzkaller/vm/vmware" ) -- cgit mrf-deployment