gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/test/syscalls/linux/setgid.cc (about)

     1  // Copyright 2020 The gVisor Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  #include <limits.h>
    16  #include <sys/types.h>
    17  #include <unistd.h>
    18  
    19  #include "gtest/gtest.h"
    20  #include "absl/flags/flag.h"
    21  #include "test/util/capability_util.h"
    22  #include "test/util/cleanup.h"
    23  #include "test/util/fs_util.h"
    24  #include "test/util/posix_error.h"
    25  #include "test/util/temp_path.h"
    26  #include "test/util/test_util.h"
    27  
    28  ABSL_FLAG(std::vector<std::string>, groups, std::vector<std::string>({}),
    29            "groups the test can use");
    30  
    31  constexpr gid_t kNobody = 65534;
    32  
    33  namespace gvisor {
    34  namespace testing {
    35  
    36  namespace {
    37  
    38  constexpr int kDirmodeMask = 07777;
    39  constexpr int kDirmodeSgid = S_ISGID | 0777;
    40  constexpr int kDirmodeNoExec = S_ISGID | 0767;
    41  constexpr int kDirmodeNoSgid = 0777;
    42  
    43  // Sets effective GID and returns a Cleanup that restores the original.
    44  PosixErrorOr<Cleanup> Setegid(gid_t egid) {
    45    gid_t old_gid = getegid();
    46    if (setegid(egid) < 0) {
    47      return PosixError(errno, absl::StrFormat("setegid(%d)", egid));
    48    }
    49    return Cleanup(
    50        [old_gid]() { EXPECT_THAT(setegid(old_gid), SyscallSucceeds()); });
    51  }
    52  
    53  // Returns a pair of groups that the user is a member of.
    54  PosixErrorOr<std::pair<gid_t, gid_t>> Groups() {
    55    // Were we explicitly passed GIDs?
    56    std::vector<std::string> flagged_groups = absl::GetFlag(FLAGS_groups);
    57    if (flagged_groups.size() >= 2) {
    58      int group1;
    59      int group2;
    60      if (!absl::SimpleAtoi(flagged_groups[0], &group1) ||
    61          !absl::SimpleAtoi(flagged_groups[1], &group2)) {
    62        return PosixError(EINVAL, "failed converting group flags to ints");
    63      }
    64      return std::pair<gid_t, gid_t>(group1, group2);
    65    }
    66  
    67    // See whether the user is a member of at least 2 groups.
    68    std::vector<gid_t> groups(64);
    69    for (; groups.size() <= NGROUPS_MAX; groups.resize(groups.size() * 2)) {
    70      int ngroups = getgroups(groups.size(), groups.data());
    71      if (ngroups < 0 && errno == EINVAL) {
    72        // Need a larger list.
    73        continue;
    74      }
    75      if (ngroups < 0) {
    76        return PosixError(errno, absl::StrFormat("getgroups(%d, %p)",
    77                                                 groups.size(), groups.data()));
    78      }
    79  
    80      if (ngroups < 2) {
    81        // There aren't enough groups.
    82        break;
    83      }
    84  
    85      // TODO(b/181878080): Read /proc/sys/fs/overflowgid once it is supported in
    86      // gVisor.
    87      if (groups[0] == kNobody || groups[1] == kNobody) {
    88        // These groups aren't mapped into our user namespace, so we can't use
    89        // them.
    90        break;
    91      }
    92      return std::pair<gid_t, gid_t>(groups[0], groups[1]);
    93    }
    94  
    95    // If we're running in gVisor and are root in the root user namespace, we can
    96    // set our GID to whatever we want. Try that before giving up.
    97    //
    98    // This won't work in native tests, as despite having CAP_SETGID, the gofer
    99    // process will be sandboxed and unable to change file GIDs.
   100    if (!IsRunningOnGvisor()) {
   101      return PosixError(EPERM, "no valid groups for native testing");
   102    }
   103    PosixErrorOr<bool> capable = HaveCapability(CAP_SETGID);
   104    if (!capable.ok()) {
   105      return capable.error();
   106    }
   107    if (!capable.ValueOrDie()) {
   108      return PosixError(EPERM, "missing CAP_SETGID");
   109    }
   110    return std::pair<gid_t, gid_t>(getegid(), kNobody);
   111  }
   112  
   113  class SetgidDirTest : public ::testing::Test {
   114   protected:
   115    void SetUp() override {
   116      original_gid_ = getegid();
   117  
   118      // If we can't find two usable groups, we're in an unsupporting environment.
   119      // Skip the test.
   120      PosixErrorOr<std::pair<gid_t, gid_t>> groups = Groups();
   121      SKIP_IF(!groups.ok());
   122      groups_ = groups.ValueOrDie();
   123  
   124      // Ensure we can actually use both groups.
   125      auto cleanup1 = Setegid(groups_.first);
   126      SKIP_IF(!cleanup1.ok());
   127      auto cleanup2 = Setegid(groups_.second);
   128      SKIP_IF(!cleanup2.ok());
   129  
   130      auto cleanup = Setegid(groups_.first);
   131      temp_dir_ = ASSERT_NO_ERRNO_AND_VALUE(
   132          TempPath::CreateDirWith(GetAbsoluteTestTmpdir(), 0777 /* mode */));
   133    }
   134  
   135    void TearDown() override {
   136      EXPECT_THAT(setegid(original_gid_), SyscallSucceeds());
   137    }
   138  
   139    void MkdirAsGid(gid_t gid, const std::string& path, mode_t mode) {
   140      auto cleanup = ASSERT_NO_ERRNO_AND_VALUE(Setegid(gid));
   141      ASSERT_THAT(mkdir(path.c_str(), mode), SyscallSucceeds());
   142    }
   143  
   144    PosixErrorOr<struct stat> Stat(const std::string& path) {
   145      struct stat stats;
   146      if (stat(path.c_str(), &stats) < 0) {
   147        return PosixError(errno, absl::StrFormat("stat(%s, _)", path));
   148      }
   149      return stats;
   150    }
   151  
   152    PosixErrorOr<struct stat> Stat(const FileDescriptor& fd) {
   153      struct stat stats;
   154      if (fstat(fd.get(), &stats) < 0) {
   155        return PosixError(errno, "fstat(_, _)");
   156      }
   157      return stats;
   158    }
   159  
   160    TempPath temp_dir_;
   161    std::pair<gid_t, gid_t> groups_;
   162    gid_t original_gid_;
   163  };
   164  
   165  // The control test. Files created with a given GID are owned by that group.
   166  TEST_F(SetgidDirTest, Control) {
   167    // Set group to G1 and create a directory.
   168    auto g1owned = JoinPath(temp_dir_.path(), "g1owned/");
   169    ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.first, g1owned, 0777));
   170  
   171    // Set group to G2, create a file in g1owned, and confirm that G2 owns it.
   172    auto cleanup = ASSERT_NO_ERRNO_AND_VALUE(Setegid(groups_.second));
   173    FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(
   174        Open(JoinPath(g1owned, "g2owned").c_str(), O_CREAT | O_RDWR, 0777));
   175    struct stat stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(fd));
   176    EXPECT_EQ(stats.st_gid, groups_.second);
   177  }
   178  
   179  // Setgid directories cause created files to inherit GID.
   180  TEST_F(SetgidDirTest, CreateFile) {
   181    // Set group to G1, create a directory, and enable setgid.
   182    auto g1owned = JoinPath(temp_dir_.path(), "g1owned/");
   183    ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.first, g1owned, kDirmodeSgid));
   184    ASSERT_THAT(chmod(g1owned.c_str(), kDirmodeSgid), SyscallSucceeds());
   185  
   186    // Set group to G2, create a file, and confirm that G1 owns it.
   187    auto cleanup = ASSERT_NO_ERRNO_AND_VALUE(Setegid(groups_.second));
   188    FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(
   189        Open(JoinPath(g1owned, "g2created").c_str(), O_CREAT | O_RDWR, 0666));
   190    struct stat stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(fd));
   191    EXPECT_EQ(stats.st_gid, groups_.first);
   192  }
   193  
   194  // Setgid directories cause created directories to inherit GID.
   195  TEST_F(SetgidDirTest, CreateDir) {
   196    // Set group to G1, create a directory, and enable setgid.
   197    auto g1owned = JoinPath(temp_dir_.path(), "g1owned/");
   198    ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.first, g1owned, kDirmodeSgid));
   199    ASSERT_THAT(chmod(g1owned.c_str(), kDirmodeSgid), SyscallSucceeds());
   200  
   201    // Set group to G2, create a directory, confirm that G1 owns it, and that the
   202    // setgid bit is enabled.
   203    auto g2created = JoinPath(g1owned, "g2created");
   204    ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.second, g2created, 0666));
   205    struct stat stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(g2created));
   206    EXPECT_EQ(stats.st_gid, groups_.first);
   207    EXPECT_EQ(stats.st_mode & S_ISGID, S_ISGID);
   208  }
   209  
   210  // Setgid directories with group execution disabled still cause GID inheritance.
   211  TEST_F(SetgidDirTest, NoGroupExec) {
   212    // Set group to G1, create a directory, and enable setgid.
   213    auto g1owned = JoinPath(temp_dir_.path(), "g1owned/");
   214    ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.first, g1owned, kDirmodeNoExec));
   215    ASSERT_THAT(chmod(g1owned.c_str(), kDirmodeNoExec), SyscallSucceeds());
   216  
   217    // Set group to G2, create a directory, confirm that G2 owns it, and that the
   218    // setgid bit is enabled.
   219    auto g2created = JoinPath(g1owned, "g2created");
   220    ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.second, g2created, 0666));
   221    struct stat stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(g2created));
   222    EXPECT_EQ(stats.st_gid, groups_.first);
   223    EXPECT_EQ(stats.st_mode & S_ISGID, S_ISGID);
   224  }
   225  
   226  // Setting the setgid bit on directories with an existing file does not change
   227  // the file's group.
   228  TEST_F(SetgidDirTest, OldFile) {
   229    // Set group to G1 and create a directory.
   230    auto g1owned = JoinPath(temp_dir_.path(), "g1owned/");
   231    ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.first, g1owned, kDirmodeNoSgid));
   232    ASSERT_THAT(chmod(g1owned.c_str(), kDirmodeNoSgid), SyscallSucceeds());
   233  
   234    // Set group to G2, create a file, confirm that G2 owns it.
   235    auto cleanup = ASSERT_NO_ERRNO_AND_VALUE(Setegid(groups_.second));
   236    FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(
   237        Open(JoinPath(g1owned, "g2created").c_str(), O_CREAT | O_RDWR, 0666));
   238    struct stat stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(fd));
   239    EXPECT_EQ(stats.st_gid, groups_.second);
   240  
   241    // Enable setgid.
   242    ASSERT_THAT(chmod(g1owned.c_str(), kDirmodeSgid), SyscallSucceeds());
   243  
   244    // Confirm that the file's group is still G2.
   245    stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(fd));
   246    EXPECT_EQ(stats.st_gid, groups_.second);
   247  }
   248  
   249  // Setting the setgid bit on directories with an existing subdirectory does not
   250  // change the subdirectory's group.
   251  TEST_F(SetgidDirTest, OldDir) {
   252    // Set group to G1, create a directory, and enable setgid.
   253    auto g1owned = JoinPath(temp_dir_.path(), "g1owned/");
   254    ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.first, g1owned, kDirmodeNoSgid));
   255    ASSERT_THAT(chmod(g1owned.c_str(), kDirmodeNoSgid), SyscallSucceeds());
   256  
   257    // Set group to G2, create a directory, confirm that G2 owns it.
   258    auto cleanup = ASSERT_NO_ERRNO_AND_VALUE(Setegid(groups_.second));
   259    auto g2created = JoinPath(g1owned, "g2created");
   260    ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.second, g2created, 0666));
   261    struct stat stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(g2created));
   262    EXPECT_EQ(stats.st_gid, groups_.second);
   263  
   264    // Enable setgid.
   265    ASSERT_THAT(chmod(g1owned.c_str(), kDirmodeSgid), SyscallSucceeds());
   266  
   267    // Confirm that the file's group is still G2.
   268    stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(g2created));
   269    EXPECT_EQ(stats.st_gid, groups_.second);
   270  }
   271  
   272  // Chowning a file clears the setgid and setuid bits.
   273  TEST_F(SetgidDirTest, ChownFileClears) {
   274    // Set group to G1, create a directory, and enable setgid.
   275    auto g1owned = JoinPath(temp_dir_.path(), "g1owned/");
   276    ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.first, g1owned, kDirmodeMask));
   277    ASSERT_THAT(chmod(g1owned.c_str(), kDirmodeMask), SyscallSucceeds());
   278  
   279    FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(
   280        Open(JoinPath(g1owned, "newfile").c_str(), O_CREAT | O_RDWR, 0666));
   281    ASSERT_THAT(fchmod(fd.get(), 0777 | S_ISUID | S_ISGID), SyscallSucceeds());
   282    struct stat stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(fd));
   283    EXPECT_EQ(stats.st_gid, groups_.first);
   284    EXPECT_EQ(stats.st_mode & (S_ISUID | S_ISGID), S_ISUID | S_ISGID);
   285  
   286    // Change the owning group.
   287    ASSERT_THAT(fchown(fd.get(), -1, groups_.second), SyscallSucceeds());
   288  
   289    // The setgid and setuid bits should be cleared.
   290    stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(fd));
   291    EXPECT_EQ(stats.st_gid, groups_.second);
   292    EXPECT_EQ(stats.st_mode & (S_ISUID | S_ISGID), 0);
   293  }
   294  
   295  // Chowning a file with setgid enabled, but not the group exec bit, clears the
   296  // setuid bit and not the setgid bit. Such files are mandatory locked.
   297  TEST_F(SetgidDirTest, ChownNoExecFileDoesNotClear) {
   298    // Set group to G1, create a directory, and enable setgid.
   299    auto g1owned = JoinPath(temp_dir_.path(), "g1owned/");
   300    ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.first, g1owned, kDirmodeNoExec));
   301    ASSERT_THAT(chmod(g1owned.c_str(), kDirmodeNoExec), SyscallSucceeds());
   302  
   303    FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(
   304        Open(JoinPath(g1owned, "newdir").c_str(), O_CREAT | O_RDWR, 0666));
   305    ASSERT_THAT(fchmod(fd.get(), 0766 | S_ISUID | S_ISGID), SyscallSucceeds());
   306    struct stat stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(fd));
   307    EXPECT_EQ(stats.st_gid, groups_.first);
   308    EXPECT_EQ(stats.st_mode & (S_ISUID | S_ISGID), S_ISUID | S_ISGID);
   309  
   310    // Change the owning group.
   311    ASSERT_THAT(fchown(fd.get(), -1, groups_.second), SyscallSucceeds());
   312  
   313    // Only the setuid bit is cleared.
   314    stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(fd));
   315    EXPECT_EQ(stats.st_gid, groups_.second);
   316    EXPECT_EQ(stats.st_mode & (S_ISUID | S_ISGID), S_ISGID);
   317  }
   318  
   319  // Chowning a directory with setgid enabled does not clear the bit.
   320  TEST_F(SetgidDirTest, ChownDirDoesNotClear) {
   321    // Set group to G1, create a directory, and enable setgid.
   322    auto g1owned = JoinPath(temp_dir_.path(), "g1owned/");
   323    ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.first, g1owned, kDirmodeMask));
   324    ASSERT_THAT(chmod(g1owned.c_str(), kDirmodeMask), SyscallSucceeds());
   325  
   326    // Change the owning group.
   327    ASSERT_THAT(chown(g1owned.c_str(), -1, groups_.second), SyscallSucceeds());
   328  
   329    struct stat stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(g1owned));
   330    EXPECT_EQ(stats.st_gid, groups_.second);
   331    EXPECT_EQ(stats.st_mode & kDirmodeMask, kDirmodeMask);
   332  }
   333  
   334  struct FileModeTestcase {
   335    std::string name;
   336    mode_t mode;
   337    mode_t result_mode;
   338  
   339    FileModeTestcase(const std::string& name, mode_t mode, mode_t result_mode)
   340        : name(name), mode(mode), result_mode(result_mode) {}
   341  };
   342  
   343  class FileModeTest : public ::testing::TestWithParam<FileModeTestcase> {};
   344  
   345  TEST_P(FileModeTest, WriteToFile) {
   346    PosixErrorOr<std::pair<gid_t, gid_t>> groups = Groups();
   347    SKIP_IF(!groups.ok());
   348  
   349    auto cleanup = Setegid(groups.ValueOrDie().first);
   350    auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(
   351        TempPath::CreateDirWith(GetAbsoluteTestTmpdir(), 0777 /* mode */));
   352    auto path = JoinPath(temp_dir.path(), GetParam().name);
   353    FileDescriptor fd =
   354        ASSERT_NO_ERRNO_AND_VALUE(Open(path.c_str(), O_CREAT | O_RDWR, 0666));
   355    ASSERT_THAT(fchmod(fd.get(), GetParam().mode), SyscallSucceeds());
   356    struct stat stats;
   357    ASSERT_THAT(fstat(fd.get(), &stats), SyscallSucceeds());
   358    EXPECT_EQ(stats.st_mode & kDirmodeMask, GetParam().mode);
   359  
   360    // For security reasons, writing to the file clears the SUID bit, and clears
   361    // the SGID bit when the group executable bit is unset (which is not a true
   362    // SGID binary).
   363    constexpr char kInput = 'M';
   364    ASSERT_THAT(write(fd.get(), &kInput, sizeof(kInput)),
   365                SyscallSucceedsWithValue(sizeof(kInput)));
   366  
   367    ASSERT_THAT(fstat(fd.get(), &stats), SyscallSucceeds());
   368    EXPECT_EQ(stats.st_mode & kDirmodeMask, GetParam().result_mode);
   369  }
   370  
   371  TEST_P(FileModeTest, TruncateFile) {
   372    PosixErrorOr<std::pair<gid_t, gid_t>> groups = Groups();
   373    SKIP_IF(!groups.ok());
   374  
   375    auto cleanup = Setegid(groups.ValueOrDie().first);
   376    auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(
   377        TempPath::CreateDirWith(GetAbsoluteTestTmpdir(), 0777 /* mode */));
   378    auto path = JoinPath(temp_dir.path(), GetParam().name);
   379    FileDescriptor fd =
   380        ASSERT_NO_ERRNO_AND_VALUE(Open(path.c_str(), O_CREAT | O_RDWR, 0666));
   381  
   382    // Write something to the file, as truncating an empty file is a no-op.
   383    constexpr char c = 'M';
   384    ASSERT_THAT(write(fd.get(), &c, sizeof(c)),
   385                SyscallSucceedsWithValue(sizeof(c)));
   386    ASSERT_THAT(fchmod(fd.get(), GetParam().mode), SyscallSucceeds());
   387  
   388    // For security reasons, truncating the file clears the SUID bit, and clears
   389    // the SGID bit when the group executable bit is unset (which is not a true
   390    // SGID binary).
   391    ASSERT_THAT(ftruncate(fd.get(), 0), SyscallSucceeds());
   392  
   393    struct stat stats;
   394    ASSERT_THAT(fstat(fd.get(), &stats), SyscallSucceeds());
   395    EXPECT_EQ(stats.st_mode & kDirmodeMask, GetParam().result_mode);
   396  }
   397  
   398  INSTANTIATE_TEST_SUITE_P(
   399      FileModes, FileModeTest,
   400      ::testing::ValuesIn<FileModeTestcase>(
   401          {FileModeTestcase("normal file", 0777, 0777),
   402           FileModeTestcase("setuid", S_ISUID | 0777, 00777),
   403           FileModeTestcase("setgid", S_ISGID | 0777, 00777),
   404           FileModeTestcase("setuid and setgid", S_ISUID | S_ISGID | 0777, 00777),
   405           FileModeTestcase("setgid without exec", S_ISGID | 0767,
   406                            S_ISGID | 0767),
   407           FileModeTestcase("setuid and setgid without exec",
   408                            S_ISGID | S_ISUID | 0767, S_ISGID | 0767)}));
   409  
   410  }  // namespace
   411  
   412  }  // namespace testing
   413  }  // namespace gvisor