diff options
author | Fabricio Voznika <fvoznika@google.com> | 2021-01-29 13:54:34 -0800 |
---|---|---|
committer | gVisor bot <gvisor-bot@google.com> | 2021-01-29 13:56:17 -0800 |
commit | fdbfd447a02e52296f48a5cb1020030756ed8da6 (patch) | |
tree | 47735f8082f17e98118a268502a5fc7b72b99c11 | |
parent | 5e2edfb8726ddb255a02352e2f68ea028f543e4b (diff) |
Remove side effect from test cases
Individual test cases must not rely on being executed in a clean environment.
PiperOrigin-RevId: 354604389
-rw-r--r-- | test/syscalls/linux/BUILD | 6 | ||||
-rw-r--r-- | test/syscalls/linux/chroot.cc | 425 | ||||
-rw-r--r-- | test/syscalls/linux/getrusage.cc | 96 | ||||
-rw-r--r-- | test/syscalls/linux/proc_net_unix.cc | 3 | ||||
-rw-r--r-- | test/syscalls/linux/uidgid.cc | 107 | ||||
-rw-r--r-- | test/util/capability_util.h | 13 | ||||
-rw-r--r-- | test/util/logging.h | 15 |
7 files changed, 374 insertions, 291 deletions
diff --git a/test/syscalls/linux/BUILD b/test/syscalls/linux/BUILD index 2b4b6f348..e974a789a 100644 --- a/test/syscalls/linux/BUILD +++ b/test/syscalls/linux/BUILD @@ -482,7 +482,9 @@ cc_binary( "//test/util:fs_util", "@com_google_absl//absl/strings", gtest, + "//test/util:logging", "//test/util:mount_util", + "//test/util:multiprocess_util", "//test/util:temp_path", "//test/util:test_main", "//test/util:test_util", @@ -672,6 +674,7 @@ cc_binary( gtest, "//test/util:logging", "//test/util:memory_util", + "//test/util:multiprocess_util", "//test/util:signal_util", "//test/util:test_main", "//test/util:test_util", @@ -3827,6 +3830,8 @@ cc_binary( "@com_google_absl//absl/flags:flag", "@com_google_absl//absl/strings", gtest, + "//test/util:cleanup", + "//test/util:multiprocess_util", "//test/util:posix_error", "//test/util:test_main", "//test/util:test_util", @@ -4082,6 +4087,7 @@ cc_binary( "@com_google_absl//absl/strings", "@com_google_absl//absl/strings:str_format", gtest, + "//test/util:cleanup", "//test/util:test_main", "//test/util:test_util", ], diff --git a/test/syscalls/linux/chroot.cc b/test/syscalls/linux/chroot.cc index 85ec013d5..fab79d300 100644 --- a/test/syscalls/linux/chroot.cc +++ b/test/syscalls/linux/chroot.cc @@ -32,7 +32,9 @@ #include "test/util/cleanup.h" #include "test/util/file_descriptor.h" #include "test/util/fs_util.h" +#include "test/util/logging.h" #include "test/util/mount_util.h" +#include "test/util/multiprocess_util.h" #include "test/util/temp_path.h" #include "test/util/test_util.h" @@ -47,17 +49,20 @@ namespace { TEST(ChrootTest, Success) { SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_CHROOT))); - auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); - EXPECT_THAT(chroot(temp_dir.path().c_str()), SyscallSucceeds()); + const auto rest = [] { + auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + TEST_CHECK_SUCCESS(chroot(temp_dir.path().c_str())); + }; + EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0)); } TEST(ChrootTest, PermissionDenied) { SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_CHROOT))); - // CAP_DAC_READ_SEARCH and CAP_DAC_OVERRIDE may override Execute permission on - // directories. - ASSERT_NO_ERRNO(SetCapability(CAP_DAC_READ_SEARCH, false)); - ASSERT_NO_ERRNO(SetCapability(CAP_DAC_OVERRIDE, false)); + // CAP_DAC_READ_SEARCH and CAP_DAC_OVERRIDE may override Execute permission + // on directories. + AutoCapability cap_search(CAP_DAC_READ_SEARCH, false); + AutoCapability cap_override(CAP_DAC_OVERRIDE, false); auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE( TempPath::CreateDirWith(GetAbsoluteTestTmpdir(), 0666 /* mode */)); @@ -78,8 +83,10 @@ TEST(ChrootTest, NotExist) { } TEST(ChrootTest, WithoutCapability) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SETPCAP))); + // Unset CAP_SYS_CHROOT. - ASSERT_NO_ERRNO(SetCapability(CAP_SYS_CHROOT, false)); + AutoCapability cap(CAP_SYS_CHROOT, false); auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); EXPECT_THAT(chroot(temp_dir.path().c_str()), SyscallFailsWithErrno(EPERM)); @@ -97,51 +104,53 @@ TEST(ChrootTest, CreatesNewRoot) { auto file_in_new_root = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileIn(new_root.path())); - // chroot into new_root. - ASSERT_THAT(chroot(new_root.path().c_str()), SyscallSucceeds()); - - // getcwd should return "(unreachable)" followed by the initial_cwd. - char cwd[1024]; - ASSERT_THAT(syscall(__NR_getcwd, cwd, sizeof(cwd)), SyscallSucceeds()); - std::string expected_cwd = "(unreachable)"; - expected_cwd += initial_cwd; - EXPECT_STREQ(cwd, expected_cwd.c_str()); - - // Should not be able to stat file by its full path. - struct stat statbuf; - EXPECT_THAT(stat(file_in_new_root.path().c_str(), &statbuf), - SyscallFailsWithErrno(ENOENT)); - - // Should be able to stat file at new rooted path. - auto basename = std::string(Basename(file_in_new_root.path())); - auto rootedFile = "/" + basename; - ASSERT_THAT(stat(rootedFile.c_str(), &statbuf), SyscallSucceeds()); - - // Should be able to stat cwd at '.' even though it's outside root. - ASSERT_THAT(stat(".", &statbuf), SyscallSucceeds()); - - // chdir into new root. - ASSERT_THAT(chdir("/"), SyscallSucceeds()); - - // getcwd should return "/". - EXPECT_THAT(syscall(__NR_getcwd, cwd, sizeof(cwd)), SyscallSucceeds()); - EXPECT_STREQ(cwd, "/"); - - // Statting '.', '..', '/', and '/..' all return the same dev and inode. - struct stat statbuf_dot; - ASSERT_THAT(stat(".", &statbuf_dot), SyscallSucceeds()); - struct stat statbuf_dotdot; - ASSERT_THAT(stat("..", &statbuf_dotdot), SyscallSucceeds()); - EXPECT_EQ(statbuf_dot.st_dev, statbuf_dotdot.st_dev); - EXPECT_EQ(statbuf_dot.st_ino, statbuf_dotdot.st_ino); - struct stat statbuf_slash; - ASSERT_THAT(stat("/", &statbuf_slash), SyscallSucceeds()); - EXPECT_EQ(statbuf_dot.st_dev, statbuf_slash.st_dev); - EXPECT_EQ(statbuf_dot.st_ino, statbuf_slash.st_ino); - struct stat statbuf_slashdotdot; - ASSERT_THAT(stat("/..", &statbuf_slashdotdot), SyscallSucceeds()); - EXPECT_EQ(statbuf_dot.st_dev, statbuf_slashdotdot.st_dev); - EXPECT_EQ(statbuf_dot.st_ino, statbuf_slashdotdot.st_ino); + const auto rest = [&] { + // chroot into new_root. + TEST_CHECK_SUCCESS(chroot(new_root.path().c_str())); + + // getcwd should return "(unreachable)" followed by the initial_cwd. + char cwd[1024]; + TEST_CHECK_SUCCESS(syscall(__NR_getcwd, cwd, sizeof(cwd))); + std::string expected_cwd = "(unreachable)"; + expected_cwd += initial_cwd; + TEST_CHECK(strcmp(cwd, expected_cwd.c_str()) == 0); + + // Should not be able to stat file by its full path. + struct stat statbuf; + TEST_CHECK_ERRNO(stat(file_in_new_root.path().c_str(), &statbuf), ENOENT); + + // Should be able to stat file at new rooted path. + auto basename = std::string(Basename(file_in_new_root.path())); + auto rootedFile = "/" + basename; + TEST_CHECK_SUCCESS(stat(rootedFile.c_str(), &statbuf)); + + // Should be able to stat cwd at '.' even though it's outside root. + TEST_CHECK_SUCCESS(stat(".", &statbuf)); + + // chdir into new root. + TEST_CHECK_SUCCESS(chdir("/")); + + // getcwd should return "/". + TEST_CHECK_SUCCESS(syscall(__NR_getcwd, cwd, sizeof(cwd))); + TEST_CHECK_SUCCESS(strcmp(cwd, "/") == 0); + + // Statting '.', '..', '/', and '/..' all return the same dev and inode. + struct stat statbuf_dot; + TEST_CHECK_SUCCESS(stat(".", &statbuf_dot)); + struct stat statbuf_dotdot; + TEST_CHECK_SUCCESS(stat("..", &statbuf_dotdot)); + TEST_CHECK(statbuf_dot.st_dev == statbuf_dotdot.st_dev); + TEST_CHECK(statbuf_dot.st_ino == statbuf_dotdot.st_ino); + struct stat statbuf_slash; + TEST_CHECK_SUCCESS(stat("/", &statbuf_slash)); + TEST_CHECK(statbuf_dot.st_dev == statbuf_slash.st_dev); + TEST_CHECK(statbuf_dot.st_ino == statbuf_slash.st_ino); + struct stat statbuf_slashdotdot; + TEST_CHECK_SUCCESS(stat("/..", &statbuf_slashdotdot)); + TEST_CHECK(statbuf_dot.st_dev == statbuf_slashdotdot.st_dev); + TEST_CHECK(statbuf_dot.st_ino == statbuf_slashdotdot.st_ino); + }; + EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0)); } TEST(ChrootTest, DotDotFromOpenFD) { @@ -152,18 +161,20 @@ TEST(ChrootTest, DotDotFromOpenFD) { Open(dir_outside_root.path(), O_RDONLY | O_DIRECTORY)); auto new_root = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); - // chroot into new_root. - ASSERT_THAT(chroot(new_root.path().c_str()), SyscallSucceeds()); + const auto rest = [&] { + // chroot into new_root. + TEST_CHECK_SUCCESS(chroot(new_root.path().c_str())); - // openat on fd with path .. will succeed. - int other_fd; - ASSERT_THAT(other_fd = openat(fd.get(), "..", O_RDONLY), SyscallSucceeds()); - EXPECT_THAT(close(other_fd), SyscallSucceeds()); + // openat on fd with path .. will succeed. + int other_fd; + TEST_CHECK_SUCCESS(other_fd = openat(fd.get(), "..", O_RDONLY)); + TEST_CHECK_SUCCESS(close(other_fd)); - // getdents on fd should not error. - char buf[1024]; - ASSERT_THAT(syscall(SYS_getdents64, fd.get(), buf, sizeof(buf)), - SyscallSucceeds()); + // getdents on fd should not error. + char buf[1024]; + TEST_CHECK_SUCCESS(syscall(SYS_getdents64, fd.get(), buf, sizeof(buf))); + }; + EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0)); } // Test that link resolution in a chroot can escape the root by following an @@ -179,24 +190,27 @@ TEST(ChrootTest, ProcFdLinkResolutionInChroot) { const FileDescriptor proc_fd = ASSERT_NO_ERRNO_AND_VALUE( Open("/proc", O_DIRECTORY | O_RDONLY | O_CLOEXEC)); - auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); - ASSERT_THAT(chroot(temp_dir.path().c_str()), SyscallSucceeds()); - - // Opening relative to an already open fd to a node outside the chroot works. - const FileDescriptor proc_self_fd = ASSERT_NO_ERRNO_AND_VALUE( - OpenAt(proc_fd.get(), "self/fd", O_DIRECTORY | O_RDONLY | O_CLOEXEC)); - - // Proc fd symlinks can escape the chroot if the fd the symlink refers to - // refers to an object outside the chroot. - struct stat s = {}; - EXPECT_THAT( - fstatat(proc_self_fd.get(), absl::StrCat(fd.get()).c_str(), &s, 0), - SyscallSucceeds()); - - // Try to stat the stdin fd. Internally, this is handled differently from a - // proc fd entry pointing to a file, since stdin is backed by a host fd, and - // isn't a walkable path on the filesystem inside the sandbox. - EXPECT_THAT(fstatat(proc_self_fd.get(), "0", &s, 0), SyscallSucceeds()); + const auto rest = [&] { + auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + TEST_CHECK_SUCCESS(chroot(temp_dir.path().c_str())); + + // Opening relative to an already open fd to a node outside the chroot + // works. + const FileDescriptor proc_self_fd = TEST_CHECK_NO_ERRNO_AND_VALUE( + OpenAt(proc_fd.get(), "self/fd", O_DIRECTORY | O_RDONLY | O_CLOEXEC)); + + // Proc fd symlinks can escape the chroot if the fd the symlink refers to + // refers to an object outside the chroot. + struct stat s = {}; + TEST_CHECK_SUCCESS( + fstatat(proc_self_fd.get(), absl::StrCat(fd.get()).c_str(), &s, 0)); + + // Try to stat the stdin fd. Internally, this is handled differently from a + // proc fd entry pointing to a file, since stdin is backed by a host fd, and + // isn't a walkable path on the filesystem inside the sandbox. + TEST_CHECK_SUCCESS(fstatat(proc_self_fd.get(), "0", &s, 0)); + }; + EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0)); } // This test will verify that when you hold a fd to proc before entering @@ -209,28 +223,30 @@ TEST(ChrootTest, ProcMemSelfFdsNoEscapeProcOpen) { const FileDescriptor proc = ASSERT_NO_ERRNO_AND_VALUE(Open("/proc", O_RDONLY)); - // Create and enter a chroot directory. - const auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); - ASSERT_THAT(chroot(temp_dir.path().c_str()), SyscallSucceeds()); - - // Open a file inside the chroot at /foo. - const FileDescriptor foo = - ASSERT_NO_ERRNO_AND_VALUE(Open("/foo", O_CREAT | O_RDONLY, 0644)); - - // Examine /proc/self/fd/{foo_fd} to see if it exposes the fact that we're - // inside a chroot, the path should be /foo and NOT {chroot_dir}/foo. - const std::string fd_path = absl::StrCat("self/fd/", foo.get()); - char buf[1024] = {}; - size_t bytes_read = 0; - ASSERT_THAT(bytes_read = - readlinkat(proc.get(), fd_path.c_str(), buf, sizeof(buf) - 1), - SyscallSucceeds()); - - // The link should resolve to something. - ASSERT_GT(bytes_read, 0); - - // Assert that the link doesn't contain the chroot path and is only /foo. - EXPECT_STREQ(buf, "/foo"); + const auto rest = [&] { + // Create and enter a chroot directory. + const auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + TEST_CHECK_SUCCESS(chroot(temp_dir.path().c_str())); + + // Open a file inside the chroot at /foo. + const FileDescriptor foo = + TEST_CHECK_NO_ERRNO_AND_VALUE(Open("/foo", O_CREAT | O_RDONLY, 0644)); + + // Examine /proc/self/fd/{foo_fd} to see if it exposes the fact that we're + // inside a chroot, the path should be /foo and NOT {chroot_dir}/foo. + const std::string fd_path = absl::StrCat("self/fd/", foo.get()); + char buf[1024] = {}; + size_t bytes_read = 0; + TEST_CHECK_SUCCESS(bytes_read = readlinkat(proc.get(), fd_path.c_str(), buf, + sizeof(buf) - 1)); + + // The link should resolve to something. + TEST_CHECK(bytes_read > 0); + + // Assert that the link doesn't contain the chroot path and is only /foo. + TEST_CHECK(strcmp(buf, "/foo") == 0); + }; + EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0)); } // This test will verify that a file inside a chroot when mmapped will not @@ -242,39 +258,41 @@ TEST(ChrootTest, ProcMemSelfMapsNoEscapeProcOpen) { const FileDescriptor proc = ASSERT_NO_ERRNO_AND_VALUE(Open("/proc", O_RDONLY)); - // Create and enter a chroot directory. - const auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); - ASSERT_THAT(chroot(temp_dir.path().c_str()), SyscallSucceeds()); - - // Open a file inside the chroot at /foo. - const FileDescriptor foo = - ASSERT_NO_ERRNO_AND_VALUE(Open("/foo", O_CREAT | O_RDONLY, 0644)); - - // Mmap the newly created file. - void* foo_map = mmap(nullptr, kPageSize, PROT_READ | PROT_WRITE, MAP_PRIVATE, - foo.get(), 0); - ASSERT_THAT(reinterpret_cast<int64_t>(foo_map), SyscallSucceeds()); - - // Always unmap. - auto cleanup_map = Cleanup( - [&] { EXPECT_THAT(munmap(foo_map, kPageSize), SyscallSucceeds()); }); - - // Examine /proc/self/maps to be sure that /foo doesn't appear to be - // mapped with the full chroot path. - const FileDescriptor maps = - ASSERT_NO_ERRNO_AND_VALUE(OpenAt(proc.get(), "self/maps", O_RDONLY)); - - size_t bytes_read = 0; - char buf[8 * 1024] = {}; - ASSERT_THAT(bytes_read = ReadFd(maps.get(), buf, sizeof(buf)), - SyscallSucceeds()); - - // The maps file should have something. - ASSERT_GT(bytes_read, 0); - - // Finally we want to make sure the maps don't contain the chroot path - ASSERT_EQ(std::string(buf, bytes_read).find(temp_dir.path()), - std::string::npos); + const auto rest = [&] { + // Create and enter a chroot directory. + const auto temp_dir = TEST_CHECK_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + TEST_CHECK_SUCCESS(chroot(temp_dir.path().c_str())); + + // Open a file inside the chroot at /foo. + const FileDescriptor foo = + TEST_CHECK_NO_ERRNO_AND_VALUE(Open("/foo", O_CREAT | O_RDONLY, 0644)); + + // Mmap the newly created file. + void* foo_map = mmap(nullptr, kPageSize, PROT_READ | PROT_WRITE, + MAP_PRIVATE, foo.get(), 0); + TEST_CHECK_SUCCESS(reinterpret_cast<int64_t>(foo_map)); + + // Always unmap. + auto cleanup_map = + Cleanup([&] { TEST_CHECK_SUCCESS(munmap(foo_map, kPageSize)); }); + + // Examine /proc/self/maps to be sure that /foo doesn't appear to be + // mapped with the full chroot path. + const FileDescriptor maps = TEST_CHECK_NO_ERRNO_AND_VALUE( + OpenAt(proc.get(), "self/maps", O_RDONLY)); + + size_t bytes_read = 0; + char buf[8 * 1024] = {}; + TEST_CHECK_SUCCESS(bytes_read = ReadFd(maps.get(), buf, sizeof(buf))); + + // The maps file should have something. + TEST_CHECK(bytes_read > 0); + + // Finally we want to make sure the maps don't contain the chroot path + TEST_CHECK(std::string(buf, bytes_read).find(temp_dir.path()) == + std::string::npos); + }; + EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0)); } // Test that mounts outside the chroot will not appear in /proc/self/mounts or @@ -283,81 +301,76 @@ TEST(ChrootTest, ProcMountsMountinfoNoEscape) { SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_CHROOT))); - // We are going to create some mounts and then chroot. In order to be able to - // unmount the mounts after the test run, we must chdir to the root and use - // relative paths for all mounts. That way, as long as we never chdir into - // the new root, we can access the mounts via relative paths and unmount them. - ASSERT_THAT(chdir("/"), SyscallSucceeds()); - - // Create nested tmpfs mounts. Note the use of relative paths in Mount calls. + // Create nested tmpfs mounts. auto const outer_dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); - auto const outer_mount = ASSERT_NO_ERRNO_AND_VALUE(Mount( - "none", JoinPath(".", outer_dir.path()), "tmpfs", 0, "mode=0700", 0)); + auto const outer_mount = ASSERT_NO_ERRNO_AND_VALUE( + Mount("none", outer_dir.path(), "tmpfs", 0, "mode=0700", 0)); auto const inner_dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(outer_dir.path())); - auto const inner_mount = ASSERT_NO_ERRNO_AND_VALUE(Mount( - "none", JoinPath(".", inner_dir.path()), "tmpfs", 0, "mode=0700", 0)); - - // Filenames that will be checked for mounts, all relative to /proc dir. - std::string paths[3] = {"mounts", "self/mounts", "self/mountinfo"}; - - for (const std::string& path : paths) { - // We should have both inner and outer mounts. - const std::string contents = - ASSERT_NO_ERRNO_AND_VALUE(GetContents(JoinPath("/proc", path))); - EXPECT_THAT(contents, AllOf(HasSubstr(outer_dir.path()), - HasSubstr(inner_dir.path()))); - // We better have at least two mounts: the mounts we created plus the root. - std::vector<absl::string_view> submounts = - absl::StrSplit(contents, '\n', absl::SkipWhitespace()); - EXPECT_GT(submounts.size(), 2); - } - - // Get a FD to /proc before we enter the chroot. - const FileDescriptor proc = - ASSERT_NO_ERRNO_AND_VALUE(Open("/proc", O_RDONLY)); - - // Chroot to outer mount. - ASSERT_THAT(chroot(outer_dir.path().c_str()), SyscallSucceeds()); - - for (const std::string& path : paths) { - const FileDescriptor proc_file = - ASSERT_NO_ERRNO_AND_VALUE(OpenAt(proc.get(), path, O_RDONLY)); - - // Only two mounts visible from this chroot: the inner and outer. Both - // paths should be relative to the new chroot. - const std::string contents = - ASSERT_NO_ERRNO_AND_VALUE(GetContentsFD(proc_file.get())); - EXPECT_THAT(contents, - AllOf(HasSubstr(absl::StrCat(Basename(inner_dir.path()))), - Not(HasSubstr(outer_dir.path())), - Not(HasSubstr(inner_dir.path())))); - std::vector<absl::string_view> submounts = - absl::StrSplit(contents, '\n', absl::SkipWhitespace()); - EXPECT_EQ(submounts.size(), 2); - } - - // Chroot to inner mount. We must use an absolute path accessible to our - // chroot. - const std::string inner_dir_basename = - absl::StrCat("/", Basename(inner_dir.path())); - ASSERT_THAT(chroot(inner_dir_basename.c_str()), SyscallSucceeds()); - - for (const std::string& path : paths) { - const FileDescriptor proc_file = - ASSERT_NO_ERRNO_AND_VALUE(OpenAt(proc.get(), path, O_RDONLY)); - const std::string contents = - ASSERT_NO_ERRNO_AND_VALUE(GetContentsFD(proc_file.get())); - - // Only the inner mount visible from this chroot. - std::vector<absl::string_view> submounts = - absl::StrSplit(contents, '\n', absl::SkipWhitespace()); - EXPECT_EQ(submounts.size(), 1); - } - - // Chroot back to ".". - ASSERT_THAT(chroot("."), SyscallSucceeds()); + auto const inner_mount = ASSERT_NO_ERRNO_AND_VALUE( + Mount("none", inner_dir.path(), "tmpfs", 0, "mode=0700", 0)); + + const auto rest = [&outer_dir, &inner_dir] { + // Filenames that will be checked for mounts, all relative to /proc dir. + std::string paths[3] = {"mounts", "self/mounts", "self/mountinfo"}; + + for (const std::string& path : paths) { + // We should have both inner and outer mounts. + const std::string contents = + TEST_CHECK_NO_ERRNO_AND_VALUE(GetContents(JoinPath("/proc", path))); + EXPECT_THAT(contents, AllOf(HasSubstr(outer_dir.path()), + HasSubstr(inner_dir.path()))); + // We better have at least two mounts: the mounts we created plus the + // root. + std::vector<absl::string_view> submounts = + absl::StrSplit(contents, '\n', absl::SkipWhitespace()); + TEST_CHECK(submounts.size() > 2); + } + + // Get a FD to /proc before we enter the chroot. + const FileDescriptor proc = + TEST_CHECK_NO_ERRNO_AND_VALUE(Open("/proc", O_RDONLY)); + + // Chroot to outer mount. + TEST_CHECK_SUCCESS(chroot(outer_dir.path().c_str())); + + for (const std::string& path : paths) { + const FileDescriptor proc_file = + TEST_CHECK_NO_ERRNO_AND_VALUE(OpenAt(proc.get(), path, O_RDONLY)); + + // Only two mounts visible from this chroot: the inner and outer. Both + // paths should be relative to the new chroot. + const std::string contents = + TEST_CHECK_NO_ERRNO_AND_VALUE(GetContentsFD(proc_file.get())); + EXPECT_THAT(contents, + AllOf(HasSubstr(absl::StrCat(Basename(inner_dir.path()))), + Not(HasSubstr(outer_dir.path())), + Not(HasSubstr(inner_dir.path())))); + std::vector<absl::string_view> submounts = + absl::StrSplit(contents, '\n', absl::SkipWhitespace()); + TEST_CHECK(submounts.size() == 2); + } + + // Chroot to inner mount. We must use an absolute path accessible to our + // chroot. + const std::string inner_dir_basename = + absl::StrCat("/", Basename(inner_dir.path())); + TEST_CHECK_SUCCESS(chroot(inner_dir_basename.c_str())); + + for (const std::string& path : paths) { + const FileDescriptor proc_file = + TEST_CHECK_NO_ERRNO_AND_VALUE(OpenAt(proc.get(), path, O_RDONLY)); + const std::string contents = + TEST_CHECK_NO_ERRNO_AND_VALUE(GetContentsFD(proc_file.get())); + + // Only the inner mount visible from this chroot. + std::vector<absl::string_view> submounts = + absl::StrSplit(contents, '\n', absl::SkipWhitespace()); + TEST_CHECK(submounts.size() == 1); + } + }; + EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0)); } } // namespace diff --git a/test/syscalls/linux/getrusage.cc b/test/syscalls/linux/getrusage.cc index 0e51d42a8..e84cbfdc3 100644 --- a/test/syscalls/linux/getrusage.cc +++ b/test/syscalls/linux/getrusage.cc @@ -23,6 +23,7 @@ #include "absl/time/time.h" #include "test/util/logging.h" #include "test/util/memory_util.h" +#include "test/util/multiprocess_util.h" #include "test/util/signal_util.h" #include "test/util/test_util.h" @@ -93,59 +94,66 @@ TEST(GetrusageTest, Grandchild) { // Verifies that processes ignoring SIGCHLD do not have updated child maxrss // updated. TEST(GetrusageTest, IgnoreSIGCHLD) { - struct sigaction sa; - sa.sa_handler = SIG_IGN; - sa.sa_flags = 0; - auto cleanup = ASSERT_NO_ERRNO_AND_VALUE(ScopedSigaction(SIGCHLD, sa)); - pid_t pid = fork(); - if (pid == 0) { + const auto rest = [] { + struct sigaction sa; + sa.sa_handler = SIG_IGN; + sa.sa_flags = 0; + auto cleanup = TEST_CHECK_NO_ERRNO_AND_VALUE(ScopedSigaction(SIGCHLD, sa)); + pid_t pid = fork(); + if (pid == 0) { + struct rusage rusage_self; + TEST_PCHECK(getrusage(RUSAGE_SELF, &rusage_self) == 0); + // The child has consumed some memory. + TEST_CHECK(rusage_self.ru_maxrss != 0); + _exit(0); + } + TEST_CHECK_SUCCESS(pid); + int status; + TEST_CHECK_ERRNO(RetryEINTR(waitpid)(pid, &status, 0), ECHILD); struct rusage rusage_self; - TEST_PCHECK(getrusage(RUSAGE_SELF, &rusage_self) == 0); - // The child has consumed some memory. - TEST_CHECK(rusage_self.ru_maxrss != 0); - _exit(0); - } - ASSERT_THAT(pid, SyscallSucceeds()); - int status; - ASSERT_THAT(RetryEINTR(waitpid)(pid, &status, 0), - SyscallFailsWithErrno(ECHILD)); - struct rusage rusage_self; - ASSERT_THAT(getrusage(RUSAGE_SELF, &rusage_self), SyscallSucceeds()); - struct rusage rusage_children; - ASSERT_THAT(getrusage(RUSAGE_CHILDREN, &rusage_children), SyscallSucceeds()); - // The parent has consumed some memory. - EXPECT_GT(rusage_self.ru_maxrss, 0); - // The child's maxrss should not have propagated up. - EXPECT_EQ(rusage_children.ru_maxrss, 0); + TEST_CHECK_SUCCESS(getrusage(RUSAGE_SELF, &rusage_self)); + struct rusage rusage_children; + TEST_CHECK_SUCCESS(getrusage(RUSAGE_CHILDREN, &rusage_children)); + // The parent has consumed some memory. + TEST_CHECK(rusage_self.ru_maxrss > 0); + // The child's maxrss should not have propagated up. + TEST_CHECK(rusage_children.ru_maxrss == 0); + }; + // Execute inside a forked process so that rusage_children is clean. + EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0)); } // Verifies that zombie processes do not update their parent's maxrss. Only // reaped processes should do this. TEST(GetrusageTest, IgnoreZombie) { - pid_t pid = fork(); - if (pid == 0) { + const auto rest = [] { + pid_t pid = fork(); + if (pid == 0) { + struct rusage rusage_self; + TEST_PCHECK(getrusage(RUSAGE_SELF, &rusage_self) == 0); + struct rusage rusage_children; + TEST_PCHECK(getrusage(RUSAGE_CHILDREN, &rusage_children) == 0); + // The child has consumed some memory. + TEST_CHECK(rusage_self.ru_maxrss != 0); + // The child has no children of its own. + TEST_CHECK(rusage_children.ru_maxrss == 0); + _exit(0); + } + TEST_CHECK_SUCCESS(pid); + // Give the child time to exit. Because we don't call wait, the child should + // remain a zombie. + absl::SleepFor(absl::Seconds(5)); struct rusage rusage_self; - TEST_PCHECK(getrusage(RUSAGE_SELF, &rusage_self) == 0); + TEST_CHECK_SUCCESS(getrusage(RUSAGE_SELF, &rusage_self)); struct rusage rusage_children; - TEST_PCHECK(getrusage(RUSAGE_CHILDREN, &rusage_children) == 0); - // The child has consumed some memory. - TEST_CHECK(rusage_self.ru_maxrss != 0); - // The child has no children of its own. + TEST_CHECK_SUCCESS(getrusage(RUSAGE_CHILDREN, &rusage_children)); + // The parent has consumed some memory. + TEST_CHECK(rusage_self.ru_maxrss > 0); + // The child has consumed some memory, but hasn't been reaped. TEST_CHECK(rusage_children.ru_maxrss == 0); - _exit(0); - } - ASSERT_THAT(pid, SyscallSucceeds()); - // Give the child time to exit. Because we don't call wait, the child should - // remain a zombie. - absl::SleepFor(absl::Seconds(5)); - struct rusage rusage_self; - ASSERT_THAT(getrusage(RUSAGE_SELF, &rusage_self), SyscallSucceeds()); - struct rusage rusage_children; - ASSERT_THAT(getrusage(RUSAGE_CHILDREN, &rusage_children), SyscallSucceeds()); - // The parent has consumed some memory. - EXPECT_GT(rusage_self.ru_maxrss, 0); - // The child has consumed some memory, but hasn't been reaped. - EXPECT_EQ(rusage_children.ru_maxrss, 0); + }; + // Execute inside a forked process so that rusage_children is clean. + EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0)); } TEST(GetrusageTest, Wait4) { diff --git a/test/syscalls/linux/proc_net_unix.cc b/test/syscalls/linux/proc_net_unix.cc index 662c6feb2..d61d94309 100644 --- a/test/syscalls/linux/proc_net_unix.cc +++ b/test/syscalls/linux/proc_net_unix.cc @@ -18,6 +18,7 @@ #include "absl/strings/str_join.h" #include "absl/strings/str_split.h" #include "test/syscalls/linux/unix_domain_socket_test_util.h" +#include "test/util/cleanup.h" #include "test/util/file_descriptor.h" #include "test/util/fs_util.h" #include "test/util/test_util.h" @@ -341,6 +342,8 @@ TEST(ProcNetUnix, StreamSocketStateStateConnectedOnAccept) { int clientfd; ASSERT_THAT(clientfd = accept(sockets->first_fd(), nullptr, nullptr), SyscallSucceeds()); + auto cleanup = Cleanup( + [clientfd]() { ASSERT_THAT(close(clientfd), SyscallSucceeds()); }); // Find the entry for the accepted socket. UDS proc entries don't have a // remote address, so we distinguish the accepted socket from the listen diff --git a/test/syscalls/linux/uidgid.cc b/test/syscalls/linux/uidgid.cc index 64d6d0b8f..4139a18d8 100644 --- a/test/syscalls/linux/uidgid.cc +++ b/test/syscalls/linux/uidgid.cc @@ -23,6 +23,8 @@ #include "absl/strings/str_cat.h" #include "absl/strings/str_join.h" #include "test/util/capability_util.h" +#include "test/util/cleanup.h" +#include "test/util/multiprocess_util.h" #include "test/util/posix_error.h" #include "test/util/test_util.h" #include "test/util/thread_util.h" @@ -33,6 +35,16 @@ ABSL_FLAG(int32_t, scratch_uid2, 65533, "second scratch UID"); ABSL_FLAG(int32_t, scratch_gid1, 65534, "first scratch GID"); ABSL_FLAG(int32_t, scratch_gid2, 65533, "second scratch GID"); +// Force use of syscall instead of glibc set*id() wrappers because we want to +// apply to the current task only. libc sets all threads in a process because +// "POSIX requires that all threads in a process share the same credentials." +#define setuid USE_SYSCALL_INSTEAD +#define setgid USE_SYSCALL_INSTEAD +#define setreuid USE_SYSCALL_INSTEAD +#define setregid USE_SYSCALL_INSTEAD +#define setresuid USE_SYSCALL_INSTEAD +#define setresgid USE_SYSCALL_INSTEAD + using ::testing::UnorderedElementsAreArray; namespace gvisor { @@ -137,21 +149,31 @@ TEST(UidGidRootTest, Setuid) { TEST(UidGidRootTest, Setgid) { SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(IsRoot())); - EXPECT_THAT(setgid(-1), SyscallFailsWithErrno(EINVAL)); + EXPECT_THAT(syscall(SYS_setgid, -1), SyscallFailsWithErrno(EINVAL)); - const gid_t gid = absl::GetFlag(FLAGS_scratch_gid1); - ASSERT_THAT(setgid(gid), SyscallSucceeds()); - EXPECT_NO_ERRNO(CheckGIDs(gid, gid, gid)); + ScopedThread([&] { + const gid_t gid = absl::GetFlag(FLAGS_scratch_gid1); + EXPECT_THAT(syscall(SYS_setgid, gid), SyscallSucceeds()); + EXPECT_NO_ERRNO(CheckGIDs(gid, gid, gid)); + }); } TEST(UidGidRootTest, SetgidNotFromThreadGroupLeader) { +#pragma push_macro("allow_setgid") +#undef setgid + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(IsRoot())); + int old_gid = getgid(); + auto clean = Cleanup([old_gid] { setgid(old_gid); }); + const gid_t gid = absl::GetFlag(FLAGS_scratch_gid1); // NOTE(b/64676707): Do setgid in a separate thread so that we can test if // info.si_pid is set correctly. ScopedThread([gid] { ASSERT_THAT(setgid(gid), SyscallSucceeds()); }); EXPECT_NO_ERRNO(CheckGIDs(gid, gid, gid)); + +#pragma pop_macro("allow_setgid") } TEST(UidGidRootTest, Setreuid) { @@ -159,27 +181,25 @@ TEST(UidGidRootTest, Setreuid) { // "Supplying a value of -1 for either the real or effective user ID forces // the system to leave that ID unchanged." - setreuid(2) - EXPECT_THAT(setreuid(-1, -1), SyscallSucceeds()); + EXPECT_THAT(syscall(SYS_setreuid, -1, -1), SyscallSucceeds()); + EXPECT_NO_ERRNO(CheckUIDs(0, 0, 0)); // Do setuid in a separate thread so that after finishing this test, the - // process can still open files the test harness created before starting this - // test. Otherwise, the files are created by root (UID before the test), but - // cannot be opened by the `uid` set below after the test. After calling - // setuid(non-zero-UID), there is no way to get root privileges back. + // process can still open files the test harness created before starting + // this test. Otherwise, the files are created by root (UID before the + // test), but cannot be opened by the `uid` set below after the test. After + // calling setuid(non-zero-UID), there is no way to get root privileges + // back. ScopedThread([&] { const uid_t ruid = absl::GetFlag(FLAGS_scratch_uid1); const uid_t euid = absl::GetFlag(FLAGS_scratch_uid2); - // Use syscall instead of glibc setuid wrapper because we want this setuid - // call to only apply to this task. posix threads, however, require that all - // threads have the same UIDs, so using the setuid wrapper sets all threads' - // real UID. EXPECT_THAT(syscall(SYS_setreuid, ruid, euid), SyscallSucceeds()); // "If the real user ID is set or the effective user ID is set to a value - // not equal to the previous real user ID, the saved set-user-ID will be set - // to the new effective user ID." - setreuid(2) + // not equal to the previous real user ID, the saved set-user-ID will be + // set to the new effective user ID." - setreuid(2) EXPECT_NO_ERRNO(CheckUIDs(ruid, euid, euid)); }); } @@ -187,13 +207,15 @@ TEST(UidGidRootTest, Setreuid) { TEST(UidGidRootTest, Setregid) { SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(IsRoot())); - EXPECT_THAT(setregid(-1, -1), SyscallSucceeds()); + EXPECT_THAT(syscall(SYS_setregid, -1, -1), SyscallSucceeds()); EXPECT_NO_ERRNO(CheckGIDs(0, 0, 0)); - const gid_t rgid = absl::GetFlag(FLAGS_scratch_gid1); - const gid_t egid = absl::GetFlag(FLAGS_scratch_gid2); - ASSERT_THAT(setregid(rgid, egid), SyscallSucceeds()); - EXPECT_NO_ERRNO(CheckGIDs(rgid, egid, egid)); + ScopedThread([&] { + const gid_t rgid = absl::GetFlag(FLAGS_scratch_gid1); + const gid_t egid = absl::GetFlag(FLAGS_scratch_gid2); + ASSERT_THAT(syscall(SYS_setregid, rgid, egid), SyscallSucceeds()); + EXPECT_NO_ERRNO(CheckGIDs(rgid, egid, egid)); + }); } TEST(UidGidRootTest, Setresuid) { @@ -201,23 +223,24 @@ TEST(UidGidRootTest, Setresuid) { // "If one of the arguments equals -1, the corresponding value is not // changed." - setresuid(2) - EXPECT_THAT(setresuid(-1, -1, -1), SyscallSucceeds()); + EXPECT_THAT(syscall(SYS_setresuid, -1, -1, -1), SyscallSucceeds()); EXPECT_NO_ERRNO(CheckUIDs(0, 0, 0)); // Do setuid in a separate thread so that after finishing this test, the - // process can still open files the test harness created before starting this - // test. Otherwise, the files are created by root (UID before the test), but - // cannot be opened by the `uid` set below after the test. After calling - // setuid(non-zero-UID), there is no way to get root privileges back. + // process can still open files the test harness created before starting + // this test. Otherwise, the files are created by root (UID before the + // test), but cannot be opened by the `uid` set below after the test. After + // calling setuid(non-zero-UID), there is no way to get root privileges + // back. ScopedThread([&] { const uid_t ruid = 12345; const uid_t euid = 23456; const uid_t suid = 34567; // Use syscall instead of glibc setuid wrapper because we want this setuid - // call to only apply to this task. posix threads, however, require that all - // threads have the same UIDs, so using the setuid wrapper sets all threads' - // real UID. + // call to only apply to this task. posix threads, however, require that + // all threads have the same UIDs, so using the setuid wrapper sets all + // threads' real UID. EXPECT_THAT(syscall(SYS_setresuid, ruid, euid, suid), SyscallSucceeds()); EXPECT_NO_ERRNO(CheckUIDs(ruid, euid, suid)); }); @@ -226,14 +249,16 @@ TEST(UidGidRootTest, Setresuid) { TEST(UidGidRootTest, Setresgid) { SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(IsRoot())); - EXPECT_THAT(setresgid(-1, -1, -1), SyscallSucceeds()); + EXPECT_THAT(syscall(SYS_setresgid, -1, -1, -1), SyscallSucceeds()); EXPECT_NO_ERRNO(CheckGIDs(0, 0, 0)); - const gid_t rgid = 12345; - const gid_t egid = 23456; - const gid_t sgid = 34567; - ASSERT_THAT(setresgid(rgid, egid, sgid), SyscallSucceeds()); - EXPECT_NO_ERRNO(CheckGIDs(rgid, egid, sgid)); + ScopedThread([&] { + const gid_t rgid = 12345; + const gid_t egid = 23456; + const gid_t sgid = 34567; + ASSERT_THAT(syscall(SYS_setresgid, rgid, egid, sgid), SyscallSucceeds()); + EXPECT_NO_ERRNO(CheckGIDs(rgid, egid, sgid)); + }); } TEST(UidGidRootTest, Setgroups) { @@ -254,14 +279,14 @@ TEST(UidGidRootTest, Setuid_prlimit) { SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(IsRoot())); // Do seteuid in a separate thread so that after finishing this test, the - // process can still open files the test harness created before starting this - // test. Otherwise, the files are created by root (UID before the test), but - // cannot be opened by the `uid` set below after the test. + // process can still open files the test harness created before starting + // this test. Otherwise, the files are created by root (UID before the + // test), but cannot be opened by the `uid` set below after the test. ScopedThread([&] { - // Use syscall instead of glibc setuid wrapper because we want this seteuid - // call to only apply to this task. POSIX threads, however, require that all - // threads have the same UIDs, so using the seteuid wrapper sets all - // threads' UID. + // Use syscall instead of glibc setuid wrapper because we want this + // seteuid call to only apply to this task. POSIX threads, however, + // require that all threads have the same UIDs, so using the seteuid + // wrapper sets all threads' UID. EXPECT_THAT(syscall(SYS_setreuid, -1, 65534), SyscallSucceeds()); // Despite the UID change, we should be able to get our own limits. diff --git a/test/util/capability_util.h b/test/util/capability_util.h index bb9ea1fe5..a03bc7e05 100644 --- a/test/util/capability_util.h +++ b/test/util/capability_util.h @@ -96,6 +96,19 @@ inline PosixError DropPermittedCapability(int cap) { PosixErrorOr<bool> CanCreateUserNamespace(); +class AutoCapability { + public: + AutoCapability(int cap, bool set) : cap_(cap), set_(set) { + EXPECT_NO_ERRNO(SetCapability(cap_, set_)); + } + + ~AutoCapability() { EXPECT_NO_ERRNO(SetCapability(cap_, !set_)); } + + private: + int cap_; + bool set_; +}; + } // namespace testing } // namespace gvisor #endif // GVISOR_TEST_UTIL_CAPABILITY_UTIL_H_ diff --git a/test/util/logging.h b/test/util/logging.h index 9d224ea05..5c17f1233 100644 --- a/test/util/logging.h +++ b/test/util/logging.h @@ -96,6 +96,21 @@ void CheckFailure(const char* cond, size_t cond_size, const char* msg, std::move(_expr_result).ValueOrDie(); \ }) +// cond must be greater or equal than 0. Used to test result of syscalls. +// +// This macro is async-signal-safe. +#define TEST_CHECK_SUCCESS(cond) TEST_PCHECK((cond) >= 0) + +// cond must be -1 and errno must match errno_value. Used to test errors from +// syscalls. +// +// This macro is async-signal-safe. +#define TEST_CHECK_ERRNO(cond, errno_value) \ + do { \ + TEST_PCHECK((cond) == -1); \ + TEST_PCHECK_MSG(errno == (errno_value), #cond " expected " #errno_value); \ + } while (0) + } // namespace testing } // namespace gvisor |