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