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 }