diff options
author | Nicolas Lacasse <nlacasse@google.com> | 2018-05-15 10:17:19 -0700 |
---|---|---|
committer | Shentubot <shentubot@google.com> | 2018-05-15 10:18:03 -0700 |
commit | 205f1027e6beb84101439172b3c776c2671b5be8 (patch) | |
tree | 10294e667ee529e140c474c475e7309cb72ea1d8 /runsc/sandbox/sandbox_test.go | |
parent | ed02ac4f668ec41063cd51cbbd451baba9e9a6e7 (diff) |
Refactor the Sandbox package into Sandbox + Container.
This is a necessary prerequisite for supporting multiple containers in a single
sandbox.
All the commands (in cmd package) now call operations on Containers (container
package). When a Container first starts, it will create a Sandbox with the same
ID.
The Sandbox class is now simpler, as it only knows how to create boot/gofer
processes, and how to forward commands into the running boot process.
There are TODOs sprinkled around for additional support for multiple
containers. Most notably, we need to detect when a container is intended to run
in an existing sandbox (by reading the metadata), and then have some way to
signal to the sandbox to start a new container. Other urpc calls into the
sandbox need to pass the container ID, so the sandbox can run the operation on
the given container. These are only half-plummed through right now.
PiperOrigin-RevId: 196688269
Change-Id: I1ecf4abbb9dd8987a53ae509df19341aaf42b5b0
Diffstat (limited to 'runsc/sandbox/sandbox_test.go')
-rw-r--r-- | runsc/sandbox/sandbox_test.go | 665 |
1 files changed, 0 insertions, 665 deletions
diff --git a/runsc/sandbox/sandbox_test.go b/runsc/sandbox/sandbox_test.go deleted file mode 100644 index 1fac38a29..000000000 --- a/runsc/sandbox/sandbox_test.go +++ /dev/null @@ -1,665 +0,0 @@ -// Copyright 2018 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package sandbox_test - -import ( - "encoding/json" - "fmt" - "io" - "io/ioutil" - "os" - "os/signal" - "path/filepath" - "reflect" - "strings" - "syscall" - "testing" - "time" - - "context" - "flag" - "github.com/google/subcommands" - specs "github.com/opencontainers/runtime-spec/specs-go" - "golang.org/x/sys/unix" - "gvisor.googlesource.com/gvisor/pkg/abi/linux" - "gvisor.googlesource.com/gvisor/pkg/log" - "gvisor.googlesource.com/gvisor/pkg/sentry/control" - "gvisor.googlesource.com/gvisor/pkg/sentry/kernel/auth" - "gvisor.googlesource.com/gvisor/pkg/unet" - "gvisor.googlesource.com/gvisor/runsc/boot" - "gvisor.googlesource.com/gvisor/runsc/cmd" - "gvisor.googlesource.com/gvisor/runsc/sandbox" -) - -func init() { - log.SetLevel(log.Debug) -} - -// writeSpec writes the spec to disk in the given directory. -func writeSpec(dir string, spec *specs.Spec) error { - b, err := json.Marshal(spec) - if err != nil { - return err - } - return ioutil.WriteFile(filepath.Join(dir, "config.json"), b, 0755) -} - -// newSpecWithArgs creates a simple spec with the given args suitable for use -// in tests. -func newSpecWithArgs(args ...string) *specs.Spec { - spec := &specs.Spec{ - // The host filesystem root is the sandbox root. - Root: &specs.Root{ - Path: "/", - Readonly: true, - }, - Process: &specs.Process{ - Args: args, - Env: []string{ - "PATH=" + os.Getenv("PATH"), - }, - }, - } - return spec -} - -// shutdownSignal will be sent to the sandbox in order to shut down cleanly. -const shutdownSignal = syscall.SIGUSR2 - -// setupSandbox creates a bundle and root dir for the sandbox, generates a test -// config, and writes the spec to config.json in the bundle dir. -func setupSandbox(spec *specs.Spec) (rootDir, bundleDir string, conf *boot.Config, err error) { - rootDir, err = ioutil.TempDir("", "sandboxes") - if err != nil { - return "", "", nil, fmt.Errorf("error creating root dir: %v", err) - } - - bundleDir, err = ioutil.TempDir("", "bundle") - if err != nil { - return "", "", nil, fmt.Errorf("error creating bundle dir: %v", err) - } - - if err = writeSpec(bundleDir, spec); err != nil { - return "", "", nil, fmt.Errorf("error writing spec: %v", err) - } - - conf = &boot.Config{ - RootDir: rootDir, - Network: boot.NetworkNone, - } - - return rootDir, bundleDir, conf, nil -} - -// uniqueSandboxID generates a unique sandbox id for each test. -// -// The sandbox id is used to create an abstract unix domain socket, which must -// be unique. While the sandbox forbids creating two sandboxes with the same -// name, sometimes between test runs the socket does not get cleaned up quickly -// enough, causing sandbox creation to fail. -func uniqueSandboxID() string { - return fmt.Sprintf("test-sandbox-%d", time.Now().UnixNano()) -} - -// waitForProcessList waits for the given process list to show up in the sandbox. -func waitForProcessList(s *sandbox.Sandbox, expected []*control.Process) error { - var got []*control.Process - for start := time.Now(); time.Now().Sub(start) < 10*time.Second; { - var err error - got, err := s.Processes() - if err != nil { - return fmt.Errorf("error getting process data from sandbox: %v", err) - } - if procListsEqual(got, expected) { - return nil - } - // Process might not have started, try again... - time.Sleep(10 * time.Millisecond) - } - return fmt.Errorf("sandbox got process list: %s, want: %s", procListToString(got), procListToString(expected)) -} - -// TestLifecycle tests the basic Create/Start/Signal/Destroy sandbox lifecycle. -// It verifies after each step that the sandbox can be loaded from disk, and -// has the correct status. -func TestLifecycle(t *testing.T) { - // The sandbox will just sleep for a long time. We will kill it before - // it finishes sleeping. - spec := newSpecWithArgs("sleep", "100") - - rootDir, bundleDir, conf, err := setupSandbox(spec) - if err != nil { - t.Fatalf("error setting up sandbox: %v", err) - } - defer os.RemoveAll(rootDir) - defer os.RemoveAll(bundleDir) - - // expectedPL lists the expected process state of the sandbox. - expectedPL := []*control.Process{ - { - UID: 0, - PID: 1, - PPID: 0, - C: 0, - Cmd: "sleep", - }, - } - // Create the sandbox. - id := uniqueSandboxID() - if _, err := sandbox.Create(id, spec, conf, bundleDir, "", "", nil); err != nil { - t.Fatalf("error creating sandbox: %v", err) - } - // Load the sandbox from disk and check the status. - s, err := sandbox.Load(rootDir, id) - if err != nil { - t.Fatalf("error loading sandbox: %v", err) - } - if got, want := s.Status, sandbox.Created; got != want { - t.Errorf("sandbox status got %v, want %v", got, want) - } - - // List should return the sandbox id. - ids, err := sandbox.List(rootDir) - if err != nil { - t.Fatalf("error listing sandboxes: %v", err) - } - if got, want := ids, []string{id}; !reflect.DeepEqual(got, want) { - t.Errorf("sandbox list got %v, want %v", got, want) - } - - // Start the sandbox. - if err := s.Start(conf); err != nil { - t.Fatalf("error starting sandbox: %v", err) - } - // Load the sandbox from disk and check the status. - s, err = sandbox.Load(rootDir, id) - if err != nil { - t.Fatalf("error loading sandbox: %v", err) - } - if got, want := s.Status, sandbox.Running; got != want { - t.Errorf("sandbox status got %v, want %v", got, want) - } - - // Verify that "sleep 100" is running. - if err := waitForProcessList(s, expectedPL); err != nil { - t.Error(err) - } - - // Send the sandbox a signal, which we catch and use to cleanly - // shutdown. - if err := s.Signal(shutdownSignal); err != nil { - t.Fatalf("error sending signal %v to sandbox: %v", shutdownSignal, err) - } - // Wait for it to die. - if _, err := s.Wait(); err != nil { - t.Fatalf("error waiting on sandbox: %v", err) - } - // Load the sandbox from disk and check the status. - s, err = sandbox.Load(rootDir, id) - if err != nil { - t.Fatalf("error loading sandbox: %v", err) - } - if got, want := s.Status, sandbox.Stopped; got != want { - t.Errorf("sandbox status got %v, want %v", got, want) - } - - // Destroy the sandbox. - if err := s.Destroy(); err != nil { - t.Fatalf("error destroying sandbox: %v", err) - } - - // List should not return the sandbox id. - ids, err = sandbox.List(rootDir) - if err != nil { - t.Fatalf("error listing sandboxes: %v", err) - } - if len(ids) != 0 { - t.Errorf("expected sandbox list to be empty, but got %v", ids) - } - - // Loading the sandbox by id should fail. - if _, err = sandbox.Load(rootDir, id); err == nil { - t.Errorf("expected loading destroyed sandbox to fail, but it did not") - } -} - -// Test the we can execute the application with different path formats. -func TestExePath(t *testing.T) { - for _, test := range []struct { - path string - success bool - }{ - {path: "true", success: true}, - {path: "bin/true", success: true}, - {path: "/bin/true", success: true}, - {path: "thisfiledoesntexit", success: false}, - {path: "bin/thisfiledoesntexit", success: false}, - {path: "/bin/thisfiledoesntexit", success: false}, - } { - spec := newSpecWithArgs(test.path) - rootDir, bundleDir, conf, err := setupSandbox(spec) - if err != nil { - t.Fatalf("exec: %s, error setting up sandbox: %v", test.path, err) - } - - ws, err := sandbox.Run(uniqueSandboxID(), spec, conf, bundleDir, "", "", nil) - - os.RemoveAll(rootDir) - os.RemoveAll(bundleDir) - - if test.success { - if err != nil { - t.Errorf("exec: %s, error running sandbox: %v", test.path, err) - } - if ws.ExitStatus() != 0 { - t.Errorf("exec: %s, got exit status %v want %v", test.path, ws.ExitStatus(), 0) - } - } else { - if err == nil { - t.Errorf("exec: %s, got: no error, want: error", test.path) - } - } - } -} - -// Test the we can retrieve the application exit status from the sandbox. -func TestAppExitStatus(t *testing.T) { - // First sandbox will succeed. - succSpec := newSpecWithArgs("true") - - rootDir, bundleDir, conf, err := setupSandbox(succSpec) - if err != nil { - t.Fatalf("error setting up sandbox: %v", err) - } - defer os.RemoveAll(rootDir) - defer os.RemoveAll(bundleDir) - - ws, err := sandbox.Run(uniqueSandboxID(), succSpec, conf, bundleDir, "", "", nil) - if err != nil { - t.Fatalf("error running sandbox: %v", err) - } - if ws.ExitStatus() != 0 { - t.Errorf("got exit status %v want %v", ws.ExitStatus(), 0) - } - - // Second sandbox exits with non-zero status. - wantStatus := 123 - errSpec := newSpecWithArgs("bash", "-c", fmt.Sprintf("exit %d", wantStatus)) - - rootDir2, bundleDir2, conf, err := setupSandbox(errSpec) - if err != nil { - t.Fatalf("error setting up sandbox: %v", err) - } - defer os.RemoveAll(rootDir2) - defer os.RemoveAll(bundleDir2) - - ws, err = sandbox.Run(uniqueSandboxID(), succSpec, conf, bundleDir2, "", "", nil) - if err != nil { - t.Fatalf("error running sandbox: %v", err) - } - if ws.ExitStatus() != wantStatus { - t.Errorf("got exit status %v want %v", ws.ExitStatus(), wantStatus) - } -} - -// TestExec verifies that a sandbox can exec a new program. -func TestExec(t *testing.T) { - const uid = 343 - spec := newSpecWithArgs("sleep", "100") - - rootDir, bundleDir, conf, err := setupSandbox(spec) - if err != nil { - t.Fatalf("error setting up sandbox: %v", err) - } - defer os.RemoveAll(rootDir) - defer os.RemoveAll(bundleDir) - - // Create and start the sandbox. - s, err := sandbox.Create(uniqueSandboxID(), spec, conf, bundleDir, "", "", nil) - if err != nil { - t.Fatalf("error creating sandbox: %v", err) - } - defer s.Destroy() - if err := s.Start(conf); err != nil { - t.Fatalf("error starting sandbox: %v", err) - } - - // expectedPL lists the expected process state of the sandbox. - expectedPL := []*control.Process{ - { - UID: 0, - PID: 1, - PPID: 0, - C: 0, - Cmd: "sleep", - }, - { - UID: uid, - PID: 2, - PPID: 0, - C: 0, - Cmd: "sleep", - }, - } - - // Verify that "sleep 100" is running. - if err := waitForProcessList(s, expectedPL[:1]); err != nil { - t.Error(err) - } - - execArgs := control.ExecArgs{ - Filename: "/bin/sleep", - Argv: []string{"sleep", "5"}, - Envv: []string{"PATH=" + os.Getenv("PATH")}, - WorkingDirectory: "/", - KUID: uid, - } - - // Verify that "sleep 100" and "sleep 5" are running after exec. - // First, start running exec (whick blocks). - status := make(chan error, 1) - go func() { - exitStatus, err := s.Execute(&execArgs) - if err != nil { - status <- err - } else if exitStatus != 0 { - status <- fmt.Errorf("failed with exit status: %v", exitStatus) - } else { - status <- nil - } - }() - - if err := waitForProcessList(s, expectedPL); err != nil { - t.Fatal(err) - } - - // Ensure that exec finished without error. - select { - case <-time.After(10 * time.Second): - t.Fatalf("sandbox timed out waiting for exec to finish.") - case st := <-status: - if st != nil { - t.Errorf("sandbox failed to exec %v: %v", execArgs, err) - } - } -} - -// TestCapabilities verifies that: -// - Running exec as non-root UID and GID will result in an error (because the -// executable file can't be read). -// - Running exec as non-root with CAP_DAC_OVERRIDE succeeds because it skips -// this check. -func TestCapabilities(t *testing.T) { - const uid = 343 - const gid = 2401 - spec := newSpecWithArgs("sleep", "100") - - // We generate files in the host temporary directory. - spec.Mounts = append(spec.Mounts, specs.Mount{ - Destination: os.TempDir(), - Source: os.TempDir(), - Type: "bind", - }) - - rootDir, bundleDir, conf, err := setupSandbox(spec) - if err != nil { - t.Fatalf("error setting up sandbox: %v", err) - } - defer os.RemoveAll(rootDir) - defer os.RemoveAll(bundleDir) - - // Create and start the sandbox. - s, err := sandbox.Create(uniqueSandboxID(), spec, conf, bundleDir, "", "", nil) - if err != nil { - t.Fatalf("error creating sandbox: %v", err) - } - defer s.Destroy() - if err := s.Start(conf); err != nil { - t.Fatalf("error starting sandbox: %v", err) - } - - // expectedPL lists the expected process state of the sandbox. - expectedPL := []*control.Process{ - { - UID: 0, - PID: 1, - PPID: 0, - C: 0, - Cmd: "sleep", - }, - { - UID: uid, - PID: 2, - PPID: 0, - C: 0, - Cmd: "exe", - }, - } - if err := waitForProcessList(s, expectedPL[:1]); err != nil { - t.Fatalf("Failed to wait for sleep to start, err: %v", err) - } - - // Create an executable that can't be run with the specified UID:GID. - // This shouldn't be callable within the sandbox until we add the - // CAP_DAC_OVERRIDE capability to skip the access check. - exePath := filepath.Join(rootDir, "exe") - if err := ioutil.WriteFile(exePath, []byte("#!/bin/sh\necho hello"), 0770); err != nil { - t.Fatalf("couldn't create executable: %v", err) - } - defer os.Remove(exePath) - - // Need to traverse the intermediate directory. - os.Chmod(rootDir, 0755) - - execArgs := control.ExecArgs{ - Filename: exePath, - Argv: []string{exePath}, - Envv: []string{"PATH=" + os.Getenv("PATH")}, - WorkingDirectory: "/", - KUID: uid, - KGID: gid, - Capabilities: &auth.TaskCapabilities{}, - } - - // "exe" should fail because we don't have the necessary permissions. - if _, err := s.Execute(&execArgs); err == nil { - t.Fatalf("sandbox executed without error, but an error was expected") - } - - // Now we run with the capability enabled and should succeed. - execArgs.Capabilities = &auth.TaskCapabilities{ - EffectiveCaps: auth.CapabilitySetOf(linux.CAP_DAC_OVERRIDE), - } - // "exe" should not fail this time. - if _, err := s.Execute(&execArgs); err != nil { - t.Fatalf("sandbox failed to exec %v: %v", execArgs, err) - } -} - -// Test that an tty FD is sent over the console socket if one is provided. -func TestConsoleSocket(t *testing.T) { - spec := newSpecWithArgs("true") - rootDir, bundleDir, conf, err := setupSandbox(spec) - if err != nil { - t.Fatalf("error setting up sandbox: %v", err) - } - defer os.RemoveAll(rootDir) - defer os.RemoveAll(bundleDir) - - // Create a named socket and start listening. We use a relative path - // to avoid overflowing the unix path length limit (108 chars). - socketPath := filepath.Join(bundleDir, "socket") - cwd, err := os.Getwd() - if err != nil { - t.Fatalf("error getting cwd: %v", err) - } - socketRelPath, err := filepath.Rel(cwd, socketPath) - if err != nil { - t.Fatalf("error getting relative path for %q from cwd %q: %v", socketPath, cwd, err) - } - if len(socketRelPath) > len(socketPath) { - socketRelPath = socketPath - } - srv, err := unet.BindAndListen(socketRelPath, false) - if err != nil { - t.Fatalf("error binding and listening to socket %q: %v", socketPath, err) - } - defer os.Remove(socketPath) - - // Create the sandbox and pass the socket name. - id := uniqueSandboxID() - s, err := sandbox.Create(id, spec, conf, bundleDir, socketRelPath, "", nil) - if err != nil { - t.Fatalf("error creating sandbox: %v", err) - } - - // Open the othe end of the socket. - sock, err := srv.Accept() - if err != nil { - t.Fatalf("error accepting socket connection: %v", err) - } - - // Allow 3 fds to be received. We only expect 1. - r := sock.Reader(true /* blocking */) - r.EnableFDs(1) - - // The socket is closed right after sending the FD, so EOF is - // an allowed error. - b := [][]byte{{}} - if _, err := r.ReadVec(b); err != nil && err != io.EOF { - t.Fatalf("error reading from socket connection: %v", err) - } - - // We should have gotten a control message. - fds, err := r.ExtractFDs() - if err != nil { - t.Fatalf("error extracting fds from socket connection: %v", err) - } - if len(fds) != 1 { - t.Fatalf("got %d fds from socket, wanted 1", len(fds)) - } - - // Verify that the fd is a terminal. - if _, err := unix.IoctlGetTermios(fds[0], unix.TCGETS); err != nil { - t.Errorf("fd is not a terminal (ioctl TGGETS got %v)", err) - } - - // Shut it down. - if err := s.Destroy(); err != nil { - t.Fatalf("error destroying sandbox: %v", err) - } - - // Close socket. - if err := srv.Close(); err != nil { - t.Fatalf("error destroying sandbox: %v", err) - } -} - -func TestSpecUnsupported(t *testing.T) { - spec := newSpecWithArgs("/bin/true") - spec.Process.SelinuxLabel = "somelabel" - - // These are normally set by docker and will just cause warnings to be logged. - spec.Process.ApparmorProfile = "someprofile" - spec.Linux = &specs.Linux{Seccomp: &specs.LinuxSeccomp{}} - - rootDir, bundleDir, conf, err := setupSandbox(spec) - if err != nil { - t.Fatalf("error setting up sandbox: %v", err) - } - defer os.RemoveAll(rootDir) - defer os.RemoveAll(bundleDir) - - id := uniqueSandboxID() - _, err = sandbox.Create(id, spec, conf, bundleDir, "", "", nil) - if err == nil || !strings.Contains(err.Error(), "is not supported") { - t.Errorf("sandbox.Create() wrong error, got: %v, want: *is not supported, spec.Process: %+v", err, spec.Process) - } -} - -// procListsEqual is used to check whether 2 Process lists are equal for all -// implemented fields. -func procListsEqual(got, want []*control.Process) bool { - if len(got) != len(want) { - return false - } - for i := range got { - pd1 := got[i] - pd2 := want[i] - // Zero out unimplemented and timing dependant fields. - pd1.Time, pd2.Time = "", "" - pd1.STime, pd2.STime = "", "" - pd1.C, pd2.C = 0, 0 - if *pd1 != *pd2 { - return false - } - } - return true -} - -func procListToString(pl []*control.Process) string { - strs := make([]string, 0, len(pl)) - for _, p := range pl { - strs = append(strs, fmt.Sprintf("%+v", p)) - } - return fmt.Sprintf("[%s]", strings.Join(strs, ",")) -} - -// TestMain acts like runsc if it is called with the "boot" argument, otherwise -// it just runs the tests. This is required because creating a sandbox will -// call "/proc/self/exe boot". Normally /proc/self/exe is the runsc binary, -// but for tests we have to fake it. -func TestMain(m *testing.M) { - // exit writes coverage data before exiting. - exit := func(status int) { - os.Exit(status) - } - - if !flag.Parsed() { - flag.Parse() - } - - // If we are passed one of the commands then run it. - subcommands.Register(new(cmd.Boot), "boot") - subcommands.Register(new(cmd.Gofer), "gofer") - switch flag.Arg(0) { - case "boot", "gofer": - // Run the command in a goroutine so we can block the main - // thread waiting for shutdownSignal. - go func() { - conf := &boot.Config{ - RootDir: "unused-root-dir", - Network: boot.NetworkNone, - } - var ws syscall.WaitStatus - subcmdCode := subcommands.Execute(context.Background(), conf, &ws) - if subcmdCode != subcommands.ExitSuccess { - panic(fmt.Sprintf("command failed to execute, err: %v", subcmdCode)) - } - // Sandbox exited normally. Shut down this process. - os.Exit(ws.ExitStatus()) - }() - - // Shutdown cleanly when the shutdownSignal is received. This - // allows us to write coverage data before exiting. - sigc := make(chan os.Signal, 1) - signal.Notify(sigc, shutdownSignal) - <-sigc - exit(0) - default: - // Otherwise run the tests. - exit(m.Run()) - } -} |