github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/vm/gvisor/gvisor.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 gvisor provides support for gVisor, user-space kernel, testing.
     5  // See https://github.com/google/gvisor
     6  package gvisor
     7  
     8  import (
     9  	"bytes"
    10  	"context"
    11  	"fmt"
    12  	"io"
    13  	"net"
    14  	"os"
    15  	"os/exec"
    16  	"path/filepath"
    17  	"runtime"
    18  	"strings"
    19  	"syscall"
    20  	"time"
    21  
    22  	"github.com/google/syzkaller/pkg/config"
    23  	"github.com/google/syzkaller/pkg/log"
    24  	"github.com/google/syzkaller/pkg/osutil"
    25  	"github.com/google/syzkaller/pkg/report"
    26  	"github.com/google/syzkaller/sys/targets"
    27  	"github.com/google/syzkaller/vm/vmimpl"
    28  )
    29  
    30  func init() {
    31  	vmimpl.Register(targets.GVisor, vmimpl.Type{
    32  		Ctor:       ctor,
    33  		Overcommit: true,
    34  	})
    35  }
    36  
    37  type Config struct {
    38  	Count            int    `json:"count"` // number of VMs to use
    39  	RunscArgs        string `json:"runsc_args"`
    40  	MemoryTotalBytes uint64 `json:"memory_total_bytes"`
    41  	CPUs             uint64 `json:"cpus"`
    42  }
    43  
    44  type Pool struct {
    45  	env *vmimpl.Env
    46  	cfg *Config
    47  }
    48  
    49  type instance struct {
    50  	cfg      *Config
    51  	image    string
    52  	debug    bool
    53  	rootDir  string
    54  	imageDir string
    55  	name     string
    56  	port     int
    57  	cmd      *exec.Cmd
    58  	merger   *vmimpl.OutputMerger
    59  }
    60  
    61  func ctor(env *vmimpl.Env) (vmimpl.Pool, error) {
    62  	cfg := &Config{
    63  		Count: 1,
    64  	}
    65  	if err := config.LoadData(env.Config, cfg); err != nil {
    66  		return nil, fmt.Errorf("failed to parse vm config: %w", err)
    67  	}
    68  	if cfg.Count < 1 || cfg.Count > 128 {
    69  		return nil, fmt.Errorf("invalid config param count: %v, want [1, 128]", cfg.Count)
    70  	}
    71  	hostTotalMemory := osutil.SystemMemorySize()
    72  	minMemory := uint64(cfg.Count) * 10_000_000
    73  	if cfg.MemoryTotalBytes != 0 && (cfg.MemoryTotalBytes < minMemory || cfg.MemoryTotalBytes > hostTotalMemory) {
    74  		return nil, fmt.Errorf("invalid config param memory_total_bytes: %v, want [%d,%d]",
    75  			minMemory, cfg.MemoryTotalBytes, hostTotalMemory)
    76  	}
    77  	if !osutil.IsExist(env.Image) {
    78  		return nil, fmt.Errorf("image file %q does not exist", env.Image)
    79  	}
    80  	pool := &Pool{
    81  		cfg: cfg,
    82  		env: env,
    83  	}
    84  	return pool, nil
    85  }
    86  
    87  func (pool *Pool) Count() int {
    88  	return pool.cfg.Count
    89  }
    90  
    91  func (pool *Pool) Create(_ context.Context, workdir string, index int) (vmimpl.Instance, error) {
    92  	rootDir := filepath.Clean(filepath.Join(workdir, "..", "gvisor_root"))
    93  	imageDir := filepath.Join(workdir, "image")
    94  	bundleDir := filepath.Join(workdir, "bundle")
    95  	osutil.MkdirAll(rootDir)
    96  	osutil.MkdirAll(bundleDir)
    97  	osutil.MkdirAll(imageDir)
    98  
    99  	caps := ""
   100  	for _, c := range sandboxCaps {
   101  		if caps != "" {
   102  			caps += ", "
   103  		}
   104  		caps += "\"" + c + "\""
   105  	}
   106  	name := fmt.Sprintf("%v-%v", pool.env.Name, index)
   107  	memoryLimit := int64(pool.cfg.MemoryTotalBytes / uint64(pool.Count()))
   108  	if pool.cfg.MemoryTotalBytes == 0 {
   109  		memoryLimit = -1
   110  	}
   111  	cpuPeriod := uint64(100000)
   112  	cpus := pool.cfg.CPUs
   113  	if cpus == 0 {
   114  		cpus = uint64(runtime.NumCPU())
   115  	}
   116  	cpuLimit := cpuPeriod * cpus
   117  	vmConfig := fmt.Sprintf(configTempl, imageDir, caps, name, memoryLimit, cpuLimit, cpuPeriod)
   118  	if err := osutil.WriteFile(filepath.Join(bundleDir, "config.json"), []byte(vmConfig)); err != nil {
   119  		return nil, err
   120  	}
   121  	bin, err := exec.LookPath(os.Args[0])
   122  	if err != nil {
   123  		return nil, fmt.Errorf("failed to lookup %v: %w", os.Args[0], err)
   124  	}
   125  	if err := osutil.CopyFile(bin, filepath.Join(imageDir, "init")); err != nil {
   126  		return nil, err
   127  	}
   128  
   129  	panicLog := filepath.Join(bundleDir, "panic.fifo")
   130  	if err := syscall.Mkfifo(panicLog, 0666); err != nil {
   131  		return nil, err
   132  	}
   133  	defer syscall.Unlink(panicLog)
   134  
   135  	// Open the fifo for read-write to be able to open for read-only
   136  	// without blocking.
   137  	panicLogWriteFD, err := os.OpenFile(panicLog, os.O_RDWR, 0)
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  	defer panicLogWriteFD.Close()
   142  
   143  	panicLogReadFD, err := os.Open(panicLog)
   144  	if err != nil {
   145  		return nil, err
   146  	}
   147  
   148  	rpipe, wpipe, err := osutil.LongPipe()
   149  	if err != nil {
   150  		panicLogReadFD.Close()
   151  		return nil, err
   152  	}
   153  	var tee io.Writer
   154  	if pool.env.Debug {
   155  		tee = os.Stdout
   156  	}
   157  	merger := vmimpl.NewOutputMerger(tee)
   158  	merger.Add("runsc", rpipe)
   159  	merger.Add("runsc-goruntime", panicLogReadFD)
   160  
   161  	inst := &instance{
   162  		cfg:      pool.cfg,
   163  		image:    pool.env.Image,
   164  		debug:    pool.env.Debug,
   165  		rootDir:  rootDir,
   166  		imageDir: imageDir,
   167  		name:     name,
   168  		merger:   merger,
   169  	}
   170  
   171  	// Kill the previous instance in case it's still running.
   172  	osutil.Run(time.Minute, inst.runscCmd("delete", "-force", inst.name))
   173  	time.Sleep(3 * time.Second)
   174  
   175  	cmd := inst.runscCmd("--panic-log", panicLog, "--cpu-num-from-quota", "run", "-bundle", bundleDir, inst.name)
   176  	cmd.Stdout = wpipe
   177  	cmd.Stderr = wpipe
   178  	if err := cmd.Start(); err != nil {
   179  		wpipe.Close()
   180  		panicLogWriteFD.Close()
   181  		merger.Wait()
   182  		return nil, err
   183  	}
   184  	inst.cmd = cmd
   185  	wpipe.Close()
   186  
   187  	if err := inst.waitBoot(); err != nil {
   188  		panicLogWriteFD.Close()
   189  		inst.Close()
   190  		return nil, err
   191  	}
   192  	return inst, nil
   193  }
   194  
   195  func (inst *instance) waitBoot() error {
   196  	errorMsg := []byte("FATAL ERROR:")
   197  	bootedMsg := []byte(initStartMsg)
   198  	timeout := time.NewTimer(time.Minute)
   199  	defer timeout.Stop()
   200  	var output []byte
   201  	for {
   202  		select {
   203  		case out := <-inst.merger.Output:
   204  			output = append(output, out...)
   205  			if pos := bytes.Index(output, errorMsg); pos != -1 {
   206  				end := bytes.IndexByte(output[pos:], '\n')
   207  				if end == -1 {
   208  					end = len(output)
   209  				} else {
   210  					end += pos
   211  				}
   212  				return vmimpl.BootError{
   213  					Title:  string(output[pos:end]),
   214  					Output: output,
   215  				}
   216  			}
   217  			if bytes.Contains(output, bootedMsg) {
   218  				return nil
   219  			}
   220  		case err := <-inst.merger.Err:
   221  			return vmimpl.BootError{
   222  				Title:  fmt.Sprintf("runsc failed: %v", err),
   223  				Output: output,
   224  			}
   225  		case <-timeout.C:
   226  			return vmimpl.BootError{
   227  				Title:  "init process did not start",
   228  				Output: output,
   229  			}
   230  		}
   231  	}
   232  }
   233  
   234  func (inst *instance) args() []string {
   235  	args := []string{
   236  		"-root", inst.rootDir,
   237  		"-watchdog-action=panic",
   238  		"-network=none",
   239  		"-debug",
   240  		// Send debug logs to stderr, so that they will be picked up by
   241  		// syzkaller. Without this, debug logs are sent to /dev/null.
   242  		"-debug-log=/dev/stderr",
   243  	}
   244  	if inst.cfg.RunscArgs != "" {
   245  		args = append(args, strings.Split(inst.cfg.RunscArgs, " ")...)
   246  	}
   247  	return args
   248  }
   249  
   250  func (inst *instance) Info() ([]byte, error) {
   251  	info := fmt.Sprintf("%v %v\n", inst.image, strings.Join(inst.args(), " "))
   252  	return []byte(info), nil
   253  }
   254  
   255  func (inst *instance) runscCmd(add ...string) *exec.Cmd {
   256  	cmd := osutil.Command(inst.image, append(inst.args(), add...)...)
   257  	cmd.Env = []string{
   258  		"GOTRACEBACK=all",
   259  		"GORACE=halt_on_error=1",
   260  		// New glibc-s enable rseq by default but ptrace and systrap
   261  		// platforms don't work in this case. runsc is linked with libc
   262  		// only when the race detector is enabled.
   263  		"GLIBC_TUNABLES=glibc.pthread.rseq=0",
   264  	}
   265  	return cmd
   266  }
   267  
   268  func (inst *instance) Close() error {
   269  	time.Sleep(3 * time.Second)
   270  	osutil.Run(time.Minute, inst.runscCmd("delete", "-force", inst.name))
   271  	inst.cmd.Process.Kill()
   272  	inst.merger.Wait()
   273  	inst.cmd.Wait()
   274  	osutil.Run(time.Minute, inst.runscCmd("delete", "-force", inst.name))
   275  	time.Sleep(3 * time.Second)
   276  	return nil
   277  }
   278  
   279  func (inst *instance) Forward(port int) (string, error) {
   280  	if inst.port != 0 {
   281  		return "", fmt.Errorf("forward port is already setup")
   282  	}
   283  	inst.port = port
   284  	return "stdin:0", nil
   285  }
   286  
   287  func (inst *instance) Copy(hostSrc string) (string, error) {
   288  	fname := filepath.Base(hostSrc)
   289  	if err := osutil.CopyFile(hostSrc, filepath.Join(inst.imageDir, fname)); err != nil {
   290  		return "", err
   291  	}
   292  	if err := os.Chmod(inst.imageDir, 0777); err != nil {
   293  		return "", err
   294  	}
   295  	return filepath.Join("/", fname), nil
   296  }
   297  
   298  func (inst *instance) Run(ctx context.Context, command string) (
   299  	<-chan []byte, <-chan error, error) {
   300  	args := []string{"exec", "-user=0:0"}
   301  	for _, c := range sandboxCaps {
   302  		args = append(args, "-cap", c)
   303  	}
   304  	args = append(args, inst.name)
   305  	args = append(args, strings.Split(command, " ")...)
   306  	cmd := inst.runscCmd(args...)
   307  
   308  	rpipe, wpipe, err := osutil.LongPipe()
   309  	if err != nil {
   310  		return nil, nil, err
   311  	}
   312  	defer wpipe.Close()
   313  	inst.merger.Add("cmd", rpipe)
   314  	cmd.Stdout = wpipe
   315  	cmd.Stderr = wpipe
   316  
   317  	guestSock, err := inst.guestProxy()
   318  	if err != nil {
   319  		return nil, nil, err
   320  	}
   321  	if guestSock != nil {
   322  		defer guestSock.Close()
   323  		cmd.Stdin = guestSock
   324  	}
   325  
   326  	if err := cmd.Start(); err != nil {
   327  		return nil, nil, err
   328  	}
   329  	errc := make(chan error, 1)
   330  	signal := func(err error) {
   331  		select {
   332  		case errc <- err:
   333  		default:
   334  		}
   335  	}
   336  
   337  	go func() {
   338  		select {
   339  		case <-ctx.Done():
   340  			signal(vmimpl.ErrTimeout)
   341  		case err := <-inst.merger.Err:
   342  			cmd.Process.Kill()
   343  			if cmdErr := cmd.Wait(); cmdErr == nil {
   344  				// If the command exited successfully, we got EOF error from merger.
   345  				// But in this case no error has happened and the EOF is expected.
   346  				err = nil
   347  			}
   348  			signal(err)
   349  			return
   350  		}
   351  		log.Logf(1, "stopping %s", inst.name)
   352  		w := make(chan bool)
   353  		go func() {
   354  			select {
   355  			case <-w:
   356  				return
   357  			case <-time.After(time.Minute):
   358  				cmd.Process.Kill()
   359  			}
   360  		}()
   361  		osutil.Run(time.Minute, inst.runscCmd("kill", inst.name, "9"))
   362  		err := cmd.Wait()
   363  		close(w)
   364  		log.Logf(1, "%s exited with %s", inst.name, err)
   365  	}()
   366  	return inst.merger.Output, errc, nil
   367  }
   368  
   369  func (inst *instance) guestProxy() (*os.File, error) {
   370  	if inst.port == 0 {
   371  		return nil, nil
   372  	}
   373  	// One does not simply let gvisor guest connect to host tcp port.
   374  	// We create a unix socket, pass it to guest in stdin.
   375  	// Guest will use it instead of dialing manager directly.
   376  	// On host we connect to manager tcp port and proxy between the tcp and unix connections.
   377  	socks, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_STREAM, 0)
   378  	if err != nil {
   379  		return nil, err
   380  	}
   381  	hostSock := os.NewFile(uintptr(socks[0]), "host unix proxy")
   382  	guestSock := os.NewFile(uintptr(socks[1]), "guest unix proxy")
   383  	conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%v", inst.port))
   384  	if err != nil {
   385  		hostSock.Close()
   386  		guestSock.Close()
   387  		return nil, err
   388  	}
   389  	go func() {
   390  		io.Copy(hostSock, conn)
   391  		hostSock.Close()
   392  	}()
   393  	go func() {
   394  		io.Copy(conn, hostSock)
   395  		conn.Close()
   396  	}()
   397  	return guestSock, nil
   398  }
   399  
   400  func (inst *instance) Diagnose(rep *report.Report) ([]byte, bool) {
   401  	// TODO: stacks and dmesg are mostly useful for hangs/stalls, so we could do this only sometimes based on rep.
   402  	b, err := osutil.Run(time.Minute, inst.runscCmd("debug", "-stacks", "--ps", inst.name))
   403  	if err != nil {
   404  		b = append(b, fmt.Sprintf("\n\nError collecting stacks: %v", err)...)
   405  	}
   406  	b1, err := osutil.RunCmd(time.Minute, "", "dmesg")
   407  	b = append(b, b1...)
   408  	if err != nil {
   409  		b = append(b, fmt.Sprintf("\n\nError collecting kernel logs: %v", err)...)
   410  	}
   411  	return b, false
   412  }
   413  
   414  func init() {
   415  	if os.Getenv("SYZ_GVISOR_PROXY") != "" {
   416  		fmt.Fprint(os.Stderr, initStartMsg)
   417  		// If we do select{}, we can get a deadlock panic.
   418  		for range time.NewTicker(time.Hour).C {
   419  		}
   420  	}
   421  }
   422  
   423  const initStartMsg = "SYZKALLER INIT STARTED\n"
   424  
   425  const configTempl = `
   426  {
   427  	"root": {
   428  		"path": "%[1]v",
   429  		"readonly": true
   430  	},
   431  	"linux": {
   432  		"cgroupsPath": "%[3]v",
   433  		"resources": {
   434  			"cpu": {
   435  				"shares": 1024,
   436  				"period": %[6]d,
   437  				"quota": %[5]d
   438  			},
   439  			"memory": {
   440  				"limit": %[4]d,
   441  				"reservation": %[4]d,
   442  				"disableOOMKiller": false
   443  			}
   444  		},
   445  		"sysctl": {
   446  			"fs.nr_open": "1048576"
   447  		}
   448  	},
   449  	"process":{
   450  		"args": ["/init"],
   451  		"cwd": "/tmp",
   452  		"env": ["SYZ_GVISOR_PROXY=1"],
   453  		"capabilities": {
   454  			"bounding": [%[2]v],
   455  			"effective": [%[2]v],
   456  			"inheritable": [%[2]v],
   457  			"permitted": [%[2]v],
   458  			"ambient": [%[2]v]
   459  		}
   460  	}
   461  }
   462  `
   463  
   464  var sandboxCaps = []string{
   465  	"CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_DAC_READ_SEARCH", "CAP_FOWNER", "CAP_FSETID",
   466  	"CAP_KILL", "CAP_SETGID", "CAP_SETUID", "CAP_SETPCAP", "CAP_LINUX_IMMUTABLE",
   467  	"CAP_NET_BIND_SERVICE", "CAP_NET_BROADCAST", "CAP_NET_ADMIN", "CAP_NET_RAW",
   468  	"CAP_IPC_LOCK", "CAP_IPC_OWNER", "CAP_SYS_MODULE", "CAP_SYS_RAWIO", "CAP_SYS_CHROOT",
   469  	"CAP_SYS_PTRACE", "CAP_SYS_PACCT", "CAP_SYS_ADMIN", "CAP_SYS_BOOT", "CAP_SYS_NICE",
   470  	"CAP_SYS_RESOURCE", "CAP_SYS_TIME", "CAP_SYS_TTY_CONFIG", "CAP_MKNOD", "CAP_LEASE",
   471  	"CAP_AUDIT_WRITE", "CAP_AUDIT_CONTROL", "CAP_SETFCAP", "CAP_MAC_OVERRIDE", "CAP_MAC_ADMIN",
   472  	"CAP_SYSLOG", "CAP_WAKE_ALARM", "CAP_BLOCK_SUSPEND", "CAP_AUDIT_READ",
   473  }