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  }