github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/vm/isolated/isolated.go (about) 1 // Copyright 2017 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 isolated 5 6 import ( 7 "bytes" 8 "fmt" 9 "io" 10 "os" 11 "path/filepath" 12 "strconv" 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 const pstoreConsoleFile = "/sys/fs/pstore/console-ramoops-0" 24 25 func init() { 26 vmimpl.Register("isolated", ctor, false) 27 } 28 29 type Config struct { 30 Host string `json:"host"` // host ip addr 31 Targets []string `json:"targets"` // target machines: (hostname|ip)(:port)? 32 TargetDir string `json:"target_dir"` // directory to copy/run on target 33 TargetReboot bool `json:"target_reboot"` // reboot target on repair 34 USBDevNums []string `json:"usb_device_num"` // /sys/bus/usb/devices/ 35 StartupScript string `json:"startup_script"` // script to execute after each startup 36 Pstore bool `json:"pstore"` // use crashlogs from pstore 37 SystemSSHCfg bool `json:"system_ssh_cfg"` // whether to allow system-wide SSH configuration 38 } 39 40 type Pool struct { 41 env *vmimpl.Env 42 cfg *Config 43 } 44 45 type instance struct { 46 cfg *Config 47 os string 48 targetAddr string 49 targetPort int 50 index int 51 closed chan bool 52 debug bool 53 sshUser string 54 sshKey string 55 forwardPort int 56 } 57 58 func ctor(env *vmimpl.Env) (vmimpl.Pool, error) { 59 cfg := &Config{} 60 if err := config.LoadData(env.Config, cfg); err != nil { 61 return nil, err 62 } 63 if cfg.Host == "" { 64 cfg.Host = "127.0.0.1" 65 } 66 if len(cfg.Targets) == 0 { 67 return nil, fmt.Errorf("config param targets is empty") 68 } 69 if cfg.TargetDir == "" { 70 return nil, fmt.Errorf("config param target_dir is empty") 71 } 72 for _, target := range cfg.Targets { 73 if _, _, err := splitTargetPort(target); err != nil { 74 return nil, fmt.Errorf("bad target %q: %w", target, err) 75 } 76 } 77 if len(cfg.USBDevNums) > 0 { 78 if len(cfg.USBDevNums) != len(cfg.Targets) { 79 return nil, fmt.Errorf("the number of Targets and the number of USBDevNums should be same") 80 } 81 } 82 if env.Debug && len(cfg.Targets) > 1 { 83 log.Logf(0, "limiting number of targets from %v to 1 in debug mode", len(cfg.Targets)) 84 cfg.Targets = cfg.Targets[:1] 85 if len(cfg.USBDevNums) > 1 { 86 cfg.USBDevNums = cfg.USBDevNums[:1] 87 } 88 } 89 pool := &Pool{ 90 cfg: cfg, 91 env: env, 92 } 93 return pool, nil 94 } 95 96 func (pool *Pool) Count() int { 97 return len(pool.cfg.Targets) 98 } 99 100 func (pool *Pool) Create(workdir string, index int) (vmimpl.Instance, error) { 101 targetAddr, targetPort, _ := splitTargetPort(pool.cfg.Targets[index]) 102 inst := &instance{ 103 cfg: pool.cfg, 104 os: pool.env.OS, 105 targetAddr: targetAddr, 106 targetPort: targetPort, 107 index: index, 108 closed: make(chan bool), 109 debug: pool.env.Debug, 110 sshUser: pool.env.SSHUser, 111 sshKey: pool.env.SSHKey, 112 } 113 closeInst := inst 114 defer func() { 115 if closeInst != nil { 116 closeInst.Close() 117 } 118 }() 119 if err := inst.repair(); err != nil { 120 return nil, fmt.Errorf("repair failed: %w", err) 121 } 122 123 // Remount to writable. 124 inst.ssh("mount -o remount,rw /") 125 126 // Create working dir if doesn't exist. 127 inst.ssh("mkdir -p '" + inst.cfg.TargetDir + "'") 128 129 // Remove temp files from previous runs. 130 inst.ssh("rm -rf '" + filepath.Join(inst.cfg.TargetDir, "*") + "'") 131 132 // Remove pstore files from previous runs. 133 if inst.cfg.Pstore { 134 inst.ssh(fmt.Sprintf("rm %v", pstoreConsoleFile)) 135 } 136 137 closeInst = nil 138 return inst, nil 139 } 140 141 func (inst *instance) Forward(port int) (string, error) { 142 if inst.forwardPort != 0 { 143 return "", fmt.Errorf("isolated: Forward port already set") 144 } 145 if port == 0 { 146 return "", fmt.Errorf("isolated: Forward port is zero") 147 } 148 inst.forwardPort = port 149 return fmt.Sprintf(inst.cfg.Host+":%v", port), nil 150 } 151 152 func (inst *instance) ssh(command string) error { 153 if inst.debug { 154 log.Logf(0, "executing ssh %+v", command) 155 } 156 157 rpipe, wpipe, err := osutil.LongPipe() 158 if err != nil { 159 return err 160 } 161 // TODO(dvyukov): who is closing rpipe? 162 163 args := append(vmimpl.SSHArgs(inst.debug, inst.sshKey, inst.targetPort, inst.cfg.SystemSSHCfg), 164 inst.sshUser+"@"+inst.targetAddr, command) 165 if inst.debug { 166 log.Logf(0, "running command: ssh %#v", args) 167 } 168 cmd := osutil.Command("ssh", args...) 169 cmd.Stdout = wpipe 170 cmd.Stderr = wpipe 171 if err := cmd.Start(); err != nil { 172 wpipe.Close() 173 return err 174 } 175 wpipe.Close() 176 177 done := make(chan bool) 178 go func() { 179 select { 180 case <-time.After(time.Second * 30): 181 if inst.debug { 182 log.Logf(0, "ssh hanged") 183 } 184 cmd.Process.Kill() 185 case <-done: 186 } 187 }() 188 if err := cmd.Wait(); err != nil { 189 close(done) 190 out, _ := io.ReadAll(rpipe) 191 if inst.debug { 192 log.Logf(0, "ssh failed: %v\n%s", err, out) 193 } 194 return fmt.Errorf("ssh %+v failed: %w\n%s", args, err, out) 195 } 196 close(done) 197 if inst.debug { 198 log.Logf(0, "ssh returned") 199 } 200 return nil 201 } 202 203 func (inst *instance) waitRebootAndSSH(rebootTimeout int, sshTimeout time.Duration) error { 204 if err := inst.waitForReboot(rebootTimeout); err != nil { 205 log.Logf(2, "isolated: machine did not reboot") 206 return err 207 } 208 log.Logf(2, "isolated: rebooted wait for comeback") 209 if err := inst.waitForSSH(sshTimeout); err != nil { 210 log.Logf(2, "isolated: machine did not comeback") 211 return err 212 } 213 log.Logf(2, "isolated: reboot succeeded") 214 return nil 215 } 216 217 func (inst *instance) repair() error { 218 log.Logf(2, "isolated: trying to ssh") 219 if err := inst.waitForSSH(30 * time.Minute); err != nil { 220 log.Logf(2, "isolated: ssh failed") 221 return fmt.Errorf("SSH failed") 222 } 223 if inst.cfg.TargetReboot { 224 if len(inst.cfg.USBDevNums) > 0 { 225 log.Logf(2, "isolated: trying to reboot by USB authorization") 226 usbAuth := fmt.Sprintf("%s%s%s", "/sys/bus/usb/devices/", inst.cfg.USBDevNums[inst.index], "/authorized") 227 if err := os.WriteFile(usbAuth, []byte("0"), 0); err != nil { 228 log.Logf(2, "isolated: failed to turn off the device") 229 return err 230 } 231 if err := os.WriteFile(usbAuth, []byte("1"), 0); err != nil { 232 log.Logf(2, "isolated: failed to turn on the device") 233 return err 234 } 235 } else { 236 log.Logf(2, "isolated: ssh succeeded, trying to reboot by ssh") 237 inst.ssh("reboot") // reboot will return an error, ignore it 238 if err := inst.waitRebootAndSSH(5*60, 30*time.Minute); err != nil { 239 return fmt.Errorf("waitRebootAndSSH failed: %w", err) 240 } 241 } 242 } 243 if inst.cfg.StartupScript != "" { 244 log.Logf(2, "isolated: executing startup_script") 245 // Execute the contents of the StartupScript on the DUT. 246 contents, err := os.ReadFile(inst.cfg.StartupScript) 247 if err != nil { 248 return fmt.Errorf("unable to read startup_script: %w", err) 249 } 250 c := string(contents) 251 if err := inst.ssh(fmt.Sprintf("bash -c \"%v\"", vmimpl.EscapeDoubleQuotes(c))); err != nil { 252 return fmt.Errorf("failed to execute startup_script: %w", err) 253 } 254 log.Logf(2, "isolated: done executing startup_script") 255 } 256 return nil 257 } 258 259 func (inst *instance) waitForSSH(timeout time.Duration) error { 260 return vmimpl.WaitForSSH(inst.debug, timeout, inst.targetAddr, inst.sshKey, inst.sshUser, 261 inst.os, inst.targetPort, nil, inst.cfg.SystemSSHCfg) 262 } 263 264 func (inst *instance) waitForReboot(timeout int) error { 265 var err error 266 start := time.Now() 267 for { 268 if !vmimpl.SleepInterruptible(time.Second) { 269 return fmt.Errorf("shutdown in progress") 270 } 271 // If it fails, then the reboot started. 272 if err = inst.ssh("pwd"); err != nil { 273 return nil 274 } 275 if time.Since(start).Seconds() > float64(timeout) { 276 break 277 } 278 } 279 return fmt.Errorf("isolated: the machine did not reboot on repair") 280 } 281 282 func (inst *instance) Close() { 283 close(inst.closed) 284 } 285 286 func (inst *instance) Copy(hostSrc string) (string, error) { 287 baseName := filepath.Base(hostSrc) 288 vmDst := filepath.Join(inst.cfg.TargetDir, baseName) 289 inst.ssh("pkill -9 '" + baseName + "'; rm -f '" + vmDst + "'") 290 args := append(vmimpl.SCPArgs(inst.debug, inst.sshKey, inst.targetPort, inst.cfg.SystemSSHCfg), 291 hostSrc, inst.sshUser+"@"+inst.targetAddr+":"+vmDst) 292 cmd := osutil.Command("scp", args...) 293 if inst.debug { 294 log.Logf(0, "running command: scp %#v", args) 295 cmd.Stdout = os.Stdout 296 cmd.Stderr = os.Stdout 297 } 298 if err := cmd.Start(); err != nil { 299 return "", err 300 } 301 done := make(chan bool) 302 go func() { 303 select { 304 case <-time.After(3 * time.Minute): 305 cmd.Process.Kill() 306 case <-done: 307 } 308 }() 309 err := cmd.Wait() 310 close(done) 311 if err != nil { 312 return "", err 313 } 314 return vmDst, nil 315 } 316 317 func (inst *instance) Run(timeout time.Duration, stop <-chan bool, command string) ( 318 <-chan []byte, <-chan error, error) { 319 args := append(vmimpl.SSHArgs(inst.debug, inst.sshKey, inst.targetPort, inst.cfg.SystemSSHCfg), 320 inst.sshUser+"@"+inst.targetAddr) 321 dmesg, err := vmimpl.OpenRemoteConsole("ssh", args...) 322 if err != nil { 323 return nil, nil, err 324 } 325 326 rpipe, wpipe, err := osutil.LongPipe() 327 if err != nil { 328 dmesg.Close() 329 return nil, nil, err 330 } 331 332 args = vmimpl.SSHArgsForward(inst.debug, inst.sshKey, inst.targetPort, inst.forwardPort, inst.cfg.SystemSSHCfg) 333 if inst.cfg.Pstore { 334 args = append(args, "-o", "ServerAliveInterval=6") 335 args = append(args, "-o", "ServerAliveCountMax=5") 336 } 337 args = append(args, inst.sshUser+"@"+inst.targetAddr, "cd "+inst.cfg.TargetDir+" && exec "+command) 338 if inst.debug { 339 log.Logf(0, "running command: ssh %#v", args) 340 } 341 cmd := osutil.Command("ssh", args...) 342 cmd.Stdout = wpipe 343 cmd.Stderr = wpipe 344 if err := cmd.Start(); err != nil { 345 dmesg.Close() 346 rpipe.Close() 347 wpipe.Close() 348 return nil, nil, err 349 } 350 wpipe.Close() 351 352 var tee io.Writer 353 if inst.debug { 354 tee = os.Stdout 355 } 356 merger := vmimpl.NewOutputMerger(tee) 357 merger.Add("dmesg", dmesg) 358 merger.Add("ssh", rpipe) 359 360 return vmimpl.Multiplex(cmd, merger, dmesg, timeout, stop, inst.closed, inst.debug) 361 } 362 363 func (inst *instance) readPstoreContents() ([]byte, error) { 364 log.Logf(0, "reading pstore contents") 365 args := append(vmimpl.SSHArgs(inst.debug, inst.sshKey, inst.targetPort, inst.cfg.SystemSSHCfg), 366 inst.sshUser+"@"+inst.targetAddr, "cat "+pstoreConsoleFile+" && rm "+pstoreConsoleFile) 367 if inst.debug { 368 log.Logf(0, "running command: ssh %#v", args) 369 } 370 var stdout, stderr bytes.Buffer 371 cmd := osutil.Command("ssh", args...) 372 cmd.Stdout = &stdout 373 cmd.Stderr = &stderr 374 if err := cmd.Run(); err != nil { 375 return nil, fmt.Errorf("unable to read pstore file: %w: %v", err, stderr.String()) 376 } 377 return stdout.Bytes(), nil 378 } 379 380 func (inst *instance) Diagnose(rep *report.Report) ([]byte, bool) { 381 if !inst.cfg.Pstore { 382 return nil, false 383 } 384 // TODO: kernel may not reboot after some errors. 385 // E.g. if panic_on_warn is not set, or some errors don't trigger reboot at all (e.g. LOCKDEP overflows). 386 log.Logf(2, "waiting for crashed DUT to come back up") 387 if err := inst.waitRebootAndSSH(5*60, 30*time.Minute); err != nil { 388 return []byte(fmt.Sprintf("unable to SSH into DUT after reboot: %v", err)), false 389 } 390 log.Logf(2, "reading contents of pstore") 391 contents, err := inst.readPstoreContents() 392 if err != nil { 393 return []byte(fmt.Sprintf("Diagnose failed: %v\n", err)), false 394 } 395 return contents, false 396 } 397 398 func splitTargetPort(addr string) (string, int, error) { 399 target := addr 400 port := 22 401 if colonPos := strings.Index(addr, ":"); colonPos != -1 { 402 p, err := strconv.ParseUint(addr[colonPos+1:], 10, 16) 403 if err != nil { 404 return "", 0, err 405 } 406 target = addr[:colonPos] 407 port = int(p) 408 } 409 if target == "" { 410 return "", 0, fmt.Errorf("target is empty") 411 } 412 return target, port, nil 413 }