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:
parent
8ae70f864d
commit
49b596b98d
|
@ -8,9 +8,6 @@ go_library(
|
|||
srcs = ["fd.go"],
|
||||
importpath = "gvisor.dev/gvisor/pkg/fd",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//pkg/unet",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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",
|
||||
],
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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.
|
||||
|
|
|
@ -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",
|
||||
],
|
||||
)
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue