diff --git a/pkg/fd/BUILD b/pkg/fd/BUILD index c7f549428..afa8f7659 100644 --- a/pkg/fd/BUILD +++ b/pkg/fd/BUILD @@ -8,9 +8,6 @@ go_library( srcs = ["fd.go"], importpath = "gvisor.dev/gvisor/pkg/fd", visibility = ["//visibility:public"], - deps = [ - "//pkg/unet", - ], ) go_test( diff --git a/pkg/fd/fd.go b/pkg/fd/fd.go index 7691b477b..83bcfe220 100644 --- a/pkg/fd/fd.go +++ b/pkg/fd/fd.go @@ -22,8 +22,6 @@ import ( "runtime" "sync/atomic" "syscall" - - "gvisor.dev/gvisor/pkg/unet" ) // ReadWriter implements io.ReadWriter, io.ReaderAt, and io.WriterAt for fd. It @@ -187,12 +185,6 @@ func OpenAt(dir *FD, path string, flags int, mode uint32) (*FD, error) { return New(f), nil } -// DialUnix connects to a Unix Domain Socket and return the file descriptor. -func DialUnix(path string) (*FD, error) { - socket, err := unet.Connect(path, false) - return New(socket.FD()), err -} - // Close closes the file descriptor contained in the FD. // // Close is safe to call multiple times, but will return an error after the diff --git a/runsc/fsgofer/filter/config.go b/runsc/fsgofer/filter/config.go index 0bf7507b7..2ea95f8fb 100644 --- a/runsc/fsgofer/filter/config.go +++ b/runsc/fsgofer/filter/config.go @@ -220,6 +220,18 @@ var udsSyscalls = seccomp.SyscallRules{ syscall.SYS_SOCKET: []seccomp.Rule{ { seccomp.AllowValue(syscall.AF_UNIX), + seccomp.AllowValue(syscall.SOCK_STREAM), + seccomp.AllowValue(0), + }, + { + seccomp.AllowValue(syscall.AF_UNIX), + seccomp.AllowValue(syscall.SOCK_DGRAM), + seccomp.AllowValue(0), + }, + { + seccomp.AllowValue(syscall.AF_UNIX), + seccomp.AllowValue(syscall.SOCK_SEQPACKET), + seccomp.AllowValue(0), }, }, syscall.SYS_CONNECT: []seccomp.Rule{ diff --git a/runsc/fsgofer/fsgofer.go b/runsc/fsgofer/fsgofer.go index ed8b02cf0..3fceecb3d 100644 --- a/runsc/fsgofer/fsgofer.go +++ b/runsc/fsgofer/fsgofer.go @@ -265,10 +265,10 @@ func openAnyFileFromParent(parent *localFile, name string) (*fd.FD, string, erro // actual file open and is customizable by the caller. func openAnyFile(path string, fn func(mode int) (*fd.FD, error)) (*fd.FD, error) { // Attempt to open file in the following mode in order: - // 1. RDONLY | NONBLOCK: for all files, works for directories and ro mounts too. - // Use non-blocking to prevent getting stuck inside open(2) for FIFOs. This option - // has no effect on regular files. - // 2. PATH: for symlinks + // 1. RDONLY | NONBLOCK: for all files, directories, ro mounts, FIFOs. + // Use non-blocking to prevent getting stuck inside open(2) for + // FIFOs. This option has no effect on regular files. + // 2. PATH: for symlinks, sockets. modes := []int{syscall.O_RDONLY | syscall.O_NONBLOCK, unix.O_PATH} var err error @@ -1032,12 +1032,48 @@ func (l *localFile) Flush() error { } // Connect implements p9.File. -func (l *localFile) Connect(p9.ConnectFlags) (*fd.FD, error) { - // Check to see if the CLI option has been set to allow the UDS mount. +func (l *localFile) Connect(flags p9.ConnectFlags) (*fd.FD, error) { if !l.attachPoint.conf.HostUDS { return nil, syscall.ECONNREFUSED } - return fd.DialUnix(l.hostPath) + + // TODO(gvisor.dev/issue/1003): Due to different app vs replacement + // mappings, the app path may have fit in the sockaddr, but we can't + // fit f.path in our sockaddr. We'd need to redirect through a shorter + // path in order to actually connect to this socket. + if len(l.hostPath) > linux.UnixPathMax { + return nil, syscall.ECONNREFUSED + } + + var stype int + switch flags { + case p9.StreamSocket: + stype = syscall.SOCK_STREAM + case p9.DgramSocket: + stype = syscall.SOCK_DGRAM + case p9.SeqpacketSocket: + stype = syscall.SOCK_SEQPACKET + default: + return nil, syscall.ENXIO + } + + f, err := syscall.Socket(syscall.AF_UNIX, stype, 0) + if err != nil { + return nil, err + } + + if err := syscall.SetNonblock(f, true); err != nil { + syscall.Close(f) + return nil, err + } + + sa := syscall.SockaddrUnix{Name: l.hostPath} + if err := syscall.Connect(f, &sa); err != nil { + syscall.Close(f) + return nil, err + } + + return fd.New(f), nil } // Close implements p9.File. diff --git a/runsc/testutil/BUILD b/runsc/testutil/BUILD index d44ebc906..c96ca2eb6 100644 --- a/runsc/testutil/BUILD +++ b/runsc/testutil/BUILD @@ -9,6 +9,7 @@ go_library( importpath = "gvisor.dev/gvisor/runsc/testutil", visibility = ["//:sandbox"], deps = [ + "//pkg/log", "//runsc/boot", "//runsc/specutils", "@com_github_cenkalti_backoff//:go_default_library", diff --git a/runsc/testutil/testutil.go b/runsc/testutil/testutil.go index edf8b126c..26467bdc7 100644 --- a/runsc/testutil/testutil.go +++ b/runsc/testutil/testutil.go @@ -25,7 +25,6 @@ import ( "fmt" "io" "io/ioutil" - "log" "math" "math/rand" "net/http" @@ -42,6 +41,7 @@ import ( "github.com/cenkalti/backoff" specs "github.com/opencontainers/runtime-spec/specs-go" + "gvisor.dev/gvisor/pkg/log" "gvisor.dev/gvisor/runsc/boot" "gvisor.dev/gvisor/runsc/specutils" ) @@ -286,7 +286,7 @@ func WaitForHTTP(port int, timeout time.Duration) error { url := fmt.Sprintf("http://localhost:%d/", port) resp, err := c.Get(url) if err != nil { - log.Printf("Waiting %s: %v", url, err) + log.Infof("Waiting %s: %v", url, err) return err } resp.Body.Close() diff --git a/test/syscalls/BUILD b/test/syscalls/BUILD index 87ef87e07..a53a23afd 100644 --- a/test/syscalls/BUILD +++ b/test/syscalls/BUILD @@ -78,6 +78,12 @@ syscall_test(test = "//test/syscalls/linux:clock_nanosleep_test") syscall_test(test = "//test/syscalls/linux:concurrency_test") +syscall_test( + add_uds_tree = True, + test = "//test/syscalls/linux:connect_external_test", + use_tmpfs = True, +) + syscall_test( add_overlay = True, test = "//test/syscalls/linux:creat_test", @@ -716,6 +722,7 @@ go_binary( "//runsc/specutils", "//runsc/testutil", "//test/syscalls/gtest", + "//test/uds", "@com_github_opencontainers_runtime-spec//specs-go:go_default_library", "@org_golang_x_sys//unix:go_default_library", ], diff --git a/test/syscalls/build_defs.bzl b/test/syscalls/build_defs.bzl index e94ef5602..dcf5b73ed 100644 --- a/test/syscalls/build_defs.bzl +++ b/test/syscalls/build_defs.bzl @@ -8,6 +8,7 @@ def syscall_test( size = "small", use_tmpfs = False, add_overlay = False, + add_uds_tree = False, tags = None): _syscall_test( test = test, @@ -15,6 +16,7 @@ def syscall_test( size = size, platform = "native", use_tmpfs = False, + add_uds_tree = add_uds_tree, tags = tags, ) @@ -24,6 +26,7 @@ def syscall_test( size = size, platform = "kvm", use_tmpfs = use_tmpfs, + add_uds_tree = add_uds_tree, tags = tags, ) @@ -33,6 +36,7 @@ def syscall_test( size = size, platform = "ptrace", use_tmpfs = use_tmpfs, + add_uds_tree = add_uds_tree, tags = tags, ) @@ -43,6 +47,7 @@ def syscall_test( size = size, platform = "ptrace", use_tmpfs = False, # overlay is adding a writable tmpfs on top of root. + add_uds_tree = add_uds_tree, tags = tags, overlay = True, ) @@ -55,6 +60,7 @@ def syscall_test( size = size, platform = "ptrace", use_tmpfs = use_tmpfs, + add_uds_tree = add_uds_tree, tags = tags, file_access = "shared", ) @@ -67,7 +73,8 @@ def _syscall_test( use_tmpfs, tags, file_access = "exclusive", - overlay = False): + overlay = False, + add_uds_tree = False): test_name = test.split(":")[1] # Prepend "runsc" to non-native platform names. @@ -103,6 +110,7 @@ def _syscall_test( "--use-tmpfs=" + str(use_tmpfs), "--file-access=" + file_access, "--overlay=" + str(overlay), + "--add-uds-tree=" + str(add_uds_tree), ] sh_test( diff --git a/test/syscalls/linux/BUILD b/test/syscalls/linux/BUILD index 84a8eb76c..cf4c63b40 100644 --- a/test/syscalls/linux/BUILD +++ b/test/syscalls/linux/BUILD @@ -479,6 +479,21 @@ cc_binary( ], ) +cc_binary( + name = "connect_external_test", + testonly = 1, + srcs = ["connect_external.cc"], + linkstatic = 1, + deps = [ + ":socket_test_util", + "//test/util:file_descriptor", + "//test/util:fs_util", + "//test/util:test_main", + "//test/util:test_util", + "@com_google_googletest//:gtest", + ], +) + cc_binary( name = "creat_test", testonly = 1, diff --git a/test/syscalls/linux/accept_bind.cc b/test/syscalls/linux/accept_bind.cc index 1122ea240..328192a05 100644 --- a/test/syscalls/linux/accept_bind.cc +++ b/test/syscalls/linux/accept_bind.cc @@ -140,6 +140,18 @@ TEST_P(AllSocketPairTest, Connect) { SyscallSucceeds()); } +TEST_P(AllSocketPairTest, ConnectNonListening) { + auto sockets = ASSERT_NO_ERRNO_AND_VALUE(NewSocketPair()); + + ASSERT_THAT(bind(sockets->first_fd(), sockets->first_addr(), + sockets->first_addr_size()), + SyscallSucceeds()); + + ASSERT_THAT(connect(sockets->second_fd(), sockets->first_addr(), + sockets->first_addr_size()), + SyscallFailsWithErrno(ECONNREFUSED)); +} + TEST_P(AllSocketPairTest, ConnectToFilePath) { auto sockets = ASSERT_NO_ERRNO_AND_VALUE(NewSocketPair()); diff --git a/test/syscalls/linux/connect_external.cc b/test/syscalls/linux/connect_external.cc new file mode 100644 index 000000000..98032ac19 --- /dev/null +++ b/test/syscalls/linux/connect_external.cc @@ -0,0 +1,164 @@ +// Copyright 2019 The gVisor Authors. +// +// 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. + +#include +#include +#include +#include +#include + +#include +#include + +#include "gtest/gtest.h" +#include "gtest/gtest.h" +#include "test/syscalls/linux/socket_test_util.h" +#include "test/util/file_descriptor.h" +#include "test/util/fs_util.h" +#include "test/util/test_util.h" + +// This file contains tests specific to connecting to host UDS managed outside +// the sandbox / test. +// +// A set of ultity sockets will be created externally in $TEST_UDS_TREE and +// $TEST_UDS_ATTACH_TREE for these tests to interact with. + +namespace gvisor { +namespace testing { + +namespace { + +struct ProtocolSocket { + int protocol; + std::string name; +}; + +// Parameter is (socket root dir, ProtocolSocket). +using GoferStreamSeqpacketTest = + ::testing::TestWithParam>; + +// Connect to a socket and verify that write/read work. +// +// An "echo" socket doesn't work for dgram sockets because our socket is +// unnamed. The server thus has no way to reply to us. +TEST_P(GoferStreamSeqpacketTest, Echo) { + std::string env; + ProtocolSocket proto; + std::tie(env, proto) = GetParam(); + + char *val = getenv(env.c_str()); + ASSERT_NE(val, nullptr); + std::string root(val); + + FileDescriptor sock = + ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_UNIX, proto.protocol, 0)); + + std::string socket_path = JoinPath(root, proto.name, "echo"); + + struct sockaddr_un addr = {}; + addr.sun_family = AF_UNIX; + memcpy(addr.sun_path, socket_path.c_str(), socket_path.length()); + + ASSERT_THAT(connect(sock.get(), reinterpret_cast(&addr), + sizeof(addr)), + SyscallSucceeds()); + + constexpr int kBufferSize = 64; + char send_buffer[kBufferSize]; + memset(send_buffer, 'a', sizeof(send_buffer)); + + ASSERT_THAT(WriteFd(sock.get(), send_buffer, sizeof(send_buffer)), + SyscallSucceedsWithValue(sizeof(send_buffer))); + + char recv_buffer[kBufferSize]; + ASSERT_THAT(ReadFd(sock.get(), recv_buffer, sizeof(recv_buffer)), + SyscallSucceedsWithValue(sizeof(recv_buffer))); + ASSERT_EQ(0, memcmp(send_buffer, recv_buffer, sizeof(send_buffer))); +} + +// It is not possible to connect to a bound but non-listening socket. +TEST_P(GoferStreamSeqpacketTest, NonListening) { + std::string env; + ProtocolSocket proto; + std::tie(env, proto) = GetParam(); + + char *val = getenv(env.c_str()); + ASSERT_NE(val, nullptr); + std::string root(val); + + FileDescriptor sock = + ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_UNIX, proto.protocol, 0)); + + std::string socket_path = JoinPath(root, proto.name, "nonlistening"); + + struct sockaddr_un addr = {}; + addr.sun_family = AF_UNIX; + memcpy(addr.sun_path, socket_path.c_str(), socket_path.length()); + + ASSERT_THAT(connect(sock.get(), reinterpret_cast(&addr), + sizeof(addr)), + SyscallFailsWithErrno(ECONNREFUSED)); +} + +INSTANTIATE_TEST_SUITE_P( + StreamSeqpacket, GoferStreamSeqpacketTest, + ::testing::Combine( + // Test access via standard path and attach point. + ::testing::Values("TEST_UDS_TREE", "TEST_UDS_ATTACH_TREE"), + ::testing::Values(ProtocolSocket{SOCK_STREAM, "stream"}, + ProtocolSocket{SOCK_SEQPACKET, "seqpacket"}))); + +// Parameter is socket root dir. +using GoferDgramTest = ::testing::TestWithParam; + +// Connect to a socket and verify that write works. +// +// An "echo" socket doesn't work for dgram sockets because our socket is +// unnamed. The server thus has no way to reply to us. +TEST_P(GoferDgramTest, Null) { + std::string env = GetParam(); + char *val = getenv(env.c_str()); + ASSERT_NE(val, nullptr); + std::string root(val); + + FileDescriptor sock = + ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_UNIX, SOCK_DGRAM, 0)); + + std::string socket_path = JoinPath(root, "dgram/null"); + + struct sockaddr_un addr = {}; + addr.sun_family = AF_UNIX; + memcpy(addr.sun_path, socket_path.c_str(), socket_path.length()); + + ASSERT_THAT(connect(sock.get(), reinterpret_cast(&addr), + sizeof(addr)), + SyscallSucceeds()); + + constexpr int kBufferSize = 64; + char send_buffer[kBufferSize]; + memset(send_buffer, 'a', sizeof(send_buffer)); + + ASSERT_THAT(WriteFd(sock.get(), send_buffer, sizeof(send_buffer)), + SyscallSucceedsWithValue(sizeof(send_buffer))); +} + +INSTANTIATE_TEST_SUITE_P(Dgram, GoferDgramTest, + // Test access via standard path and attach point. + ::testing::Values("TEST_UDS_TREE", + "TEST_UDS_ATTACH_TREE")); + +} // namespace + +} // namespace testing +} // namespace gvisor diff --git a/test/syscalls/syscall_test_runner.go b/test/syscalls/syscall_test_runner.go index c1e9ce22c..856398994 100644 --- a/test/syscalls/syscall_test_runner.go +++ b/test/syscalls/syscall_test_runner.go @@ -35,6 +35,7 @@ import ( "gvisor.dev/gvisor/runsc/specutils" "gvisor.dev/gvisor/runsc/testutil" "gvisor.dev/gvisor/test/syscalls/gtest" + "gvisor.dev/gvisor/test/uds" ) // Location of syscall tests, relative to the repo root. @@ -50,6 +51,8 @@ var ( overlay = flag.Bool("overlay", false, "wrap filesystem mounts with writable tmpfs overlay") parallel = flag.Bool("parallel", false, "run tests in parallel") runscPath = flag.String("runsc", "", "path to runsc binary") + + addUDSTree = flag.Bool("add-uds-tree", false, "expose a tree of UDS utilities for use in tests") ) // runTestCaseNative runs the test case directly on the host machine. @@ -86,6 +89,19 @@ func runTestCaseNative(testBin string, tc gtest.TestCase, t *testing.T) { // intepret them. env = filterEnv(env, []string{"TEST_SHARD_INDEX", "TEST_TOTAL_SHARDS", "GTEST_SHARD_INDEX", "GTEST_TOTAL_SHARDS"}) + if *addUDSTree { + socketDir, cleanup, err := uds.CreateSocketTree("/tmp") + if err != nil { + t.Fatalf("failed to create socket tree: %v", err) + } + defer cleanup() + + env = append(env, "TEST_UDS_TREE="+socketDir) + // On Linux, the concept of "attach" location doesn't exist. + // Just pass the same path to make these test identical. + env = append(env, "TEST_UDS_ATTACH_TREE="+socketDir) + } + cmd := exec.Command(testBin, gtest.FilterTestFlag+"="+tc.FullName()) cmd.Env = env cmd.Stdout = os.Stdout @@ -96,14 +112,186 @@ func runTestCaseNative(testBin string, tc gtest.TestCase, t *testing.T) { } } -// runsTestCaseRunsc runs the test case in runsc. -func runTestCaseRunsc(testBin string, tc gtest.TestCase, t *testing.T) { +// runRunsc runs spec in runsc in a standard test configuration. +// +// runsc logs will be saved to a path in TEST_UNDECLARED_OUTPUTS_DIR. +// +// Returns an error if the sandboxed application exits non-zero. +func runRunsc(tc gtest.TestCase, spec *specs.Spec) error { + bundleDir, err := testutil.SetupBundleDir(spec) + if err != nil { + return fmt.Errorf("SetupBundleDir failed: %v", err) + } + defer os.RemoveAll(bundleDir) + rootDir, err := testutil.SetupRootDir() if err != nil { - t.Fatalf("SetupRootDir failed: %v", err) + return fmt.Errorf("SetupRootDir failed: %v", err) } defer os.RemoveAll(rootDir) + name := tc.FullName() + id := testutil.UniqueContainerID() + log.Infof("Running test %q in container %q", name, id) + specutils.LogSpec(spec) + + args := []string{ + "-root", rootDir, + "-network=none", + "-log-format=text", + "-TESTONLY-unsafe-nonroot=true", + "-net-raw=true", + fmt.Sprintf("-panic-signal=%d", syscall.SIGTERM), + "-watchdog-action=panic", + "-platform", *platform, + "-file-access", *fileAccess, + } + if *overlay { + args = append(args, "-overlay") + } + if *debug { + args = append(args, "-debug", "-log-packets=true") + } + if *strace { + args = append(args, "-strace") + } + if *addUDSTree { + args = append(args, "-fsgofer-host-uds") + } + + if outDir, ok := syscall.Getenv("TEST_UNDECLARED_OUTPUTS_DIR"); ok { + tdir := filepath.Join(outDir, strings.Replace(name, "/", "_", -1)) + if err := os.MkdirAll(tdir, 0755); err != nil { + return fmt.Errorf("could not create test dir: %v", err) + } + debugLogDir, err := ioutil.TempDir(tdir, "runsc") + if err != nil { + return fmt.Errorf("could not create temp dir: %v", err) + } + debugLogDir += "/" + log.Infof("runsc logs: %s", debugLogDir) + args = append(args, "-debug-log", debugLogDir) + + // Default -log sends messages to stderr which makes reading the test log + // difficult. Instead, drop them when debug log is enabled given it's a + // better place for these messages. + args = append(args, "-log=/dev/null") + } + + // Current process doesn't have CAP_SYS_ADMIN, create user namespace and run + // as root inside that namespace to get it. + rArgs := append(args, "run", "--bundle", bundleDir, id) + cmd := exec.Command(*runscPath, rArgs...) + cmd.SysProcAttr = &syscall.SysProcAttr{ + Cloneflags: syscall.CLONE_NEWUSER | syscall.CLONE_NEWNS, + // Set current user/group as root inside the namespace. + UidMappings: []syscall.SysProcIDMap{ + {ContainerID: 0, HostID: os.Getuid(), Size: 1}, + }, + GidMappings: []syscall.SysProcIDMap{ + {ContainerID: 0, HostID: os.Getgid(), Size: 1}, + }, + GidMappingsEnableSetgroups: false, + Credential: &syscall.Credential{ + Uid: 0, + Gid: 0, + }, + } + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGTERM) + go func() { + s, ok := <-sig + if !ok { + return + } + log.Warningf("%s: Got signal: %v", name, s) + done := make(chan bool) + go func() { + dArgs := append(args, "-alsologtostderr=true", "debug", "--stacks", id) + cmd := exec.Command(*runscPath, dArgs...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Run() + done <- true + }() + + timeout := time.After(3 * time.Second) + select { + case <-timeout: + log.Infof("runsc debug --stacks is timeouted") + case <-done: + } + + log.Warningf("Send SIGTERM to the sandbox process") + dArgs := append(args, "debug", + fmt.Sprintf("--signal=%d", syscall.SIGTERM), + id) + cmd = exec.Command(*runscPath, dArgs...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Run() + }() + + err = cmd.Run() + + signal.Stop(sig) + close(sig) + + return err +} + +// setupUDSTree updates the spec to expose a UDS tree for gofer socket testing. +func setupUDSTree(spec *specs.Spec) (cleanup func(), err error) { + socketDir, cleanup, err := uds.CreateSocketTree("/tmp") + if err != nil { + return nil, fmt.Errorf("failed to create socket tree: %v", err) + } + + // Standard access to entire tree. + spec.Mounts = append(spec.Mounts, specs.Mount{ + Destination: "/tmp/sockets", + Source: socketDir, + Type: "bind", + }) + + // Individial attach points for each socket to test mounts that attach + // directly to the sockets. + spec.Mounts = append(spec.Mounts, specs.Mount{ + Destination: "/tmp/sockets-attach/stream/echo", + Source: filepath.Join(socketDir, "stream/echo"), + Type: "bind", + }) + spec.Mounts = append(spec.Mounts, specs.Mount{ + Destination: "/tmp/sockets-attach/stream/nonlistening", + Source: filepath.Join(socketDir, "stream/nonlistening"), + Type: "bind", + }) + spec.Mounts = append(spec.Mounts, specs.Mount{ + Destination: "/tmp/sockets-attach/seqpacket/echo", + Source: filepath.Join(socketDir, "seqpacket/echo"), + Type: "bind", + }) + spec.Mounts = append(spec.Mounts, specs.Mount{ + Destination: "/tmp/sockets-attach/seqpacket/nonlistening", + Source: filepath.Join(socketDir, "seqpacket/nonlistening"), + Type: "bind", + }) + spec.Mounts = append(spec.Mounts, specs.Mount{ + Destination: "/tmp/sockets-attach/dgram/null", + Source: filepath.Join(socketDir, "dgram/null"), + Type: "bind", + }) + + spec.Process.Env = append(spec.Process.Env, "TEST_UDS_TREE=/tmp/sockets") + spec.Process.Env = append(spec.Process.Env, "TEST_UDS_ATTACH_TREE=/tmp/sockets-attach") + + return cleanup, nil +} + +// runsTestCaseRunsc runs the test case in runsc. +func runTestCaseRunsc(testBin string, tc gtest.TestCase, t *testing.T) { // Run a new container with the test executable and filter for the // given test suite and name. spec := testutil.NewSpecWithArgs(testBin, gtest.FilterTestFlag+"="+tc.FullName()) @@ -171,115 +359,17 @@ func runTestCaseRunsc(testBin string, tc gtest.TestCase, t *testing.T) { spec.Process.Env = env - bundleDir, err := testutil.SetupBundleDir(spec) - if err != nil { - t.Fatalf("SetupBundleDir failed: %v", err) - } - defer os.RemoveAll(bundleDir) - - id := testutil.UniqueContainerID() - log.Infof("Running test %q in container %q", tc.FullName(), id) - specutils.LogSpec(spec) - - args := []string{ - "-platform", *platform, - "-root", rootDir, - "-file-access", *fileAccess, - "-network=none", - "-log-format=text", - "-TESTONLY-unsafe-nonroot=true", - "-net-raw=true", - fmt.Sprintf("-panic-signal=%d", syscall.SIGTERM), - "-watchdog-action=panic", - } - if *overlay { - args = append(args, "-overlay") - } - if *debug { - args = append(args, "-debug", "-log-packets=true") - } - if *strace { - args = append(args, "-strace") - } - if outDir, ok := syscall.Getenv("TEST_UNDECLARED_OUTPUTS_DIR"); ok { - tdir := filepath.Join(outDir, strings.Replace(tc.FullName(), "/", "_", -1)) - if err := os.MkdirAll(tdir, 0755); err != nil { - t.Fatalf("could not create test dir: %v", err) - } - debugLogDir, err := ioutil.TempDir(tdir, "runsc") + if *addUDSTree { + cleanup, err := setupUDSTree(spec) if err != nil { - t.Fatalf("could not create temp dir: %v", err) + t.Fatalf("error creating UDS tree: %v", err) } - debugLogDir += "/" - log.Infof("runsc logs: %s", debugLogDir) - args = append(args, "-debug-log", debugLogDir) - - // Default -log sends messages to stderr which makes reading the test log - // difficult. Instead, drop them when debug log is enabled given it's a - // better place for these messages. - args = append(args, "-log=/dev/null") + defer cleanup() } - // Current process doesn't have CAP_SYS_ADMIN, create user namespace and run - // as root inside that namespace to get it. - rArgs := append(args, "run", "--bundle", bundleDir, id) - cmd := exec.Command(*runscPath, rArgs...) - cmd.SysProcAttr = &syscall.SysProcAttr{ - Cloneflags: syscall.CLONE_NEWUSER | syscall.CLONE_NEWNS, - // Set current user/group as root inside the namespace. - UidMappings: []syscall.SysProcIDMap{ - {ContainerID: 0, HostID: os.Getuid(), Size: 1}, - }, - GidMappings: []syscall.SysProcIDMap{ - {ContainerID: 0, HostID: os.Getgid(), Size: 1}, - }, - GidMappingsEnableSetgroups: false, - Credential: &syscall.Credential{ - Uid: 0, - Gid: 0, - }, + if err := runRunsc(tc, spec); err != nil { + t.Errorf("test %q failed with error %v, want nil", tc.FullName(), err) } - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - sig := make(chan os.Signal, 1) - signal.Notify(sig, syscall.SIGTERM) - go func() { - s, ok := <-sig - if !ok { - return - } - t.Errorf("%s: Got signal: %v", tc.FullName(), s) - done := make(chan bool) - go func() { - dArgs := append(args, "-alsologtostderr=true", "debug", "--stacks", id) - cmd := exec.Command(*runscPath, dArgs...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Run() - done <- true - }() - - timeout := time.Tick(3 * time.Second) - select { - case <-timeout: - t.Logf("runsc debug --stacks is timeouted") - case <-done: - } - - t.Logf("Send SIGTERM to the sandbox process") - dArgs := append(args, "debug", - fmt.Sprintf("--signal=%d", syscall.SIGTERM), - id) - cmd = exec.Command(*runscPath, dArgs...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Run() - }() - if err = cmd.Run(); err != nil { - t.Errorf("test %q exited with status %v, want 0", tc.FullName(), err) - } - signal.Stop(sig) - close(sig) } // filterEnv returns an environment with the blacklisted variables removed. diff --git a/test/uds/BUILD b/test/uds/BUILD new file mode 100644 index 000000000..a3843e699 --- /dev/null +++ b/test/uds/BUILD @@ -0,0 +1,17 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +package( + default_visibility = ["//:sandbox"], + licenses = ["notice"], +) + +go_library( + name = "uds", + testonly = 1, + srcs = ["uds.go"], + importpath = "gvisor.dev/gvisor/test/uds", + deps = [ + "//pkg/log", + "//pkg/unet", + ], +) diff --git a/test/uds/uds.go b/test/uds/uds.go new file mode 100644 index 000000000..b714c61b0 --- /dev/null +++ b/test/uds/uds.go @@ -0,0 +1,228 @@ +// Copyright 2019 The gVisor Authors. +// +// 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 uds contains helpers for testing external UDS functionality. +package uds + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "syscall" + + "gvisor.dev/gvisor/pkg/log" + "gvisor.dev/gvisor/pkg/unet" +) + +// createEchoSocket creates a socket that echoes back anything received. +// +// Only works for stream, seqpacket sockets. +func createEchoSocket(path string, protocol int) (cleanup func(), err error) { + fd, err := syscall.Socket(syscall.AF_UNIX, protocol, 0) + if err != nil { + return nil, fmt.Errorf("error creating echo(%d) socket: %v", protocol, err) + } + + if err := syscall.Bind(fd, &syscall.SockaddrUnix{Name: path}); err != nil { + return nil, fmt.Errorf("error binding echo(%d) socket: %v", protocol, err) + } + + if err := syscall.Listen(fd, 0); err != nil { + return nil, fmt.Errorf("error listening echo(%d) socket: %v", protocol, err) + } + + server, err := unet.NewServerSocket(fd) + if err != nil { + return nil, fmt.Errorf("error creating echo(%d) unet socket: %v", protocol, err) + } + + acceptAndEchoOne := func() error { + s, err := server.Accept() + if err != nil { + return fmt.Errorf("failed to accept: %v", err) + } + defer s.Close() + + for { + buf := make([]byte, 512) + for { + n, err := s.Read(buf) + if err == io.EOF { + return nil + } + if err != nil { + return fmt.Errorf("failed to read: %d, %v", n, err) + } + + n, err = s.Write(buf[:n]) + if err != nil { + return fmt.Errorf("failed to write: %d, %v", n, err) + } + } + } + } + + go func() { + for { + if err := acceptAndEchoOne(); err != nil { + log.Warningf("Failed to handle echo(%d) socket: %v", protocol, err) + return + } + } + }() + + cleanup = func() { + if err := server.Close(); err != nil { + log.Warningf("Failed to close echo(%d) socket: %v", protocol, err) + } + } + + return cleanup, nil +} + +// createNonListeningSocket creates a socket that is bound but not listening. +// +// Only relevant for stream, seqpacket sockets. +func createNonListeningSocket(path string, protocol int) (cleanup func(), err error) { + fd, err := syscall.Socket(syscall.AF_UNIX, protocol, 0) + if err != nil { + return nil, fmt.Errorf("error creating nonlistening(%d) socket: %v", protocol, err) + } + + if err := syscall.Bind(fd, &syscall.SockaddrUnix{Name: path}); err != nil { + return nil, fmt.Errorf("error binding nonlistening(%d) socket: %v", protocol, err) + } + + cleanup = func() { + if err := syscall.Close(fd); err != nil { + log.Warningf("Failed to close nonlistening(%d) socket: %v", protocol, err) + } + } + + return cleanup, nil +} + +// createNullSocket creates a socket that reads anything received. +// +// Only works for dgram sockets. +func createNullSocket(path string, protocol int) (cleanup func(), err error) { + fd, err := syscall.Socket(syscall.AF_UNIX, protocol, 0) + if err != nil { + return nil, fmt.Errorf("error creating null(%d) socket: %v", protocol, err) + } + + if err := syscall.Bind(fd, &syscall.SockaddrUnix{Name: path}); err != nil { + return nil, fmt.Errorf("error binding null(%d) socket: %v", protocol, err) + } + + s, err := unet.NewSocket(fd) + if err != nil { + return nil, fmt.Errorf("error creating null(%d) unet socket: %v", protocol, err) + } + + go func() { + buf := make([]byte, 512) + for { + n, err := s.Read(buf) + if err != nil { + log.Warningf("failed to read: %d, %v", n, err) + return + } + } + }() + + cleanup = func() { + if err := s.Close(); err != nil { + log.Warningf("Failed to close null(%d) socket: %v", protocol, err) + } + } + + return cleanup, nil +} + +type socketCreator func(path string, proto int) (cleanup func(), err error) + +// CreateSocketTree creates a local tree of unix domain sockets for use in +// testing: +// * /stream/echo +// * /stream/nonlistening +// * /seqpacket/echo +// * /seqpacket/nonlistening +// * /dgram/null +func CreateSocketTree(baseDir string) (dir string, cleanup func(), err error) { + dir, err = ioutil.TempDir(baseDir, "sockets") + if err != nil { + return "", nil, fmt.Errorf("error creating temp dir: %v", err) + } + + var protocols = []struct { + protocol int + name string + sockets map[string]socketCreator + }{ + { + protocol: syscall.SOCK_STREAM, + name: "stream", + sockets: map[string]socketCreator{ + "echo": createEchoSocket, + "nonlistening": createNonListeningSocket, + }, + }, + { + protocol: syscall.SOCK_SEQPACKET, + name: "seqpacket", + sockets: map[string]socketCreator{ + "echo": createEchoSocket, + "nonlistening": createNonListeningSocket, + }, + }, + { + protocol: syscall.SOCK_DGRAM, + name: "dgram", + sockets: map[string]socketCreator{ + "null": createNullSocket, + }, + }, + } + + var cleanups []func() + for _, proto := range protocols { + protoDir := filepath.Join(dir, proto.name) + if err := os.Mkdir(protoDir, 0755); err != nil { + return "", nil, fmt.Errorf("error creating %s dir: %v", proto.name, err) + } + + for name, fn := range proto.sockets { + path := filepath.Join(protoDir, name) + cleanup, err := fn(path, proto.protocol) + if err != nil { + return "", nil, fmt.Errorf("error creating %s %s socket: %v", proto.name, name, err) + } + + cleanups = append(cleanups, cleanup) + } + } + + cleanup = func() { + for _, c := range cleanups { + c() + } + + os.RemoveAll(dir) + } + + return dir, cleanup, nil +}