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 }