summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorKevin Krakauer <krakauer@google.com>2021-01-29 14:46:41 -0800
committergVisor bot <gvisor-bot@google.com>2021-01-29 14:49:04 -0800
commit66aa6f3d4fb148145ab1b91c49f483d501185ff8 (patch)
tree0895cf198a8d241c2e630fea4fb7d7259d13302e
parentfdbfd447a02e52296f48a5cb1020030756ed8da6 (diff)
setgid directory syscall tests
PiperOrigin-RevId: 354615220
-rw-r--r--test/syscalls/BUILD4
-rw-r--r--test/syscalls/linux/BUILD18
-rw-r--r--test/syscalls/linux/setgid.cc370
3 files changed, 392 insertions, 0 deletions
diff --git a/test/syscalls/BUILD b/test/syscalls/BUILD
index 6ee2b73c1..e43f30ba3 100644
--- a/test/syscalls/BUILD
+++ b/test/syscalls/BUILD
@@ -557,6 +557,10 @@ syscall_test(
)
syscall_test(
+ test = "//test/syscalls/linux:setgid_test",
+)
+
+syscall_test(
add_overlay = True,
test = "//test/syscalls/linux:splice_test",
)
diff --git a/test/syscalls/linux/BUILD b/test/syscalls/linux/BUILD
index e974a789a..1cbe5479c 100644
--- a/test/syscalls/linux/BUILD
+++ b/test/syscalls/linux/BUILD
@@ -2146,6 +2146,24 @@ cc_binary(
)
cc_binary(
+ name = "setgid_test",
+ testonly = 1,
+ srcs = ["setgid.cc"],
+ linkstatic = 1,
+ deps = [
+ "//test/util:capability_util",
+ "//test/util:cleanup",
+ "//test/util:fs_util",
+ "//test/util:posix_error",
+ "//test/util:temp_path",
+ "//test/util:test_main",
+ "//test/util:test_util",
+ "@com_google_absl//absl/strings",
+ gtest,
+ ],
+)
+
+cc_binary(
name = "splice_test",
testonly = 1,
srcs = ["splice.cc"],
diff --git a/test/syscalls/linux/setgid.cc b/test/syscalls/linux/setgid.cc
new file mode 100644
index 000000000..bfd91ba4f
--- /dev/null
+++ b/test/syscalls/linux/setgid.cc
@@ -0,0 +1,370 @@
+// Copyright 2020 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 <limits.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "gtest/gtest.h"
+#include "test/util/capability_util.h"
+#include "test/util/cleanup.h"
+#include "test/util/fs_util.h"
+#include "test/util/posix_error.h"
+#include "test/util/temp_path.h"
+#include "test/util/test_util.h"
+
+namespace gvisor {
+namespace testing {
+
+namespace {
+
+constexpr int kDirmodeMask = 07777;
+constexpr int kDirmodeSgid = S_ISGID | 0777;
+constexpr int kDirmodeNoExec = S_ISGID | 0767;
+constexpr int kDirmodeNoSgid = 0777;
+
+// Sets effective GID and returns a Cleanup that restores the original.
+PosixErrorOr<Cleanup> Setegid(gid_t egid) {
+ gid_t old_gid = getegid();
+ if (setegid(egid) < 0) {
+ return PosixError(errno, absl::StrFormat("setegid(%d)", egid));
+ }
+ return Cleanup(
+ [old_gid]() { EXPECT_THAT(setegid(old_gid), SyscallSucceeds()); });
+}
+
+// Returns a pair of groups that the user is a member of.
+PosixErrorOr<std::pair<gid_t, gid_t>> Groups() {
+ // See whether the user is a member of at least 2 groups.
+ std::vector<gid_t> groups(64);
+ for (; groups.size() <= NGROUPS_MAX; groups.resize(groups.size() * 2)) {
+ int ngroups = getgroups(groups.size(), groups.data());
+ if (ngroups < 0 && errno == EINVAL) {
+ // Need a larger list.
+ continue;
+ }
+ if (ngroups < 0) {
+ return PosixError(errno, absl::StrFormat("getgroups(%d, %p)",
+ groups.size(), groups.data()));
+ }
+ if (ngroups >= 2) {
+ return std::pair<gid_t, gid_t>(groups[0], groups[1]);
+ }
+ // There aren't enough groups.
+ break;
+ }
+
+ // If we're root in the root user namespace, we can set our GID to whatever we
+ // want. Try that before giving up.
+ constexpr gid_t kGID1 = 1111;
+ constexpr gid_t kGID2 = 2222;
+ auto cleanup1 = Setegid(kGID1);
+ if (!cleanup1.ok()) {
+ return cleanup1.error();
+ }
+ auto cleanup2 = Setegid(kGID2);
+ if (!cleanup2.ok()) {
+ return cleanup2.error();
+ }
+ return std::pair<gid_t, gid_t>(kGID1, kGID2);
+}
+
+class SetgidDirTest : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ original_gid_ = getegid();
+
+ // TODO(b/175325250): Enable when setgid directories are supported.
+ SKIP_IF(IsRunningOnGvisor());
+ SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SETGID)));
+
+ temp_dir_ = ASSERT_NO_ERRNO_AND_VALUE(
+ TempPath::CreateDirWith(GetAbsoluteTestTmpdir(), 0777 /* mode */));
+ groups_ = ASSERT_NO_ERRNO_AND_VALUE(Groups());
+ }
+
+ void TearDown() override {
+ ASSERT_THAT(setegid(original_gid_), SyscallSucceeds());
+ }
+
+ void MkdirAsGid(gid_t gid, const std::string& path, mode_t mode) {
+ auto cleanup = ASSERT_NO_ERRNO_AND_VALUE(Setegid(gid));
+ ASSERT_THAT(mkdir(path.c_str(), mode), SyscallSucceeds());
+ }
+
+ PosixErrorOr<struct stat> Stat(const std::string& path) {
+ struct stat stats;
+ if (stat(path.c_str(), &stats) < 0) {
+ return PosixError(errno, absl::StrFormat("stat(%s, _)", path));
+ }
+ return stats;
+ }
+
+ PosixErrorOr<struct stat> Stat(const FileDescriptor& fd) {
+ struct stat stats;
+ if (fstat(fd.get(), &stats) < 0) {
+ return PosixError(errno, "fstat(_, _)");
+ }
+ return stats;
+ }
+
+ TempPath temp_dir_;
+ std::pair<gid_t, gid_t> groups_;
+ gid_t original_gid_;
+};
+
+// The control test. Files created with a given GID are owned by that group.
+TEST_F(SetgidDirTest, Control) {
+ // Set group to G1 and create a directory.
+ auto g1owned = JoinPath(temp_dir_.path(), "g1owned/");
+ ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.first, g1owned, 0777));
+
+ // Set group to G2, create a file in g1owned, and confirm that G2 owns it.
+ ASSERT_THAT(setegid(groups_.second), SyscallSucceeds());
+ FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(
+ Open(JoinPath(g1owned, "g2owned").c_str(), O_CREAT | O_RDWR, 0777));
+ struct stat stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(fd));
+ EXPECT_EQ(stats.st_gid, groups_.second);
+}
+
+// Setgid directories cause created files to inherit GID.
+TEST_F(SetgidDirTest, CreateFile) {
+ // Set group to G1, create a directory, and enable setgid.
+ auto g1owned = JoinPath(temp_dir_.path(), "g1owned/");
+ ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.first, g1owned, kDirmodeSgid));
+ ASSERT_THAT(chmod(g1owned.c_str(), kDirmodeSgid), SyscallSucceeds());
+
+ // Set group to G2, create a file, and confirm that G1 owns it.
+ ASSERT_THAT(setegid(groups_.second), SyscallSucceeds());
+ FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(
+ Open(JoinPath(g1owned, "g2created").c_str(), O_CREAT | O_RDWR, 0666));
+ struct stat stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(fd));
+ EXPECT_EQ(stats.st_gid, groups_.first);
+}
+
+// Setgid directories cause created directories to inherit GID.
+TEST_F(SetgidDirTest, CreateDir) {
+ // Set group to G1, create a directory, and enable setgid.
+ auto g1owned = JoinPath(temp_dir_.path(), "g1owned/");
+ ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.first, g1owned, kDirmodeSgid));
+ ASSERT_THAT(chmod(g1owned.c_str(), kDirmodeSgid), SyscallSucceeds());
+
+ // Set group to G2, create a directory, confirm that G1 owns it, and that the
+ // setgid bit is enabled.
+ auto g2created = JoinPath(g1owned, "g2created");
+ ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.second, g2created, 0666));
+ struct stat stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(g2created));
+ EXPECT_EQ(stats.st_gid, groups_.first);
+ EXPECT_EQ(stats.st_mode & S_ISGID, S_ISGID);
+}
+
+// Setgid directories with group execution disabled still cause GID inheritance.
+TEST_F(SetgidDirTest, NoGroupExec) {
+ // Set group to G1, create a directory, and enable setgid.
+ auto g1owned = JoinPath(temp_dir_.path(), "g1owned/");
+ ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.first, g1owned, kDirmodeNoExec));
+ ASSERT_THAT(chmod(g1owned.c_str(), kDirmodeNoExec), SyscallSucceeds());
+
+ // Set group to G2, create a directory, confirm that G2 owns it, and that the
+ // setgid bit is enabled.
+ auto g2created = JoinPath(g1owned, "g2created");
+ ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.second, g2created, 0666));
+ struct stat stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(g2created));
+ EXPECT_EQ(stats.st_gid, groups_.first);
+ EXPECT_EQ(stats.st_mode & S_ISGID, S_ISGID);
+}
+
+// Setting the setgid bit on directories with an existing file does not change
+// the file's group.
+TEST_F(SetgidDirTest, OldFile) {
+ // Set group to G1 and create a directory.
+ auto g1owned = JoinPath(temp_dir_.path(), "g1owned/");
+ ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.first, g1owned, kDirmodeNoSgid));
+ ASSERT_THAT(chmod(g1owned.c_str(), kDirmodeNoSgid), SyscallSucceeds());
+
+ // Set group to G2, create a file, confirm that G2 owns it.
+ ASSERT_THAT(setegid(groups_.second), SyscallSucceeds());
+ FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(
+ Open(JoinPath(g1owned, "g2created").c_str(), O_CREAT | O_RDWR, 0666));
+ struct stat stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(fd));
+ EXPECT_EQ(stats.st_gid, groups_.second);
+
+ // Enable setgid.
+ ASSERT_THAT(chmod(g1owned.c_str(), kDirmodeSgid), SyscallSucceeds());
+
+ // Confirm that the file's group is still G2.
+ stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(fd));
+ EXPECT_EQ(stats.st_gid, groups_.second);
+}
+
+// Setting the setgid bit on directories with an existing subdirectory does not
+// change the subdirectory's group.
+TEST_F(SetgidDirTest, OldDir) {
+ // Set group to G1, create a directory, and enable setgid.
+ auto g1owned = JoinPath(temp_dir_.path(), "g1owned/");
+ ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.first, g1owned, kDirmodeNoSgid));
+ ASSERT_THAT(chmod(g1owned.c_str(), kDirmodeNoSgid), SyscallSucceeds());
+
+ // Set group to G2, create a directory, confirm that G2 owns it.
+ ASSERT_THAT(setegid(groups_.second), SyscallSucceeds());
+ auto g2created = JoinPath(g1owned, "g2created");
+ ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.second, g2created, 0666));
+ struct stat stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(g2created));
+ EXPECT_EQ(stats.st_gid, groups_.second);
+
+ // Enable setgid.
+ ASSERT_THAT(chmod(g1owned.c_str(), kDirmodeSgid), SyscallSucceeds());
+
+ // Confirm that the file's group is still G2.
+ stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(g2created));
+ EXPECT_EQ(stats.st_gid, groups_.second);
+}
+
+// Chowning a file clears the setgid and setuid bits.
+TEST_F(SetgidDirTest, ChownFileClears) {
+ // Set group to G1, create a directory, and enable setgid.
+ auto g1owned = JoinPath(temp_dir_.path(), "g1owned/");
+ ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.first, g1owned, kDirmodeMask));
+ ASSERT_THAT(chmod(g1owned.c_str(), kDirmodeMask), SyscallSucceeds());
+
+ FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(
+ Open(JoinPath(g1owned, "newfile").c_str(), O_CREAT | O_RDWR, 0666));
+ ASSERT_THAT(fchmod(fd.get(), 0777 | S_ISUID | S_ISGID), SyscallSucceeds());
+ struct stat stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(fd));
+ EXPECT_EQ(stats.st_gid, groups_.first);
+ EXPECT_EQ(stats.st_mode & (S_ISUID | S_ISGID), S_ISUID | S_ISGID);
+
+ // Change the owning group.
+ ASSERT_THAT(fchown(fd.get(), -1, groups_.second), SyscallSucceeds());
+
+ // The setgid and setuid bits should be cleared.
+ stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(fd));
+ EXPECT_EQ(stats.st_gid, groups_.second);
+ EXPECT_EQ(stats.st_mode & (S_ISUID | S_ISGID), 0);
+}
+
+// Chowning a file with setgid enabled, but not the group exec bit, does not
+// clear the setgid bit. Such files are mandatory locked.
+TEST_F(SetgidDirTest, ChownNoExecFileDoesNotClear) {
+ // Set group to G1, create a directory, and enable setgid.
+ auto g1owned = JoinPath(temp_dir_.path(), "g1owned/");
+ ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.first, g1owned, kDirmodeNoExec));
+ ASSERT_THAT(chmod(g1owned.c_str(), kDirmodeNoExec), SyscallSucceeds());
+
+ FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(
+ Open(JoinPath(g1owned, "newdir").c_str(), O_CREAT | O_RDWR, 0666));
+ ASSERT_THAT(fchmod(fd.get(), 0766 | S_ISUID | S_ISGID), SyscallSucceeds());
+ struct stat stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(fd));
+ EXPECT_EQ(stats.st_gid, groups_.first);
+ EXPECT_EQ(stats.st_mode & (S_ISUID | S_ISGID), S_ISUID | S_ISGID);
+
+ // Change the owning group.
+ ASSERT_THAT(fchown(fd.get(), -1, groups_.second), SyscallSucceeds());
+
+ // Only the setuid bit is cleared.
+ stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(fd));
+ EXPECT_EQ(stats.st_gid, groups_.second);
+ EXPECT_EQ(stats.st_mode & (S_ISUID | S_ISGID), S_ISGID);
+}
+
+// Chowning a directory with setgid enabled does not clear the bit.
+TEST_F(SetgidDirTest, ChownDirDoesNotClear) {
+ // Set group to G1, create a directory, and enable setgid.
+ auto g1owned = JoinPath(temp_dir_.path(), "g1owned/");
+ ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.first, g1owned, kDirmodeMask));
+ ASSERT_THAT(chmod(g1owned.c_str(), kDirmodeMask), SyscallSucceeds());
+
+ // Change the owning group.
+ ASSERT_THAT(chown(g1owned.c_str(), -1, groups_.second), SyscallSucceeds());
+
+ struct stat stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(g1owned));
+ EXPECT_EQ(stats.st_gid, groups_.second);
+ EXPECT_EQ(stats.st_mode & kDirmodeMask, kDirmodeMask);
+}
+
+struct FileModeTestcase {
+ std::string name;
+ mode_t mode;
+ mode_t result_mode;
+
+ FileModeTestcase(const std::string& name, mode_t mode, mode_t result_mode)
+ : name(name), mode(mode), result_mode(result_mode) {}
+};
+
+class FileModeTest : public ::testing::TestWithParam<FileModeTestcase> {};
+
+TEST_P(FileModeTest, WriteToFile) {
+ // TODO(b/175325250): Enable when setgid directories are supported.
+ SKIP_IF(IsRunningOnGvisor());
+
+ auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(
+ TempPath::CreateDirWith(GetAbsoluteTestTmpdir(), 0777 /* mode */));
+ auto path = JoinPath(temp_dir.path(), GetParam().name);
+ FileDescriptor fd =
+ ASSERT_NO_ERRNO_AND_VALUE(Open(path.c_str(), O_CREAT | O_RDWR, 0666));
+ ASSERT_THAT(fchmod(fd.get(), GetParam().mode), SyscallSucceeds());
+ struct stat stats;
+ ASSERT_THAT(fstat(fd.get(), &stats), SyscallSucceeds());
+ EXPECT_EQ(stats.st_mode & kDirmodeMask, GetParam().mode);
+
+ // For security reasons, writing to the file clears the SUID bit, and clears
+ // the SGID bit when the group executable bit is unset (which is not a true
+ // SGID binary).
+ constexpr char kInput = 'M';
+ ASSERT_THAT(write(fd.get(), &kInput, sizeof(kInput)),
+ SyscallSucceedsWithValue(sizeof(kInput)));
+
+ ASSERT_THAT(fstat(fd.get(), &stats), SyscallSucceeds());
+ EXPECT_EQ(stats.st_mode & kDirmodeMask, GetParam().result_mode);
+}
+
+TEST_P(FileModeTest, TruncateFile) {
+ // TODO(b/175325250): Enable when setgid directories are supported.
+ SKIP_IF(IsRunningOnGvisor());
+
+ auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(
+ TempPath::CreateDirWith(GetAbsoluteTestTmpdir(), 0777 /* mode */));
+ auto path = JoinPath(temp_dir.path(), GetParam().name);
+ FileDescriptor fd =
+ ASSERT_NO_ERRNO_AND_VALUE(Open(path.c_str(), O_CREAT | O_RDWR, 0666));
+ ASSERT_THAT(fchmod(fd.get(), GetParam().mode), SyscallSucceeds());
+ struct stat stats;
+ ASSERT_THAT(fstat(fd.get(), &stats), SyscallSucceeds());
+ EXPECT_EQ(stats.st_mode & kDirmodeMask, GetParam().mode);
+
+ // For security reasons, truncating the file clears the SUID bit, and clears
+ // the SGID bit when the group executable bit is unset (which is not a true
+ // SGID binary).
+ ASSERT_THAT(ftruncate(fd.get(), 0), SyscallSucceeds());
+
+ ASSERT_THAT(fstat(fd.get(), &stats), SyscallSucceeds());
+ EXPECT_EQ(stats.st_mode & kDirmodeMask, GetParam().result_mode);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ FileModes, FileModeTest,
+ ::testing::ValuesIn<FileModeTestcase>(
+ {FileModeTestcase("normal file", 0777, 0777),
+ FileModeTestcase("setuid", S_ISUID | 0777, 00777),
+ FileModeTestcase("setgid", S_ISGID | 0777, 00777),
+ FileModeTestcase("setuid and setgid", S_ISUID | S_ISGID | 0777, 00777),
+ FileModeTestcase("setgid without exec", S_ISGID | 0767,
+ S_ISGID | 0767),
+ FileModeTestcase("setuid and setgid without exec",
+ S_ISGID | S_ISUID | 0767, S_ISGID | 0767)}));
+
+} // namespace
+
+} // namespace testing
+} // namespace gvisor