Cleanup host UDS support

This change fixes several issues with the fsgofer host UDS support. Notably, it
adds support for SOCK_SEQPACKET and SOCK_DGRAM sockets [1]. It also fixes
unsafe use of unet.Socket, which could cause a panic if Socket.FD is called
when err != nil, and calls to Socket.FD with nothing to prevent the garbage
collector from destroying and closing the socket.

A set of tests is added to exercise host UDS access. This required extracting
most of the syscall test runner into a library that can be used by custom
tests.

Updates #235
Updates #1003

[1] N.B. SOCK_DGRAM sockets are likely not particularly useful, as a server can
only reply to a client that binds first. We don't allow bind, so these are
unlikely to be used.

PiperOrigin-RevId: 275558502
This commit is contained in:
Michael Pratt 2019-10-18 15:31:33 -07:00 committed by gVisor bot
parent 8ae70f864d
commit 49b596b98d
14 changed files with 707 additions and 128 deletions

View File

@ -8,9 +8,6 @@ go_library(
srcs = ["fd.go"],
importpath = "gvisor.dev/gvisor/pkg/fd",
visibility = ["//visibility:public"],
deps = [
"//pkg/unet",
],
)
go_test(

View File

@ -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

View File

@ -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{

View File

@ -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.

View File

@ -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",

View File

@ -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()

View File

@ -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",
],

View File

@ -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(

View File

@ -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,

View File

@ -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());

View File

@ -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 <errno.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/un.h>
#include <string>
#include <tuple>
#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<std::tuple<std::string, ProtocolSocket>>;
// 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<struct sockaddr *>(&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<struct sockaddr *>(&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<std::string>;
// 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<struct sockaddr *>(&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

View File

@ -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.

17
test/uds/BUILD Normal file
View File

@ -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",
],
)

228
test/uds/uds.go Normal file
View File

@ -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
}