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