github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/vm/vmm/vmm.go (about) 1 // Copyright 2018 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 vmm provides VMs based on OpenBSD vmm virtualization. 5 package vmm 6 7 import ( 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 // Locates the VM id which is used for VM address. 25 var vmctlStatusRegex = regexp.MustCompile(`^\s+([0-9]+)\b.*\brunning`) 26 27 func init() { 28 vmimpl.Register("vmm", ctor, true) 29 } 30 31 type Config struct { 32 Count int `json:"count"` // number of VMs to use 33 Mem int `json:"mem"` // amount of VM memory in MBs 34 Kernel string `json:"kernel"` // kernel to boot 35 Template string `json:"template"` // vm template 36 } 37 38 type Pool struct { 39 env *vmimpl.Env 40 cfg *Config 41 } 42 43 type instance struct { 44 cfg *Config 45 image string 46 debug bool 47 os string 48 sshkey string 49 sshuser string 50 sshhost string 51 sshport int 52 merger *vmimpl.OutputMerger 53 vmName string 54 vmm *exec.Cmd 55 consolew io.WriteCloser 56 } 57 58 func ctor(env *vmimpl.Env) (vmimpl.Pool, error) { 59 cfg := &Config{ 60 Count: 1, 61 Mem: 512, 62 } 63 64 if !osutil.IsExist(env.Image) { 65 return nil, fmt.Errorf("image file '%v' does not exist", env.Image) 66 } 67 68 if err := config.LoadData(env.Config, cfg); err != nil { 69 return nil, fmt.Errorf("failed to parse vmm vm config: %w", err) 70 } 71 if cfg.Count < 1 || cfg.Count > 128 { 72 return nil, fmt.Errorf("invalid config param count: %v, want [1-128]", cfg.Count) 73 } 74 if env.Debug && cfg.Count > 1 { 75 log.Logf(0, "limiting number of VMs from %v to 1 in debug mode", cfg.Count) 76 cfg.Count = 1 77 } 78 if cfg.Mem < 128 || cfg.Mem > 1048576 { 79 return nil, fmt.Errorf("invalid config param mem: %v, want [128-1048576]", cfg.Mem) 80 } 81 if cfg.Kernel == "" { 82 return nil, fmt.Errorf("missing config param kernel") 83 } 84 if !osutil.IsExist(cfg.Kernel) { 85 return nil, fmt.Errorf("kernel '%v' does not exist", cfg.Kernel) 86 } 87 pool := &Pool{ 88 cfg: cfg, 89 env: env, 90 } 91 92 return pool, nil 93 } 94 95 func (pool *Pool) Count() int { 96 return pool.cfg.Count 97 } 98 99 func (pool *Pool) Create(workdir string, index int) (vmimpl.Instance, error) { 100 var tee io.Writer 101 if pool.env.Debug { 102 tee = os.Stdout 103 } 104 inst := &instance{ 105 cfg: pool.cfg, 106 image: filepath.Join(workdir, "disk.qcow2"), 107 debug: pool.env.Debug, 108 os: pool.env.OS, 109 sshkey: pool.env.SSHKey, 110 sshuser: pool.env.SSHUser, 111 sshport: 22, 112 vmName: fmt.Sprintf("%v-%v", pool.env.Name, index), 113 merger: vmimpl.NewOutputMerger(tee), 114 } 115 116 // Stop the instance from the previous run in case it's still running. 117 // This is racy even with -w flag, start periodically fails with: 118 // vmctl: start vm command failed: Operation already in progress 119 // So also sleep for a bit. 120 inst.vmctl("stop", "-f", "-w", inst.vmName) 121 time.Sleep(3 * time.Second) 122 123 createArgs := []string{ 124 "create", 125 "-b", pool.env.Image, 126 inst.image, 127 } 128 if _, err := inst.vmctl(createArgs...); err != nil { 129 return nil, err 130 } 131 132 if err := inst.Boot(); err != nil { 133 // Cleans up if Boot fails. 134 inst.Close() 135 return nil, err 136 } 137 138 return inst, nil 139 } 140 141 func (inst *instance) Boot() error { 142 outr, outw, err := osutil.LongPipe() 143 if err != nil { 144 return err 145 } 146 inr, inw, err := osutil.LongPipe() 147 if err != nil { 148 outr.Close() 149 outw.Close() 150 return err 151 } 152 startArgs := []string{ 153 "start", 154 "-b", inst.cfg.Kernel, 155 "-d", inst.image, 156 "-m", fmt.Sprintf("%vM", inst.cfg.Mem), 157 "-L", // add a local network interface 158 "-c", // connect to the console 159 } 160 if inst.cfg.Template != "" { 161 startArgs = append(startArgs, "-t", inst.cfg.Template) 162 } 163 startArgs = append(startArgs, inst.vmName) 164 if inst.debug { 165 log.Logf(0, "running command: vmctl %#v", startArgs) 166 } 167 cmd := osutil.Command("vmctl", startArgs...) 168 cmd.Stdin = inr 169 cmd.Stdout = outw 170 cmd.Stderr = outw 171 if err := cmd.Start(); err != nil { 172 outr.Close() 173 outw.Close() 174 inr.Close() 175 inw.Close() 176 return err 177 } 178 inst.vmm = cmd 179 inst.consolew = inw 180 outw.Close() 181 inr.Close() 182 inst.merger.Add("console", outr) 183 184 inst.sshhost, err = inst.lookupSSHAddress() 185 if err != nil { 186 return err 187 } 188 189 if err := vmimpl.WaitForSSH(inst.debug, 20*time.Minute, inst.sshhost, 190 inst.sshkey, inst.sshuser, inst.os, inst.sshport, nil, false); err != nil { 191 out := <-inst.merger.Output 192 return vmimpl.BootError{Title: err.Error(), Output: out} 193 } 194 return nil 195 } 196 197 func (inst *instance) lookupSSHAddress() (string, error) { 198 out, err := inst.vmctl("status", inst.vmName) 199 if err != nil { 200 return "", err 201 } 202 lines := strings.Split(out, "\n") 203 if len(lines) < 2 { 204 return "", vmimpl.InfraError{ 205 Title: "unexpected vmctl status output", 206 Output: []byte(out), 207 } 208 } 209 matches := vmctlStatusRegex.FindStringSubmatch(lines[1]) 210 if len(matches) < 2 { 211 return "", vmimpl.InfraError{ 212 Title: "unexpected vmctl status output", 213 Output: []byte(out), 214 } 215 } 216 return fmt.Sprintf("100.64.%s.3", matches[1]), nil 217 } 218 219 func (inst *instance) Close() { 220 inst.vmctl("stop", "-f", inst.vmName) 221 if inst.consolew != nil { 222 inst.consolew.Close() 223 } 224 if inst.vmm != nil { 225 inst.vmm.Process.Kill() 226 inst.vmm.Wait() 227 } 228 inst.merger.Wait() 229 } 230 231 func (inst *instance) Forward(port int) (string, error) { 232 octets := strings.Split(inst.sshhost, ".") 233 if len(octets) < 3 { 234 return "", fmt.Errorf("too few octets in hostname %v", inst.sshhost) 235 } 236 addr := fmt.Sprintf("%v.%v.%v.2:%v", octets[0], octets[1], octets[2], port) 237 return addr, nil 238 } 239 240 func (inst *instance) Copy(hostSrc string) (string, error) { 241 vmDst := filepath.Join("/root", filepath.Base(hostSrc)) 242 args := append(vmimpl.SCPArgs(inst.debug, inst.sshkey, inst.sshport, false), 243 hostSrc, inst.sshuser+"@"+inst.sshhost+":"+vmDst) 244 if inst.debug { 245 log.Logf(0, "running command: scp %#v", args) 246 } 247 _, err := osutil.RunCmd(10*time.Minute, "", "scp", args...) 248 if err != nil { 249 return "", err 250 } 251 return vmDst, nil 252 } 253 254 func (inst *instance) Run(timeout time.Duration, stop <-chan bool, command string) ( 255 <-chan []byte, <-chan error, error) { 256 rpipe, wpipe, err := osutil.LongPipe() 257 if err != nil { 258 return nil, nil, err 259 } 260 inst.merger.Add("ssh", rpipe) 261 262 args := append(vmimpl.SSHArgs(inst.debug, inst.sshkey, inst.sshport, false), 263 inst.sshuser+"@"+inst.sshhost, command) 264 if inst.debug { 265 log.Logf(0, "running command: ssh %#v", args) 266 } 267 cmd := osutil.Command("ssh", args...) 268 cmd.Stdout = wpipe 269 cmd.Stderr = wpipe 270 if err := cmd.Start(); err != nil { 271 wpipe.Close() 272 return nil, nil, err 273 } 274 wpipe.Close() 275 errc := make(chan error, 1) 276 signal := func(err error) { 277 select { 278 case errc <- err: 279 default: 280 } 281 } 282 283 go func() { 284 select { 285 case <-time.After(timeout): 286 signal(vmimpl.ErrTimeout) 287 case <-stop: 288 signal(vmimpl.ErrTimeout) 289 case err := <-inst.merger.Err: 290 cmd.Process.Kill() 291 if cmdErr := cmd.Wait(); cmdErr == nil { 292 // If the command exited successfully, we got EOF error from merger. 293 // But in this case no error has happened and the EOF is expected. 294 err = nil 295 } 296 signal(err) 297 return 298 } 299 cmd.Process.Kill() 300 cmd.Wait() 301 }() 302 return inst.merger.Output, errc, nil 303 } 304 305 func (inst *instance) Diagnose(rep *report.Report) ([]byte, bool) { 306 return vmimpl.DiagnoseOpenBSD(inst.consolew) 307 } 308 309 // Run the given vmctl(8) command and wait for it to finish. 310 func (inst *instance) vmctl(args ...string) (string, error) { 311 if inst.debug { 312 log.Logf(0, "running command: vmctl %#v", args) 313 } 314 out, err := osutil.RunCmd(time.Minute, "", "vmctl", args...) 315 if err != nil { 316 if inst.debug { 317 log.Logf(0, "vmctl failed: %v", err) 318 } 319 return "", err 320 } 321 if inst.debug { 322 log.Logf(0, "vmctl output: %v", string(out)) 323 } 324 return string(out), nil 325 }