diff options
author | Nicolas Lacasse <nlacasse@google.com> | 2018-10-17 12:27:58 -0700 |
---|---|---|
committer | Shentubot <shentubot@google.com> | 2018-10-17 12:29:05 -0700 |
commit | 4e6f0892c96c374b1abcf5c39b75ba52d98c97f8 (patch) | |
tree | cb21538ad26a50ff61086d55c1bef36d5026e4c0 /runsc/container | |
parent | 578fe5a50dcf8e104b6bce3802987b0f8c069ade (diff) |
runsc: Support job control signals for the root container.
Now containers run with "docker run -it" support control characters like ^C and
^Z.
This required refactoring our signal handling a bit. Signals delivered to the
"runsc boot" process are turned into loader.Signal calls with the appropriate
delivery mode. Previously they were always sent directly to PID 1.
PiperOrigin-RevId: 217566770
Change-Id: I5b7220d9a0f2b591a56335479454a200c6de8732
Diffstat (limited to 'runsc/container')
-rw-r--r-- | runsc/container/BUILD | 1 | ||||
-rw-r--r-- | runsc/container/console_test.go | 452 | ||||
-rw-r--r-- | runsc/container/container_test.go | 203 |
3 files changed, 453 insertions, 203 deletions
diff --git a/runsc/container/BUILD b/runsc/container/BUILD index 60f1d3033..f4c6f1525 100644 --- a/runsc/container/BUILD +++ b/runsc/container/BUILD @@ -30,6 +30,7 @@ go_test( name = "container_test", size = "medium", srcs = [ + "console_test.go", "container_test.go", "fs_test.go", "multi_container_test.go", diff --git a/runsc/container/console_test.go b/runsc/container/console_test.go new file mode 100644 index 000000000..82adcbb7d --- /dev/null +++ b/runsc/container/console_test.go @@ -0,0 +1,452 @@ +// 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 container + +import ( + "bytes" + "fmt" + "io" + "os" + "path/filepath" + "sync" + "syscall" + "testing" + "time" + + "github.com/kr/pty" + "golang.org/x/sys/unix" + "gvisor.googlesource.com/gvisor/pkg/sentry/control" + "gvisor.googlesource.com/gvisor/pkg/unet" + "gvisor.googlesource.com/gvisor/pkg/urpc" + "gvisor.googlesource.com/gvisor/runsc/test/testutil" +) + +// createConsoleSocket creates a socket that will receive a console fd from the +// sandbox. If no error occurs, it returns the server socket and a cleanup +// function. +func createConsoleSocket(socketPath string) (*unet.ServerSocket, func() error, error) { + cwd, err := os.Getwd() + if err != nil { + return nil, nil, fmt.Errorf("error getting cwd: %v", err) + } + // We use a relative path to avoid overflowing the unix path length + // limit (108 chars). + socketRelPath, err := filepath.Rel(cwd, socketPath) + if err != nil { + return nil, nil, fmt.Errorf("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 { + return nil, nil, fmt.Errorf("error binding and listening to socket %q: %v", socketPath, err) + } + + cleanup := func() error { + if err := srv.Close(); err != nil { + return fmt.Errorf("error closing socket %q: %v", socketRelPath, err) + } + if err := os.Remove(socketPath); err != nil { + return fmt.Errorf("error removing socket %q: %v", socketRelPath, err) + } + return nil + } + + return srv, cleanup, nil +} + +// receiveConsolePTY accepts a connection on the server socket and reads fds. +// It fails if more than one FD is received, or if the FD is not a PTY. It +// returns the PTY master file. +func receiveConsolePTY(srv *unet.ServerSocket) (*os.File, error) { + sock, err := srv.Accept() + if err != nil { + return nil, fmt.Errorf("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 { + return nil, fmt.Errorf("error reading from socket connection: %v", err) + } + + // We should have gotten a control message. + fds, err := r.ExtractFDs() + if err != nil { + return nil, fmt.Errorf("error extracting fds from socket connection: %v", err) + } + if len(fds) != 1 { + return nil, fmt.Errorf("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 { + return nil, fmt.Errorf("fd is not a terminal (ioctl TGGETS got %v)", err) + } + + return os.NewFile(uintptr(fds[0]), "pty_master"), nil +} + +// Test that an pty FD is sent over the console socket if one is provided. +func TestConsoleSocket(t *testing.T) { + for _, conf := range configs(all...) { + t.Logf("Running test with conf: %+v", conf) + spec := testutil.NewSpecWithArgs("true") + rootDir, bundleDir, err := testutil.SetupContainer(spec, conf) + if err != nil { + t.Fatalf("error setting up container: %v", err) + } + defer os.RemoveAll(rootDir) + defer os.RemoveAll(bundleDir) + + socketPath := filepath.Join(bundleDir, "socket") + srv, cleanup, err := createConsoleSocket(socketPath) + if err != nil { + t.Fatalf("error creating socket at %q: %v", socketPath, err) + } + defer cleanup() + + // Create the container and pass the socket name. + id := testutil.UniqueContainerID() + c, err := Create(id, spec, conf, bundleDir, socketPath, "", "") + if err != nil { + t.Fatalf("error creating container: %v", err) + } + defer c.Destroy() + + // Make sure we get a console PTY. + ptyMaster, err := receiveConsolePTY(srv) + if err != nil { + t.Fatalf("error receiving console FD: %v", err) + } + ptyMaster.Close() + } +} + +// Test that job control signals work on a console created with "exec -ti". +func TestJobControlSignalExec(t *testing.T) { + spec := testutil.NewSpecWithArgs("/bin/sleep", "10000") + conf := testutil.TestConfig() + + rootDir, bundleDir, err := testutil.SetupContainer(spec, conf) + if err != nil { + t.Fatalf("error setting up container: %v", err) + } + defer os.RemoveAll(rootDir) + defer os.RemoveAll(bundleDir) + + // Create and start the container. + c, err := Create(testutil.UniqueContainerID(), spec, conf, bundleDir, "", "", "") + if err != nil { + t.Fatalf("error creating container: %v", err) + } + defer c.Destroy() + if err := c.Start(conf); err != nil { + t.Fatalf("error starting container: %v", err) + } + + // Create a pty master/slave. The slave will be passed to the exec + // process. + ptyMaster, ptySlave, err := pty.Open() + if err != nil { + t.Fatalf("error opening pty: %v", err) + } + defer ptyMaster.Close() + defer ptySlave.Close() + + // Exec bash and attach a terminal. + args := &control.ExecArgs{ + Filename: "/bin/bash", + // Don't let bash execute from profile or rc files, otherwise + // our PID counts get messed up. + Argv: []string{"/bin/bash", "--noprofile", "--norc"}, + // Pass the pty slave as FD 0, 1, and 2. + FilePayload: urpc.FilePayload{ + Files: []*os.File{ptySlave, ptySlave, ptySlave}, + }, + StdioIsPty: true, + } + + pid, err := c.Execute(args) + if err != nil { + t.Fatalf("error executing: %v", err) + } + if pid != 2 { + t.Fatalf("exec got pid %d, wanted %d", pid, 2) + } + + // Make sure all the processes are running. + expectedPL := []*control.Process{ + // Root container process. + {PID: 1, Cmd: "sleep"}, + // Bash from exec process. + {PID: 2, Cmd: "bash"}, + } + if err := waitForProcessList(c, expectedPL); err != nil { + t.Error(err) + } + + // Execute sleep. + ptyMaster.Write([]byte("sleep 100\n")) + + // Wait for it to start. Sleep's PPID is bash's PID. + expectedPL = append(expectedPL, &control.Process{PID: 3, PPID: 2, Cmd: "sleep"}) + if err := waitForProcessList(c, expectedPL); err != nil { + t.Error(err) + } + + // Send a SIGTERM to the foreground process for the exec PID. Note that + // although we pass in the PID of "bash", it should actually terminate + // "sleep", since that is the foreground process. + if err := c.Sandbox.SignalProcess(c.ID, pid, syscall.SIGTERM, true /* fgProcess */); err != nil { + t.Fatalf("error signaling container: %v", err) + } + + // Sleep process should be gone. + expectedPL = expectedPL[:len(expectedPL)-1] + if err := waitForProcessList(c, expectedPL); err != nil { + t.Error(err) + } + + // Sleep is dead, but it may take more time for bash to notice and + // change the foreground process back to itself. We know it is done + // when bash writes "Terminated" to the pty. + if err := testutil.WaitUntilRead(ptyMaster, "Terminated", nil, 5*time.Second); err != nil { + t.Fatalf("bash did not take over pty: %v", err) + } + + // Send a SIGKILL to the foreground process again. This time "bash" + // should be killed. We use SIGKILL instead of SIGTERM or SIGINT + // because bash ignores those. + if err := c.Sandbox.SignalProcess(c.ID, pid, syscall.SIGKILL, true /* fgProcess */); err != nil { + t.Fatalf("error signaling container: %v", err) + } + expectedPL = expectedPL[:1] + if err := waitForProcessList(c, expectedPL); err != nil { + t.Error(err) + } + + // Make sure the process indicates it was killed by a SIGKILL. + ws, err := c.WaitPID(pid, true) + if err != nil { + t.Errorf("waiting on container failed: %v", err) + } + if !ws.Signaled() { + t.Error("ws.Signaled() got false, want true") + } + if got, want := ws.Signal(), syscall.SIGKILL; got != want { + t.Errorf("ws.Signal() got %v, want %v", got, want) + } +} + +// Test that job control signals work on a console created with "run -ti". +func TestJobControlSignalRootContainer(t *testing.T) { + conf := testutil.TestConfig() + // Don't let bash execute from profile or rc files, otherwise our PID + // counts get messed up. + spec := testutil.NewSpecWithArgs("/bin/bash", "--noprofile", "--norc") + spec.Process.Terminal = true + + rootDir, bundleDir, err := testutil.SetupContainer(spec, conf) + if err != nil { + t.Fatalf("error setting up container: %v", err) + } + defer os.RemoveAll(rootDir) + defer os.RemoveAll(bundleDir) + + socketPath := filepath.Join(bundleDir, "socket") + srv, cleanup, err := createConsoleSocket(socketPath) + if err != nil { + t.Fatalf("error creating socket at %q: %v", socketPath, err) + } + defer cleanup() + + // Create the container and pass the socket name. + id := testutil.UniqueContainerID() + c, err := Create(id, spec, conf, bundleDir, socketPath, "", "") + if err != nil { + t.Fatalf("error creating container: %v", err) + } + defer c.Destroy() + + // Get the PTY master. + ptyMaster, err := receiveConsolePTY(srv) + if err != nil { + t.Fatalf("error receiving console FD: %v", err) + } + defer ptyMaster.Close() + + // Bash output as well as sandbox output will be written to the PTY + // file. Writes after a certain point will block unless we drain the + // PTY, so we must continually copy from it. + // + // We log the output to stdout for debugabilitly, and also to a buffer, + // since we wait on particular output from bash below. We use a custom + // blockingBuffer which is thread-safe and also blocks on Read calls, + // which makes this a suitable Reader for WaitUntilRead. + ptyBuf := newBlockingBuffer() + tee := io.TeeReader(ptyMaster, ptyBuf) + go io.Copy(os.Stdout, tee) + + // Start the container. + if err := c.Start(conf); err != nil { + t.Fatalf("error starting container: %v", err) + } + + // Start waiting for the container to exit in a goroutine. We do this + // very early, otherwise it might exit before we have a chance to call + // Wait. + var ( + ws syscall.WaitStatus + wg sync.WaitGroup + ) + wg.Add(1) + go func() { + var err error + ws, err = c.Wait() + if err != nil { + t.Errorf("error waiting on container: %v", err) + } + wg.Done() + }() + + // Wait for bash to start. + expectedPL := []*control.Process{ + {PID: 1, Cmd: "bash"}, + } + if err := waitForProcessList(c, expectedPL); err != nil { + t.Fatal(err) + } + + // Execute sleep via the terminal. + ptyMaster.Write([]byte("sleep 100\n")) + + // Wait for sleep to start. + expectedPL = append(expectedPL, &control.Process{PID: 2, PPID: 1, Cmd: "sleep"}) + if err := waitForProcessList(c, expectedPL); err != nil { + t.Fatal(err) + } + + // Reset the pty buffer, so there is less output for us to scan later. + ptyBuf.Reset() + + // Send a SIGTERM to the foreground process. We pass PID=0, indicating + // that the root process should be killed. However, by setting + // fgProcess=true, the signal should actually be sent to sleep. + if err := c.Sandbox.SignalProcess(c.ID, 0 /* PID */, syscall.SIGTERM, true /* fgProcess */); err != nil { + t.Fatalf("error signaling container: %v", err) + } + + // Sleep process should be gone. + expectedPL = expectedPL[:len(expectedPL)-1] + if err := waitForProcessList(c, expectedPL); err != nil { + t.Error(err) + } + + // Sleep is dead, but it may take more time for bash to notice and + // change the foreground process back to itself. We know it is done + // when bash writes "Terminated" to the pty. + if err := testutil.WaitUntilRead(ptyBuf, "Terminated", nil, 5*time.Second); err != nil { + t.Fatalf("bash did not take over pty: %v", err) + } + + // Send a SIGKILL to the foreground process again. This time "bash" + // should be killed. We use SIGKILL instead of SIGTERM or SIGINT + // because bash ignores those. + if err := c.Sandbox.SignalProcess(c.ID, 0 /* PID */, syscall.SIGKILL, true /* fgProcess */); err != nil { + t.Fatalf("error signaling container: %v", err) + } + + // Wait for the sandbox to exit. It should exit with a SIGKILL status. + wg.Wait() + if !ws.Signaled() { + t.Error("ws.Signaled() got false, want true") + } + if got, want := ws.Signal(), syscall.SIGKILL; got != want { + t.Errorf("ws.Signal() got %v, want %v", got, want) + } +} + +// blockingBuffer is a thread-safe buffer that blocks when reading if the +// buffer is empty. It implements io.ReadWriter. +type blockingBuffer struct { + // A send to readCh indicates that a previously empty buffer now has + // data for reading. + readCh chan struct{} + + // mu protects buf. + mu sync.Mutex + buf bytes.Buffer +} + +func newBlockingBuffer() *blockingBuffer { + return &blockingBuffer{ + readCh: make(chan struct{}, 1), + } +} + +// Write implements Writer.Write. +func (bb *blockingBuffer) Write(p []byte) (int, error) { + bb.mu.Lock() + defer bb.mu.Unlock() + l := bb.buf.Len() + n, err := bb.buf.Write(p) + if l == 0 && n > 0 { + // New data! + bb.readCh <- struct{}{} + } + return n, err +} + +// Read implements Reader.Read. It will block until data is available. +func (bb *blockingBuffer) Read(p []byte) (int, error) { + for { + bb.mu.Lock() + n, err := bb.buf.Read(p) + if n > 0 || err != io.EOF { + if bb.buf.Len() == 0 { + // Reset the readCh. + select { + case <-bb.readCh: + default: + } + } + bb.mu.Unlock() + return n, err + } + bb.mu.Unlock() + + // Wait for new data. + <-bb.readCh + } +} + +// Reset resets the buffer. +func (bb *blockingBuffer) Reset() { + bb.mu.Lock() + defer bb.mu.Unlock() + bb.buf.Reset() + // Reset the readCh. + select { + case <-bb.readCh: + default: + } +} diff --git a/runsc/container/container_test.go b/runsc/container/container_test.go index d9cd38c0a..e2bb7d8ec 100644 --- a/runsc/container/container_test.go +++ b/runsc/container/container_test.go @@ -17,7 +17,6 @@ package container import ( "bytes" "fmt" - "io" "io/ioutil" "os" "path" @@ -31,15 +30,11 @@ import ( "time" "github.com/cenkalti/backoff" - "github.com/kr/pty" 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/pkg/urpc" "gvisor.googlesource.com/gvisor/runsc/boot" "gvisor.googlesource.com/gvisor/runsc/test/testutil" ) @@ -1151,89 +1146,6 @@ func TestCapabilities(t *testing.T) { } } -// Test that an tty FD is sent over the console socket if one is provided. -func TestConsoleSocket(t *testing.T) { - for _, conf := range configs(all...) { - t.Logf("Running test with conf: %+v", conf) - spec := testutil.NewSpecWithArgs("true") - rootDir, bundleDir, err := testutil.SetupContainer(spec, conf) - if err != nil { - t.Fatalf("error setting up container: %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 container and pass the socket name. - id := testutil.UniqueContainerID() - c, err := Create(id, spec, conf, bundleDir, socketRelPath, "", "") - if err != nil { - t.Fatalf("error creating container: %v", err) - } - c.Destroy() - - // 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 := c.Destroy(); err != nil { - t.Fatalf("error destroying container: %v", err) - } - - // Close socket. - if err := srv.Close(); err != nil { - t.Fatalf("error destroying container: %v", err) - } - } -} - // TestRunNonRoot checks that sandbox can be configured when running as // non-privileged user. func TestRunNonRoot(t *testing.T) { @@ -1626,121 +1538,6 @@ func TestRootNotMount(t *testing.T) { } } -func TestJobControlSignalExec(t *testing.T) { - spec := testutil.NewSpecWithArgs("/bin/sleep", "10000") - conf := testutil.TestConfig() - - rootDir, bundleDir, err := testutil.SetupContainer(spec, conf) - if err != nil { - t.Fatalf("error setting up container: %v", err) - } - defer os.RemoveAll(rootDir) - defer os.RemoveAll(bundleDir) - - // Create and start the container. - c, err := Create(testutil.UniqueContainerID(), spec, conf, bundleDir, "", "", "") - if err != nil { - t.Fatalf("error creating container: %v", err) - } - defer c.Destroy() - if err := c.Start(conf); err != nil { - t.Fatalf("error starting container: %v", err) - } - - // Create a pty master/slave. The slave will be passed to the exec - // process. - ptyMaster, ptySlave, err := pty.Open() - if err != nil { - t.Fatalf("error opening pty: %v", err) - } - defer ptyMaster.Close() - defer ptySlave.Close() - - // Exec bash and attach a terminal. - args := &control.ExecArgs{ - Filename: "/bin/bash", - // Don't let bash execute from profile or rc files, otherwise - // our PID counts get messed up. - Argv: []string{"/bin/bash", "--noprofile", "--norc"}, - // Pass the pty slave as FD 0, 1, and 2. - FilePayload: urpc.FilePayload{ - Files: []*os.File{ptySlave, ptySlave, ptySlave}, - }, - StdioIsPty: true, - } - - pid, err := c.Execute(args) - if err != nil { - t.Fatalf("error executing: %v", err) - } - if pid != 2 { - t.Fatalf("exec got pid %d, wanted %d", pid, 2) - } - - // Make sure all the processes are running. - expectedPL := []*control.Process{ - // Root container process. - {PID: 1, Cmd: "sleep"}, - // Bash from exec process. - {PID: 2, Cmd: "bash"}, - } - if err := waitForProcessList(c, expectedPL); err != nil { - t.Error(err) - } - - // Execute sleep. - ptyMaster.Write([]byte("sleep 100\n")) - - // Wait for it to start. Sleep's PPID is bash's PID. - expectedPL = append(expectedPL, &control.Process{PID: 3, PPID: 2, Cmd: "sleep"}) - if err := waitForProcessList(c, expectedPL); err != nil { - t.Error(err) - } - - // Send a SIGTERM to the foreground process for the exec PID. Note that - // although we pass in the PID of "bash", it should actually terminate - // "sleep", since that is the foreground process. - if err := c.Sandbox.SignalProcess(c.ID, pid, syscall.SIGTERM, true /* fgProcess */); err != nil { - t.Fatalf("error signaling container: %v", err) - } - - // Sleep process should be gone. - expectedPL = expectedPL[:len(expectedPL)-1] - if err := waitForProcessList(c, expectedPL); err != nil { - t.Error(err) - } - - // Sleep is dead, but it may take more time for bash to notice and - // change the foreground process back to itself. We know it is done - // when bash writes "Terminated" to the pty. - if err := testutil.WaitUntilRead(ptyMaster, "Terminated", nil, 5*time.Second); err != nil { - t.Fatalf("bash did not take over pty: %v", err) - } - - // Send a SIGKILL to the foreground process again. This time "bash" - // should be killed. We use SIGKILL instead of SIGTERM or SIGINT - // because bash ignores those. - if err := c.Sandbox.SignalProcess(c.ID, pid, syscall.SIGKILL, true /* fgProcess */); err != nil { - t.Fatalf("error signaling container: %v", err) - } - expectedPL = expectedPL[:1] - if err := waitForProcessList(c, expectedPL); err != nil { - t.Error(err) - } - - // Make sure the process indicates it was killed by a SIGKILL. - ws, err := c.WaitPID(pid, true) - if err != nil { - t.Errorf("waiting on container failed: %v", err) - } - if !ws.Signaled() { - t.Error("ws.Signaled() got false, want true") - } - if got, want := ws.Signal(), syscall.SIGKILL; got != want { - t.Errorf("ws.Signal() got %v, want %v", got, want) - } -} - func TestUserLog(t *testing.T) { app, err := testutil.FindFile("runsc/container/test_app") if err != nil { |