github.com/SagerNet/gvisor@v0.0.0-20210707092255-7731c139d75c/test/runner/main.go (about)

     1  // Copyright 2018 The gVisor Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Binary syscall_test_runner runs the syscall test suites in gVisor
    16  // containers and on the host platform.
    17  package main
    18  
    19  import (
    20  	"flag"
    21  	"fmt"
    22  	"io/ioutil"
    23  	"os"
    24  	"os/exec"
    25  	"os/signal"
    26  	"path/filepath"
    27  	"strings"
    28  	"syscall"
    29  	"testing"
    30  	"time"
    31  
    32  	specs "github.com/opencontainers/runtime-spec/specs-go"
    33  	"github.com/syndtr/gocapability/capability"
    34  	"golang.org/x/sys/unix"
    35  	"github.com/SagerNet/gvisor/pkg/log"
    36  	"github.com/SagerNet/gvisor/pkg/test/testutil"
    37  	"github.com/SagerNet/gvisor/runsc/specutils"
    38  	"github.com/SagerNet/gvisor/test/runner/gtest"
    39  	"github.com/SagerNet/gvisor/test/uds"
    40  )
    41  
    42  var (
    43  	debug              = flag.Bool("debug", false, "enable debug logs")
    44  	strace             = flag.Bool("strace", false, "enable strace logs")
    45  	platform           = flag.String("platform", "ptrace", "platform to run on")
    46  	network            = flag.String("network", "none", "network stack to run on (sandbox, host, none)")
    47  	useTmpfs           = flag.Bool("use-tmpfs", false, "mounts tmpfs for /tmp")
    48  	fileAccess         = flag.String("file-access", "exclusive", "mounts root in exclusive or shared mode")
    49  	overlay            = flag.Bool("overlay", false, "wrap filesystem mounts with writable tmpfs overlay")
    50  	vfs2               = flag.Bool("vfs2", false, "enable VFS2")
    51  	fuse               = flag.Bool("fuse", false, "enable FUSE")
    52  	container          = flag.Bool("container", false, "run tests in their own namespaces (user ns, network ns, etc), pretending to be root")
    53  	setupContainerPath = flag.String("setup-container", "", "path to setup_container binary (for use with --container)")
    54  	runscPath          = flag.String("runsc", "", "path to runsc binary")
    55  
    56  	addUDSTree = flag.Bool("add-uds-tree", false, "expose a tree of UDS utilities for use in tests")
    57  	// TODO(github.com/SagerNet/issue/4572): properly support leak checking for runsc, and
    58  	// set to true as the default for the test runner.
    59  	leakCheck = flag.Bool("leak-check", false, "check for reference leaks")
    60  )
    61  
    62  // runTestCaseNative runs the test case directly on the host machine.
    63  func runTestCaseNative(testBin string, tc gtest.TestCase, t *testing.T) {
    64  	// These tests might be running in parallel, so make sure they have a
    65  	// unique test temp dir.
    66  	tmpDir, err := ioutil.TempDir(testutil.TmpDir(), "")
    67  	if err != nil {
    68  		t.Fatalf("could not create temp dir: %v", err)
    69  	}
    70  	defer os.RemoveAll(tmpDir)
    71  
    72  	// Replace TEST_TMPDIR in the current environment with something
    73  	// unique.
    74  	env := os.Environ()
    75  	newEnvVar := "TEST_TMPDIR=" + tmpDir
    76  	var found bool
    77  	for i, kv := range env {
    78  		if strings.HasPrefix(kv, "TEST_TMPDIR=") {
    79  			env[i] = newEnvVar
    80  			found = true
    81  			break
    82  		}
    83  	}
    84  	if !found {
    85  		env = append(env, newEnvVar)
    86  	}
    87  	// Remove shard env variables so that the gunit binary does not try to
    88  	// interpret them.
    89  	env = filterEnv(env, []string{"TEST_SHARD_INDEX", "TEST_TOTAL_SHARDS", "GTEST_SHARD_INDEX", "GTEST_TOTAL_SHARDS"})
    90  
    91  	if *addUDSTree {
    92  		socketDir, cleanup, err := uds.CreateSocketTree("/tmp")
    93  		if err != nil {
    94  			t.Fatalf("failed to create socket tree: %v", err)
    95  		}
    96  		defer cleanup()
    97  
    98  		env = append(env, "TEST_UDS_TREE="+socketDir)
    99  		// On Linux, the concept of "attach" location doesn't exist.
   100  		// Just pass the same path to make these test identical.
   101  		env = append(env, "TEST_UDS_ATTACH_TREE="+socketDir)
   102  	}
   103  
   104  	cmd := exec.Command(testBin, tc.Args()...)
   105  	cmd.Env = env
   106  	cmd.Stdout = os.Stdout
   107  	cmd.Stderr = os.Stderr
   108  	cmd.SysProcAttr = &unix.SysProcAttr{}
   109  
   110  	if *container {
   111  		// setup_container takes in its target argv as positional arguments.
   112  		cmd.Path = *setupContainerPath
   113  		cmd.Args = append([]string{cmd.Path}, cmd.Args...)
   114  		cmd.SysProcAttr = &unix.SysProcAttr{
   115  			Cloneflags: unix.CLONE_NEWUSER | unix.CLONE_NEWNET | unix.CLONE_NEWIPC | unix.CLONE_NEWUTS,
   116  			// Set current user/group as root inside the namespace.
   117  			UidMappings: []syscall.SysProcIDMap{
   118  				{ContainerID: 0, HostID: os.Getuid(), Size: 1},
   119  			},
   120  			GidMappings: []syscall.SysProcIDMap{
   121  				{ContainerID: 0, HostID: os.Getgid(), Size: 1},
   122  			},
   123  			GidMappingsEnableSetgroups: false,
   124  			Credential: &syscall.Credential{
   125  				Uid: 0,
   126  				Gid: 0,
   127  			},
   128  		}
   129  	}
   130  
   131  	if specutils.HasCapabilities(capability.CAP_SYS_ADMIN) {
   132  		cmd.SysProcAttr.Cloneflags |= unix.CLONE_NEWUTS
   133  	}
   134  
   135  	if specutils.HasCapabilities(capability.CAP_NET_ADMIN) {
   136  		cmd.SysProcAttr.Cloneflags |= unix.CLONE_NEWNET
   137  	}
   138  
   139  	if err := cmd.Run(); err != nil {
   140  		ws := err.(*exec.ExitError).Sys().(syscall.WaitStatus)
   141  		t.Errorf("test %q exited with status %d, want 0", tc.FullName(), ws.ExitStatus())
   142  	}
   143  }
   144  
   145  // runRunsc runs spec in runsc in a standard test configuration.
   146  //
   147  // runsc logs will be saved to a path in TEST_UNDECLARED_OUTPUTS_DIR.
   148  //
   149  // Returns an error if the sandboxed application exits non-zero.
   150  func runRunsc(tc gtest.TestCase, spec *specs.Spec) error {
   151  	bundleDir, cleanup, err := testutil.SetupBundleDir(spec)
   152  	if err != nil {
   153  		return fmt.Errorf("SetupBundleDir failed: %v", err)
   154  	}
   155  	defer cleanup()
   156  
   157  	rootDir, cleanup, err := testutil.SetupRootDir()
   158  	if err != nil {
   159  		return fmt.Errorf("SetupRootDir failed: %v", err)
   160  	}
   161  	defer cleanup()
   162  
   163  	name := tc.FullName()
   164  	id := testutil.RandomContainerID()
   165  	log.Infof("Running test %q in container %q", name, id)
   166  	specutils.LogSpec(spec)
   167  
   168  	args := []string{
   169  		"-root", rootDir,
   170  		"-network", *network,
   171  		"-log-format=text",
   172  		"-TESTONLY-unsafe-nonroot=true",
   173  		"-net-raw=true",
   174  		fmt.Sprintf("-panic-signal=%d", unix.SIGTERM),
   175  		"-watchdog-action=panic",
   176  		"-platform", *platform,
   177  		"-file-access", *fileAccess,
   178  	}
   179  	if *overlay {
   180  		args = append(args, "-overlay")
   181  	}
   182  	if *vfs2 {
   183  		args = append(args, "-vfs2")
   184  		if *fuse {
   185  			args = append(args, "-fuse")
   186  		}
   187  	}
   188  	if *debug {
   189  		args = append(args, "-debug", "-log-packets=true")
   190  	}
   191  	if *strace {
   192  		args = append(args, "-strace")
   193  	}
   194  	if *addUDSTree {
   195  		args = append(args, "-fsgofer-host-uds")
   196  	}
   197  	if *leakCheck {
   198  		args = append(args, "-ref-leak-mode=log-names")
   199  	}
   200  
   201  	testLogDir := ""
   202  	if undeclaredOutputsDir, ok := unix.Getenv("TEST_UNDECLARED_OUTPUTS_DIR"); ok {
   203  		// Create log directory dedicated for this test.
   204  		testLogDir = filepath.Join(undeclaredOutputsDir, strings.Replace(name, "/", "_", -1))
   205  		if err := os.MkdirAll(testLogDir, 0755); err != nil {
   206  			return fmt.Errorf("could not create test dir: %v", err)
   207  		}
   208  		debugLogDir, err := ioutil.TempDir(testLogDir, "runsc")
   209  		if err != nil {
   210  			return fmt.Errorf("could not create temp dir: %v", err)
   211  		}
   212  		debugLogDir += "/"
   213  		log.Infof("runsc logs: %s", debugLogDir)
   214  		args = append(args, "-debug-log", debugLogDir)
   215  		args = append(args, "-coverage-report", debugLogDir)
   216  
   217  		// Default -log sends messages to stderr which makes reading the test log
   218  		// difficult. Instead, drop them when debug log is enabled given it's a
   219  		// better place for these messages.
   220  		args = append(args, "-log=/dev/null")
   221  	}
   222  
   223  	// Current process doesn't have CAP_SYS_ADMIN, create user namespace and run
   224  	// as root inside that namespace to get it.
   225  	rArgs := append(args, "run", "--bundle", bundleDir, id)
   226  	cmd := exec.Command(*runscPath, rArgs...)
   227  	cmd.SysProcAttr = &unix.SysProcAttr{
   228  		Cloneflags: unix.CLONE_NEWUSER | unix.CLONE_NEWNS,
   229  		// Set current user/group as root inside the namespace.
   230  		UidMappings: []syscall.SysProcIDMap{
   231  			{ContainerID: 0, HostID: os.Getuid(), Size: 1},
   232  		},
   233  		GidMappings: []syscall.SysProcIDMap{
   234  			{ContainerID: 0, HostID: os.Getgid(), Size: 1},
   235  		},
   236  		GidMappingsEnableSetgroups: false,
   237  		Credential: &syscall.Credential{
   238  			Uid: 0,
   239  			Gid: 0,
   240  		},
   241  	}
   242  	cmd.Stdout = os.Stdout
   243  	cmd.Stderr = os.Stderr
   244  	sig := make(chan os.Signal, 1)
   245  	defer close(sig)
   246  	signal.Notify(sig, unix.SIGTERM)
   247  	defer signal.Stop(sig)
   248  	go func() {
   249  		s, ok := <-sig
   250  		if !ok {
   251  			return
   252  		}
   253  		log.Warningf("%s: Got signal: %v", name, s)
   254  		done := make(chan bool, 1)
   255  		dArgs := append([]string{}, args...)
   256  		dArgs = append(dArgs, "-alsologtostderr=true", "debug", "--stacks", id)
   257  		go func(dArgs []string) {
   258  			debug := exec.Command(*runscPath, dArgs...)
   259  			debug.Stdout = os.Stdout
   260  			debug.Stderr = os.Stderr
   261  			debug.Run()
   262  			done <- true
   263  		}(dArgs)
   264  
   265  		timeout := time.After(3 * time.Second)
   266  		select {
   267  		case <-timeout:
   268  			log.Infof("runsc debug --stacks is timeouted")
   269  		case <-done:
   270  		}
   271  
   272  		log.Warningf("Send SIGTERM to the sandbox process")
   273  		dArgs = append(args, "debug",
   274  			fmt.Sprintf("--signal=%d", unix.SIGTERM),
   275  			id)
   276  		signal := exec.Command(*runscPath, dArgs...)
   277  		signal.Stdout = os.Stdout
   278  		signal.Stderr = os.Stderr
   279  		signal.Run()
   280  	}()
   281  
   282  	err = cmd.Run()
   283  	if err == nil && len(testLogDir) > 0 {
   284  		// If the test passed, then we erase the log directory. This speeds up
   285  		// uploading logs in continuous integration & saves on disk space.
   286  		os.RemoveAll(testLogDir)
   287  	}
   288  
   289  	return err
   290  }
   291  
   292  // setupUDSTree updates the spec to expose a UDS tree for gofer socket testing.
   293  func setupUDSTree(spec *specs.Spec) (cleanup func(), err error) {
   294  	socketDir, cleanup, err := uds.CreateSocketTree("/tmp")
   295  	if err != nil {
   296  		return nil, fmt.Errorf("failed to create socket tree: %v", err)
   297  	}
   298  
   299  	// Standard access to entire tree.
   300  	spec.Mounts = append(spec.Mounts, specs.Mount{
   301  		Destination: "/tmp/sockets",
   302  		Source:      socketDir,
   303  		Type:        "bind",
   304  	})
   305  
   306  	// Individial attach points for each socket to test mounts that attach
   307  	// directly to the sockets.
   308  	spec.Mounts = append(spec.Mounts, specs.Mount{
   309  		Destination: "/tmp/sockets-attach/stream/echo",
   310  		Source:      filepath.Join(socketDir, "stream/echo"),
   311  		Type:        "bind",
   312  	})
   313  	spec.Mounts = append(spec.Mounts, specs.Mount{
   314  		Destination: "/tmp/sockets-attach/stream/nonlistening",
   315  		Source:      filepath.Join(socketDir, "stream/nonlistening"),
   316  		Type:        "bind",
   317  	})
   318  	spec.Mounts = append(spec.Mounts, specs.Mount{
   319  		Destination: "/tmp/sockets-attach/seqpacket/echo",
   320  		Source:      filepath.Join(socketDir, "seqpacket/echo"),
   321  		Type:        "bind",
   322  	})
   323  	spec.Mounts = append(spec.Mounts, specs.Mount{
   324  		Destination: "/tmp/sockets-attach/seqpacket/nonlistening",
   325  		Source:      filepath.Join(socketDir, "seqpacket/nonlistening"),
   326  		Type:        "bind",
   327  	})
   328  	spec.Mounts = append(spec.Mounts, specs.Mount{
   329  		Destination: "/tmp/sockets-attach/dgram/null",
   330  		Source:      filepath.Join(socketDir, "dgram/null"),
   331  		Type:        "bind",
   332  	})
   333  
   334  	spec.Process.Env = append(spec.Process.Env, "TEST_UDS_TREE=/tmp/sockets")
   335  	spec.Process.Env = append(spec.Process.Env, "TEST_UDS_ATTACH_TREE=/tmp/sockets-attach")
   336  
   337  	return cleanup, nil
   338  }
   339  
   340  // runsTestCaseRunsc runs the test case in runsc.
   341  func runTestCaseRunsc(testBin string, tc gtest.TestCase, t *testing.T) {
   342  	// Run a new container with the test executable and filter for the
   343  	// given test suite and name.
   344  	spec := testutil.NewSpecWithArgs(append([]string{testBin}, tc.Args()...)...)
   345  
   346  	// Mark the root as writeable, as some tests attempt to
   347  	// write to the rootfs, and expect EACCES, not EROFS.
   348  	spec.Root.Readonly = false
   349  
   350  	// Test spec comes with pre-defined mounts that we don't want. Reset it.
   351  	spec.Mounts = nil
   352  	testTmpDir := "/tmp"
   353  	if *useTmpfs {
   354  		// Forces '/tmp' to be mounted as tmpfs, otherwise test that rely on
   355  		// features only available in gVisor's internal tmpfs may fail.
   356  		spec.Mounts = append(spec.Mounts, specs.Mount{
   357  			Destination: "/tmp",
   358  			Type:        "tmpfs",
   359  		})
   360  	} else {
   361  		// Use a gofer-backed directory as '/tmp'.
   362  		//
   363  		// Tests might be running in parallel, so make sure each has a
   364  		// unique test temp dir.
   365  		//
   366  		// Some tests (e.g., sticky) access this mount from other
   367  		// users, so make sure it is world-accessible.
   368  		tmpDir, err := ioutil.TempDir(testutil.TmpDir(), "")
   369  		if err != nil {
   370  			t.Fatalf("could not create temp dir: %v", err)
   371  		}
   372  		defer os.RemoveAll(tmpDir)
   373  
   374  		if err := os.Chmod(tmpDir, 0777); err != nil {
   375  			t.Fatalf("could not chmod temp dir: %v", err)
   376  		}
   377  
   378  		// "/tmp" is not replaced with a tmpfs mount inside the sandbox
   379  		// when it's not empty. This ensures that testTmpDir uses gofer
   380  		// in exclusive mode.
   381  		testTmpDir = tmpDir
   382  		if *fileAccess == "shared" {
   383  			// All external mounts except the root mount are shared.
   384  			spec.Mounts = append(spec.Mounts, specs.Mount{
   385  				Type:        "bind",
   386  				Destination: "/tmp",
   387  				Source:      tmpDir,
   388  			})
   389  			testTmpDir = "/tmp"
   390  		}
   391  	}
   392  
   393  	// Set environment variables that indicate we are running in gVisor with
   394  	// the given platform, network, and filesystem stack.
   395  	platformVar := "TEST_ON_GVISOR"
   396  	networkVar := "GVISOR_NETWORK"
   397  	env := append(os.Environ(), platformVar+"="+*platform, networkVar+"="+*network)
   398  	vfsVar := "GVISOR_VFS"
   399  	if *vfs2 {
   400  		env = append(env, vfsVar+"=VFS2")
   401  		fuseVar := "FUSE_ENABLED"
   402  		if *fuse {
   403  			env = append(env, fuseVar+"=TRUE")
   404  		} else {
   405  			env = append(env, fuseVar+"=FALSE")
   406  		}
   407  	} else {
   408  		env = append(env, vfsVar+"=VFS1")
   409  	}
   410  
   411  	// Remove shard env variables so that the gunit binary does not try to
   412  	// interpret them.
   413  	env = filterEnv(env, []string{"TEST_SHARD_INDEX", "TEST_TOTAL_SHARDS", "GTEST_SHARD_INDEX", "GTEST_TOTAL_SHARDS"})
   414  
   415  	// Set TEST_TMPDIR to /tmp, as some of the syscall tests require it to
   416  	// be backed by tmpfs.
   417  	env = filterEnv(env, []string{"TEST_TMPDIR"})
   418  	env = append(env, fmt.Sprintf("TEST_TMPDIR=%s", testTmpDir))
   419  
   420  	spec.Process.Env = env
   421  
   422  	if *addUDSTree {
   423  		cleanup, err := setupUDSTree(spec)
   424  		if err != nil {
   425  			t.Fatalf("error creating UDS tree: %v", err)
   426  		}
   427  		defer cleanup()
   428  	}
   429  
   430  	if err := runRunsc(tc, spec); err != nil {
   431  		t.Errorf("test %q failed with error %v, want nil", tc.FullName(), err)
   432  	}
   433  }
   434  
   435  // filterEnv returns an environment with the excluded variables removed.
   436  func filterEnv(env, exclude []string) []string {
   437  	var out []string
   438  	for _, kv := range env {
   439  		ok := true
   440  		for _, k := range exclude {
   441  			if strings.HasPrefix(kv, k+"=") {
   442  				ok = false
   443  				break
   444  			}
   445  		}
   446  		if ok {
   447  			out = append(out, kv)
   448  		}
   449  	}
   450  	return out
   451  }
   452  
   453  func fatalf(s string, args ...interface{}) {
   454  	fmt.Fprintf(os.Stderr, s+"\n", args...)
   455  	os.Exit(1)
   456  }
   457  
   458  func matchString(a, b string) (bool, error) {
   459  	return a == b, nil
   460  }
   461  
   462  func main() {
   463  	flag.Parse()
   464  	if flag.NArg() != 1 {
   465  		fatalf("test must be provided")
   466  	}
   467  	testBin := flag.Args()[0] // Only argument.
   468  
   469  	log.SetLevel(log.Info)
   470  	if *debug {
   471  		log.SetLevel(log.Debug)
   472  	}
   473  
   474  	if *platform != "native" && *runscPath == "" {
   475  		if err := testutil.ConfigureExePath(); err != nil {
   476  			panic(err.Error())
   477  		}
   478  		*runscPath = specutils.ExePath
   479  	}
   480  	if *container && *setupContainerPath == "" {
   481  		setupContainer, err := testutil.FindFile("test/runner/setup_container/setup_container")
   482  		if err != nil {
   483  			fatalf("cannot find setup_container: %v", err)
   484  		}
   485  		*setupContainerPath = setupContainer
   486  	}
   487  
   488  	// Make sure stdout and stderr are opened with O_APPEND, otherwise logs
   489  	// from outside the sandbox can (and will) stomp on logs from inside
   490  	// the sandbox.
   491  	for _, f := range []*os.File{os.Stdout, os.Stderr} {
   492  		flags, err := unix.FcntlInt(f.Fd(), unix.F_GETFL, 0)
   493  		if err != nil {
   494  			fatalf("error getting file flags for %v: %v", f, err)
   495  		}
   496  		if flags&unix.O_APPEND == 0 {
   497  			flags |= unix.O_APPEND
   498  			if _, err := unix.FcntlInt(f.Fd(), unix.F_SETFL, flags); err != nil {
   499  				fatalf("error setting file flags for %v: %v", f, err)
   500  			}
   501  		}
   502  	}
   503  
   504  	// Get all test cases in each binary.
   505  	testCases, err := gtest.ParseTestCases(testBin, true)
   506  	if err != nil {
   507  		fatalf("ParseTestCases(%q) failed: %v", testBin, err)
   508  	}
   509  
   510  	// Get subset of tests corresponding to shard.
   511  	indices, err := testutil.TestIndicesForShard(len(testCases))
   512  	if err != nil {
   513  		fatalf("TestsForShard() failed: %v", err)
   514  	}
   515  
   516  	// Resolve the absolute path for the binary.
   517  	testBin, err = filepath.Abs(testBin)
   518  	if err != nil {
   519  		fatalf("Abs() failed: %v", err)
   520  	}
   521  
   522  	// Run the tests.
   523  	var tests []testing.InternalTest
   524  	for _, tci := range indices {
   525  		// Capture tc.
   526  		tc := testCases[tci]
   527  		tests = append(tests, testing.InternalTest{
   528  			Name: fmt.Sprintf("%s_%s", tc.Suite, tc.Name),
   529  			F: func(t *testing.T) {
   530  				if *platform == "native" {
   531  					// Run the test case on host.
   532  					runTestCaseNative(testBin, tc, t)
   533  				} else {
   534  					// Run the test case in runsc.
   535  					runTestCaseRunsc(testBin, tc, t)
   536  				}
   537  			},
   538  		})
   539  	}
   540  
   541  	testing.Main(matchString, tests, nil, nil)
   542  }