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