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  }