| // Copyright 2022 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <dirent.h> |
| #include <fcntl.h> |
| #include <lib/fit/defer.h> |
| #include <sys/fsuid.h> |
| #include <sys/mman.h> |
| #include <sys/mount.h> |
| #include <sys/syscall.h> |
| #include <sys/types.h> |
| #include <sys/uio.h> |
| #include <unistd.h> |
| |
| #include <algorithm> |
| #include <climits> |
| #include <cstdint> |
| #include <optional> |
| #include <string> |
| #include <vector> |
| |
| #include <gtest/gtest.h> |
| #include <linux/capability.h> |
| |
| #include "src/lib/files/file.h" |
| #include "src/lib/files/path.h" |
| #include "src/lib/fxl/strings/string_printf.h" |
| #include "src/starnix/tests/syscalls/cpp/capabilities_helper.h" |
| #include "src/starnix/tests/syscalls/cpp/syscall_matchers.h" |
| #include "src/starnix/tests/syscalls/cpp/test_helper.h" |
| |
| namespace { |
| |
| std::vector<std::string> GetEntries(DIR *d) { |
| std::vector<std::string> entries; |
| |
| struct dirent *entry; |
| while ((entry = readdir(d)) != nullptr) { |
| entries.push_back(entry->d_name); |
| } |
| return entries; |
| } |
| |
| TEST(FsTest, NoDuplicatedDoDirectories) { |
| DIR *root_dir = opendir("/"); |
| std::vector<std::string> entries = GetEntries(root_dir); |
| std::vector<std::string> dot_entries; |
| std::copy_if(entries.begin(), entries.end(), std::back_inserter(dot_entries), |
| [](const std::string &filename) { return filename == "." || filename == ".."; }); |
| closedir(root_dir); |
| |
| ASSERT_EQ(2u, dot_entries.size()); |
| ASSERT_NE(dot_entries[0], dot_entries[1]); |
| } |
| |
| TEST(FsTest, ReadDirRespectsSeek) { |
| DIR *root_dir = opendir("/"); |
| std::vector<std::string> entries = GetEntries(root_dir); |
| closedir(root_dir); |
| |
| root_dir = opendir("/"); |
| readdir(root_dir); |
| long position = telldir(root_dir); |
| closedir(root_dir); |
| root_dir = opendir("/"); |
| seekdir(root_dir, position); |
| std::vector<std::string> next_entries = GetEntries(root_dir); |
| closedir(root_dir); |
| |
| EXPECT_NE(next_entries[0], entries[0]); |
| EXPECT_LT(next_entries.size(), entries.size()); |
| // Remove the first elements from entries |
| entries.erase(entries.begin(), entries.begin() + (entries.size() - next_entries.size())); |
| EXPECT_EQ(entries, next_entries); |
| } |
| |
| TEST(FsTest, FchmodTest) { |
| char *tmp = getenv("TEST_TMPDIR"); |
| std::string path = tmp == nullptr ? "/tmp/fchmodtest" : std::string(tmp) + "/fchmodtest"; |
| int fd = open(path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0777); |
| ASSERT_GE(fd, 0); |
| ASSERT_EQ(fchmod(fd, S_IRWXU | S_IRWXG), 0); |
| ASSERT_EQ(fchmod(fd, S_IRWXU | S_IRWXG | S_IFCHR), 0); |
| } |
| |
| // This test passes non-null arguments and has other quirks that fail under sanitizers. |
| #if (!__has_feature(address_sanitizer) && !defined(__arm__)) |
| TEST(FsTest, DevZeroAndNullQuirks) { |
| size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGESIZE)); |
| |
| for (const auto path : {"/dev/zero", "/dev/null"}) { |
| SCOPED_TRACE(path); |
| int fd = open(path, O_RDWR); |
| |
| // Attempting to write with an invalid buffer pointer still successfully "writes" the specified |
| // number of bytes. |
| EXPECT_EQ(write(fd, nullptr, page_size), static_cast<ssize_t>(page_size)); |
| |
| // write will report success up to the maximum number of bytes. |
| ssize_t max_rw_count = 0x8000'0000 - page_size; |
| EXPECT_EQ(write(fd, nullptr, max_rw_count), max_rw_count); |
| |
| // Attempting to write more than this reports a short write. |
| EXPECT_EQ(write(fd, nullptr, max_rw_count + 1), max_rw_count); |
| |
| // Producing a range that goes outside the userspace accessible range does produce EFAULT. |
| ssize_t implausibly_large_len = (1ll << 48); |
| |
| EXPECT_EQ(write(fd, nullptr, implausibly_large_len), -1); |
| EXPECT_EQ(errno, EFAULT); |
| |
| // A pointer unlikely to be backed by real memory is successful. |
| void *plausible_pointer = reinterpret_cast<void *>(1ll << 30); |
| EXPECT_EQ(write(fd, plausible_pointer, 1), 1); |
| |
| // An implausible pointer is unsuccessful. |
| void *implausible_pointer = reinterpret_cast<void *>(implausibly_large_len); |
| EXPECT_EQ(write(fd, implausible_pointer, 1), -1); |
| EXPECT_EQ(errno, EFAULT); |
| |
| // Passing an invalid iov pointer produces EFAULT. |
| EXPECT_EQ(writev(fd, nullptr, 1), -1); |
| EXPECT_EQ(errno, EFAULT); |
| |
| struct iovec iov_null_base_valid_length[] = {{ |
| .iov_base = nullptr, |
| .iov_len = 1, |
| }}; |
| |
| // Passing a valid iov pointer with null base pointers "successfully" writes the number of bytes |
| // specified in the entry. |
| EXPECT_EQ(writev(fd, iov_null_base_valid_length, 1), 1); |
| |
| struct iovec iov_null_base_max_rw_count_length[] = {{ |
| .iov_base = nullptr, |
| .iov_len = static_cast<size_t>(max_rw_count), |
| }}; |
| EXPECT_EQ(writev(fd, iov_null_base_max_rw_count_length, 1), max_rw_count); |
| |
| struct iovec iov_null_base_max_rw_count_in_two_entries[] = { |
| { |
| .iov_base = nullptr, |
| .iov_len = static_cast<size_t>(max_rw_count - 100), |
| }, |
| { |
| .iov_base = nullptr, |
| .iov_len = 100, |
| }, |
| }; |
| EXPECT_EQ(writev(fd, iov_null_base_max_rw_count_in_two_entries, 2), max_rw_count); |
| |
| struct iovec iov_null_base_max_rwcount_length_plus_one[] = {{ |
| .iov_base = nullptr, |
| .iov_len = static_cast<size_t>(max_rw_count + 1), |
| }}; |
| EXPECT_EQ(writev(fd, iov_null_base_max_rwcount_length_plus_one, 1), max_rw_count); |
| |
| struct iovec iov_null_base_max_rwcount_length_plus_one_in_two_entries[] = { |
| { |
| .iov_base = nullptr, |
| .iov_len = static_cast<size_t>(max_rw_count - 100), |
| }, |
| { |
| .iov_base = nullptr, |
| .iov_len = 101, |
| }, |
| }; |
| EXPECT_EQ(writev(fd, iov_null_base_max_rwcount_length_plus_one_in_two_entries, 2), |
| max_rw_count); |
| |
| // Implausibly large iov_len values still generate EFAULT. |
| struct iovec iov_null_base_implausible_length[] = {{ |
| .iov_base = nullptr, |
| .iov_len = static_cast<size_t>(implausibly_large_len), |
| }}; |
| EXPECT_EQ(writev(fd, iov_null_base_implausible_length, 1), -1); |
| EXPECT_EQ(errno, EFAULT); |
| |
| struct iovec iov_null_base_implausible_length_behind_max_rw_count[] = { |
| { |
| .iov_base = nullptr, |
| .iov_len = static_cast<size_t>(max_rw_count), |
| }, |
| { |
| .iov_base = nullptr, |
| .iov_len = static_cast<size_t>(implausibly_large_len), |
| }, |
| }; |
| |
| EXPECT_EQ(writev(fd, iov_null_base_implausible_length_behind_max_rw_count, 2), -1); |
| EXPECT_EQ(errno, EFAULT); |
| |
| if (std::string(path) == "/dev/null") { |
| // Reading any plausible number of bytes from an invalid buffer pointer into /dev/null |
| // will successfully read 0 bytes. |
| EXPECT_EQ(read(fd, nullptr, 1), 0); |
| EXPECT_EQ(read(fd, nullptr, max_rw_count), 0); |
| EXPECT_EQ(read(fd, nullptr, max_rw_count + 1), 0); |
| } |
| |
| // Reading an implausibly large number of bytes from /dev/zero or /dev/null will fail with |
| // EFAULT. |
| EXPECT_EQ(read(fd, nullptr, implausibly_large_len), -1); |
| EXPECT_EQ(errno, EFAULT); |
| |
| close(fd); |
| } |
| } |
| #endif |
| |
| TEST(FsTest, CreateExistingFileInReadonlyFilesystemReturnsEEXIST) { |
| // This test requires that / is readonly. |
| ASSERT_EQ(mkdir("/asdfasdf", 0777), -1); |
| ASSERT_EQ(errno, EROFS); |
| |
| EXPECT_EQ(mkdir("/tmp", 0777), -1); |
| EXPECT_EQ(errno, EEXIST); |
| } |
| |
| constexpr uid_t kOwnerUid = 65534; |
| constexpr uid_t kNonOwnerUid = 65533; |
| constexpr gid_t kOwnerGid = 65534; |
| constexpr gid_t kNonOwnerGid = 65533; |
| |
| constexpr uid_t kUser1Uid = 65532; |
| constexpr uid_t kUser2Uid = 65531; |
| constexpr gid_t kUser1Gid = 65532; |
| constexpr gid_t kUser2Gid = 65531; |
| |
| class UtimensatTest : public ::testing::Test { |
| protected: |
| void SetUp() { |
| if (!test_helper::HasSysAdmin()) { |
| GTEST_SKIP() << "Not running with sysadmin capabilities, skipping."; |
| } |
| |
| char dir_template[] = "/tmp/XXXXXX"; |
| ASSERT_NE(mkdtemp(dir_template), nullptr) |
| << "failed to create test folder: " << std::strerror(errno); |
| test_folder_ = std::string(dir_template); |
| |
| test_file_ = test_folder_ + "/testfile"; |
| int fd = open(test_file_.c_str(), O_RDWR | O_CREAT, 0666); |
| ASSERT_NE(fd, -1) << "failed to create test file: " << std::strerror(errno); |
| close(fd); |
| |
| ASSERT_EQ(chown(test_folder_.c_str(), kOwnerUid, kOwnerGid), 0); |
| ASSERT_EQ(chmod(test_folder_.c_str(), 0777), 0); |
| ASSERT_EQ(chmod(test_file_.c_str(), 0666), 0); |
| ASSERT_EQ(chown(test_file_.c_str(), kOwnerUid, kOwnerGid), 0); |
| } |
| |
| void TearDown() { |
| if (test_file_.length() != 0) { |
| ASSERT_EQ(remove(test_file_.c_str()), 0); |
| } |
| if (test_folder_.length() != 0) { |
| ASSERT_EQ(remove(test_folder_.c_str()), 0); |
| } |
| } |
| |
| // test folder owned by kOwnerUid, perms 0o777 |
| std::string test_folder_; |
| |
| // test file owned by kOwnerUid, perms 0o666 |
| std::string test_file_; |
| }; |
| |
| bool change_ids(uid_t user, gid_t group) { |
| return (setresgid(group, group, group) == 0) && (setresuid(user, user, user) == 0); |
| } |
| |
| TEST_F(UtimensatTest, OwnerCanAlwaysSetTime) { |
| ASSERT_EQ(chmod(test_file_.c_str(), 0), 0); |
| |
| // File owner can change time to now even without write perms. |
| test_helper::ForkHelper helper; |
| helper.RunInForkedProcess([this] { |
| ASSERT_TRUE(change_ids(kOwnerUid, kOwnerGid)); |
| EXPECT_EQ(0, utimensat(-1, test_file_.c_str(), nullptr, 0)) |
| << "utimensat failed: " << std::strerror(errno); |
| }); |
| |
| EXPECT_TRUE(helper.WaitForChildren()); |
| |
| // File owner can change time to any time without write perms. |
| helper.RunInForkedProcess([this] { |
| ASSERT_TRUE(change_ids(kOwnerUid, kOwnerGid)); |
| struct timespec times[2] = {{0, 0}}; |
| EXPECT_EQ(0, utimensat(-1, test_file_.c_str(), times, 0)) |
| << "utimensat failed: " << std::strerror(errno); |
| }); |
| |
| EXPECT_TRUE(helper.WaitForChildren()); |
| } |
| |
| TEST_F(UtimensatTest, NonOwnerWithWriteAccessCanOnlySetTimeToNow) { |
| ASSERT_EQ(chmod(test_file_.c_str(), 0), 0); |
| |
| // Non file owner cannot change time to now without write perms. |
| test_helper::ForkHelper helper; |
| helper.RunInForkedProcess([this] { |
| ASSERT_TRUE(change_ids(kNonOwnerUid, kNonOwnerGid)); |
| EXPECT_NE(0, utimensat(-1, test_file_.c_str(), nullptr, 0)); |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| |
| // Non file owner can change time to now with write perms. |
| ASSERT_EQ(chmod(test_file_.c_str(), 0006), 0); |
| helper.RunInForkedProcess([this] { |
| ASSERT_TRUE(change_ids(kNonOwnerUid, kNonOwnerGid)); |
| EXPECT_EQ(0, utimensat(-1, test_file_.c_str(), nullptr, 0)) |
| << "utimensat failed: " << std::strerror(errno); |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| |
| // Non file owner cannot change time to some other value, even with write |
| // perms. |
| helper.RunInForkedProcess([this] { |
| ASSERT_TRUE(change_ids(kNonOwnerUid, kNonOwnerGid)); |
| struct timespec times[2] = {{0, 0}}; |
| EXPECT_NE(0, utimensat(-1, test_file_.c_str(), times, 0)); |
| }); |
| |
| EXPECT_TRUE(helper.WaitForChildren()); |
| } |
| |
| TEST_F(UtimensatTest, NonOwnerWithCapabilitiesCanSetTime) { |
| ASSERT_EQ(chmod(test_file_.c_str(), 0), 0); |
| |
| // Non file owner without write permissions can set the time to now with |
| // either CAP_DAC_OVERRIDE or CAP_FOWNER capability. |
| test_helper::ForkHelper helper; |
| helper.RunInForkedProcess([this] { |
| ASSERT_TRUE(test_helper::HasCapability(CAP_DAC_OVERRIDE)); |
| ASSERT_TRUE(test_helper::HasCapability(CAP_FOWNER)); |
| EXPECT_EQ(0, utimensat(-1, test_file_.c_str(), nullptr, 0)) |
| << "utimensat failed: " << std::strerror(errno); |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| |
| helper.RunInForkedProcess([this] { |
| test_helper::UnsetCapability(CAP_DAC_OVERRIDE); |
| ASSERT_FALSE(test_helper::HasCapability(CAP_DAC_OVERRIDE)); |
| ASSERT_TRUE(test_helper::HasCapability(CAP_FOWNER)); |
| EXPECT_EQ(0, utimensat(-1, test_file_.c_str(), nullptr, 0)) |
| << "utimensat failed: " << std::strerror(errno); |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| |
| helper.RunInForkedProcess([this] { |
| test_helper::UnsetCapability(CAP_FOWNER); |
| ASSERT_TRUE(test_helper::HasCapability(CAP_DAC_OVERRIDE)); |
| ASSERT_FALSE(test_helper::HasCapability(CAP_FOWNER)); |
| EXPECT_EQ(0, utimensat(-1, test_file_.c_str(), nullptr, 0)) |
| << "utimensat failed: " << std::strerror(errno); |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| |
| helper.RunInForkedProcess([this] { |
| test_helper::UnsetCapability(CAP_DAC_OVERRIDE); |
| test_helper::UnsetCapability(CAP_FOWNER); |
| ASSERT_FALSE(test_helper::HasCapability(CAP_DAC_OVERRIDE)); |
| ASSERT_FALSE(test_helper::HasCapability(CAP_FOWNER)); |
| EXPECT_NE(0, utimensat(-1, test_file_.c_str(), nullptr, 0)); |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| |
| // Non file owner without write permissions can set the time to some other |
| // value with the CAP_FOWNER capability. |
| helper.RunInForkedProcess([this] { |
| test_helper::UnsetCapability(CAP_DAC_OVERRIDE); |
| ASSERT_FALSE(test_helper::HasCapability(CAP_DAC_OVERRIDE)); |
| ASSERT_TRUE(test_helper::HasCapability(CAP_FOWNER)); |
| struct timespec times[2] = {{0, 0}}; |
| EXPECT_EQ(0, utimensat(-1, test_file_.c_str(), times, 0)) |
| << "utimensat failed: " << std::strerror(errno); |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| |
| helper.RunInForkedProcess([this] { |
| test_helper::UnsetCapability(CAP_DAC_OVERRIDE); |
| test_helper::UnsetCapability(CAP_FOWNER); |
| ASSERT_FALSE(test_helper::HasCapability(CAP_DAC_OVERRIDE)); |
| ASSERT_FALSE(test_helper::HasCapability(CAP_FOWNER)); |
| struct timespec times[2] = {{0, 0}}; |
| EXPECT_NE(0, utimensat(-1, test_file_.c_str(), times, 0)); |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| } |
| |
| TEST_F(UtimensatTest, CanSetOmitTimestampsWithoutPermissions) { |
| // Non file owner without write permissions and without the CAP_DAC_OVERRIDE or |
| // CAP_FOWNER capability can set the timestamps to UTIME_OMIT. |
| ASSERT_EQ(chmod(test_file_.c_str(), 0), 0); |
| test_helper::ForkHelper helper; |
| helper.RunInForkedProcess([this] { |
| test_helper::UnsetCapability(CAP_DAC_OVERRIDE); |
| test_helper::UnsetCapability(CAP_FOWNER); |
| ASSERT_FALSE(test_helper::HasCapability(CAP_DAC_OVERRIDE)); |
| ASSERT_FALSE(test_helper::HasCapability(CAP_FOWNER)); |
| struct timespec times[2] = {{0, UTIME_OMIT}, {0, UTIME_OMIT}}; |
| EXPECT_EQ(0, utimensat(-1, test_file_.c_str(), times, 0)) |
| << "utimensat failed: " << std::strerror(errno); |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| } |
| |
| TEST_F(UtimensatTest, ReturnsEFAULTOnNullPathAndCWDDirFd) { |
| test_helper::ForkHelper helper; |
| helper.RunInForkedProcess([] { |
| struct timespec times[2] = {{0, 0}}; |
| EXPECT_NE(0, syscall(SYS_utimensat, AT_FDCWD, nullptr, times, 0)); |
| EXPECT_EQ(errno, EFAULT); |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| } |
| |
| TEST_F(UtimensatTest, ReturnsENOENTOnEmptyPath) { |
| test_helper::ForkHelper helper; |
| helper.RunInForkedProcess([] { |
| EXPECT_NE(0, utimensat(-1, "", nullptr, 0)); |
| EXPECT_EQ(errno, ENOENT); |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| } |
| |
| class CapDacTest : public ::testing::Test { |
| protected: |
| void SetUp() { |
| if (!test_helper::HasSysAdmin()) { |
| GTEST_SKIP() << "Not running with sysadmin capabilities, skipping."; |
| } |
| |
| char dir_template[] = "/tmp/XXXXXX"; |
| ASSERT_NE(mkdtemp(dir_template), nullptr) |
| << "failed to create test folder: " << std::strerror(errno); |
| test_folder_ = std::string(dir_template); |
| |
| test_file_ = test_folder_ + "/testfile"; |
| int fd = open(test_file_.c_str(), O_RDWR | O_CREAT, 0666); |
| ASSERT_NE(fd, -1) << "failed to create test file: " << std::strerror(errno); |
| close(fd); |
| |
| ASSERT_EQ(chown(test_folder_.c_str(), kOwnerUid, kOwnerGid), 0); |
| ASSERT_EQ(chmod(test_folder_.c_str(), 0777), 0); |
| ASSERT_EQ(chmod(test_file_.c_str(), 0666), 0); |
| ASSERT_EQ(chown(test_file_.c_str(), kOwnerUid, kOwnerGid), 0); |
| } |
| |
| void TearDown() { |
| if (test_file_.length() != 0) { |
| ASSERT_EQ(remove(test_file_.c_str()), 0); |
| } |
| if (test_folder_.length() != 0) { |
| ASSERT_EQ(remove(test_folder_.c_str()), 0); |
| } |
| } |
| |
| // test folder owned by kOwnerUid, perms 0o777 |
| std::string test_folder_; |
| |
| // test file owned by kOwnerUid, perms 0o666 |
| std::string test_file_; |
| }; |
| |
| TEST_F(CapDacTest, NonOwnerCanReadAndTraverseDirectoryWithDacOverrideOrReadSearch) { |
| ASSERT_EQ(chmod(test_folder_.c_str(), 0), 0); |
| |
| // Unreadable directory is unreadable without CAP_DAC_* capabilities. |
| test_helper::ForkHelper helper; |
| helper.RunInForkedProcess([this] { |
| test_helper::UnsetCapability(CAP_DAC_OVERRIDE); |
| test_helper::UnsetCapability(CAP_DAC_READ_SEARCH); |
| |
| fbl::unique_fd fd(open(test_folder_.c_str(), O_RDONLY)); |
| EXPECT_FALSE(fd.is_valid()) << "open(O_RDONLY)"; |
| |
| fd.reset(open(test_file_.c_str(), O_RDONLY)); |
| EXPECT_FALSE(fd.is_valid()) << "open(O_RDONLY)"; |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| |
| // Unreadable directory can be read and traversed with only CAP_DAC_READ_SEARCH. |
| helper.RunInForkedProcess([this] { |
| test_helper::UnsetCapability(CAP_DAC_OVERRIDE); |
| ASSERT_TRUE(test_helper::HasCapability(CAP_DAC_READ_SEARCH)); |
| |
| fbl::unique_fd fd(open(test_folder_.c_str(), O_RDONLY)); |
| EXPECT_TRUE(fd.is_valid()) << "open(O_RDONLY): " << std::strerror(errno); |
| fd.reset(open(test_folder_.c_str(), O_RDWR)); |
| EXPECT_FALSE(fd.is_valid()) << "open(O_RDWR)"; |
| |
| fd.reset(open(test_file_.c_str(), O_RDONLY)); |
| EXPECT_TRUE(fd.is_valid()) << "open(O_RDONLY): " << std::strerror(errno); |
| // The file can also be written, since the caller still has write permission to it. |
| fd.reset(open(test_file_.c_str(), O_RDWR)); |
| EXPECT_TRUE(fd.is_valid()) << "open(O_RDWR): " << std::strerror(errno); |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| |
| // Unreadable directory can be read and traversed with only CAP_DAC_OVERRIDE. |
| helper.RunInForkedProcess([this] { |
| test_helper::UnsetCapability(CAP_DAC_READ_SEARCH); |
| ASSERT_TRUE(test_helper::HasCapability(CAP_DAC_OVERRIDE)); |
| |
| fbl::unique_fd fd(open(test_folder_.c_str(), O_RDONLY)); |
| EXPECT_TRUE(fd.is_valid()) << "open(O_RDONLY): " << std::strerror(errno); |
| fd.reset(open(test_folder_.c_str(), O_RDWR)); |
| EXPECT_FALSE(fd.is_valid()) << "open(O_RDWR)"; |
| |
| fd.reset(open(test_file_.c_str(), O_RDONLY)); |
| EXPECT_TRUE(fd.is_valid()) << "open(O_RDONLY): " << std::strerror(errno); |
| // The file can also be written, since the caller still has write permission to it. |
| fd.reset(open(test_file_.c_str(), O_RDWR)); |
| EXPECT_TRUE(fd.is_valid()) << "open(O_RDWR): " << std::strerror(errno); |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| } |
| |
| TEST_F(CapDacTest, NonOwnerCanWriteDirectoryWithDacOverride) { |
| ASSERT_EQ(chmod(test_folder_.c_str(), 0), 0); |
| |
| // Unwritable directory is unwritable without CAP_DAC_OVERRIDE capability. |
| test_helper::ForkHelper helper; |
| helper.RunInForkedProcess([this] { |
| test_helper::UnsetCapability(CAP_DAC_OVERRIDE); |
| test_helper::UnsetCapability(CAP_DAC_READ_SEARCH); |
| std::string write_test_file = test_folder_ + "/testfile_without_dac_caps"; |
| fbl::unique_fd fd(open(write_test_file.c_str(), O_RDWR | O_CREAT, 0666)); |
| EXPECT_FALSE(fd.is_valid()) << "open(O_RDWR|O_CREAT) inside dir"; |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| |
| // Unwritable directory cannot be written with only CAP_DAC_READ_SEARCH. |
| helper.RunInForkedProcess([this] { |
| test_helper::UnsetCapability(CAP_DAC_OVERRIDE); |
| ASSERT_TRUE(test_helper::HasCapability(CAP_DAC_READ_SEARCH)); |
| std::string write_test_file = test_folder_ + "/testfile_with_dac_read_search"; |
| fbl::unique_fd fd(open(write_test_file.c_str(), O_RDWR | O_CREAT, 0666)); |
| EXPECT_FALSE(fd.is_valid()) << "open(O_RDWR|O_CREAT) inside dir"; |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| |
| // Unwritable directory can be written with only CAP_DAC_OVERRIDE. |
| helper.RunInForkedProcess([this] { |
| test_helper::UnsetCapability(CAP_DAC_READ_SEARCH); |
| ASSERT_TRUE(test_helper::HasCapability(CAP_DAC_OVERRIDE)); |
| |
| std::string write_test_file = test_folder_ + "/testfile_with_dac_override"; |
| fbl::unique_fd fd(open(write_test_file.c_str(), O_RDWR | O_CREAT, 0666)); |
| EXPECT_TRUE(fd.is_valid()) << "open(O_RDWR|O_CREAT) inside dir: " << std::strerror(errno); |
| ASSERT_EQ(remove(write_test_file.c_str()), 0) << "remove() test file: " << std::strerror(errno); |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| } |
| |
| TEST_F(CapDacTest, NonOwnerCanReadFileWithDacOverrideOrReadSearch) { |
| ASSERT_EQ(chmod(test_file_.c_str(), 0), 0); |
| |
| // Unreadable file is unreadable without CAP_DAC_* capabilities. |
| test_helper::ForkHelper helper; |
| helper.RunInForkedProcess([this] { |
| test_helper::UnsetCapability(CAP_DAC_OVERRIDE); |
| test_helper::UnsetCapability(CAP_DAC_READ_SEARCH); |
| fbl::unique_fd fd(open(test_file_.c_str(), O_RDONLY)); |
| EXPECT_FALSE(fd.is_valid()) << "open(O_RDONLY)"; |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| |
| // Unreadable file can be read with only CAP_DAC_READ_SEARCH. |
| helper.RunInForkedProcess([this] { |
| test_helper::UnsetCapability(CAP_DAC_OVERRIDE); |
| ASSERT_TRUE(test_helper::HasCapability(CAP_DAC_READ_SEARCH)); |
| fbl::unique_fd fd(open(test_file_.c_str(), O_RDONLY)); |
| EXPECT_TRUE(fd.is_valid()) << "open(O_RDONLY): " << std::strerror(errno); |
| fd.reset(open(test_file_.c_str(), O_RDWR)); |
| EXPECT_FALSE(fd.is_valid()) << "open(O_RDWR)"; |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| |
| // Unreadable file can be read with only CAP_DAC_OVERRIDE. |
| helper.RunInForkedProcess([this] { |
| test_helper::UnsetCapability(CAP_DAC_READ_SEARCH); |
| ASSERT_TRUE(test_helper::HasCapability(CAP_DAC_OVERRIDE)); |
| fbl::unique_fd fd(open(test_file_.c_str(), O_RDONLY)); |
| EXPECT_TRUE(fd.is_valid()) << "open(O_RDONLY): " << std::strerror(errno); |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| } |
| |
| TEST_F(CapDacTest, NonOwnerCanWriteFileWithDacOverride) { |
| ASSERT_EQ(chmod(test_file_.c_str(), 0), 0); |
| |
| // Unwritable file is unwritable without CAP_DAC_OVERRIDE capability. |
| test_helper::ForkHelper helper; |
| helper.RunInForkedProcess([this] { |
| test_helper::UnsetCapability(CAP_DAC_OVERRIDE); |
| fbl::unique_fd fd(open(test_file_.c_str(), O_WRONLY)); |
| EXPECT_FALSE(fd.is_valid()) << "open(O_WRONLY)"; |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| |
| // Unwritable file cannot be written with only CAP_DAC_READ_SEARCH. |
| helper.RunInForkedProcess([this] { |
| test_helper::UnsetCapability(CAP_DAC_OVERRIDE); |
| ASSERT_TRUE(test_helper::HasCapability(CAP_DAC_READ_SEARCH)); |
| fbl::unique_fd fd(open(test_file_.c_str(), O_WRONLY)); |
| EXPECT_FALSE(fd.is_valid()) << "open(O_WRONLY)"; |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| |
| // Unwritable file can be written with only CAP_DAC_OVERRIDE. |
| helper.RunInForkedProcess([this] { |
| test_helper::UnsetCapability(CAP_DAC_READ_SEARCH); |
| ASSERT_TRUE(test_helper::HasCapability(CAP_DAC_OVERRIDE)); |
| fbl::unique_fd fd(open(test_file_.c_str(), O_WRONLY)); |
| EXPECT_TRUE(fd.is_valid()) << "open(O_WRONLY): " << std::strerror(errno); |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| } |
| |
| class AccessTest : public ::testing::Test { |
| protected: |
| void SetUp() { |
| if (getuid() != 0) { |
| GTEST_SKIP() << "Not running as root, skipping."; |
| } |
| |
| ASSERT_THAT(chmod(test_folder_.path().c_str(), 0777), SyscallSucceeds()); |
| |
| ASSERT_TRUE(CreateFile("only_user", 0700, kOwnerUid, kOwnerGid, only_user_file_)); |
| ASSERT_TRUE(CreateFile("everyone", 0777, kOwnerUid, kOwnerGid, everyone_file_)); |
| } |
| |
| bool CreateFile(const char *name, int mode, uid_t uid, gid_t gid, std::string &path) { |
| path = test_folder_.path() + "/" + name; |
| auto fd = fbl::unique_fd(open(path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, mode)); |
| return fd.is_valid() && (chown(path.c_str(), uid, gid) == 0); |
| } |
| |
| // World readable & writable test folder. |
| test_helper::ScopedTempDir test_folder_; |
| |
| // File owned by kOwnerUid/Gid with permissions only granting owning user access. |
| std::string only_user_file_; |
| |
| // File owned by kOwnerUid/Gid with permissions only granting everyone access. |
| std::string everyone_file_; |
| }; |
| |
| TEST_F(AccessTest, ChecksAgainstRealCredsNonOwner) { |
| test_helper::ForkHelper helper; |
| helper.RunInForkedProcess([&] { |
| ASSERT_THAT(setresgid(kNonOwnerGid, kOwnerGid, 0), SyscallSucceeds()); |
| ASSERT_THAT(setresuid(kNonOwnerUid, kOwnerUid, 0), SyscallSucceeds()); |
| |
| EXPECT_THAT(access(only_user_file_.c_str(), R_OK), SyscallFailsWithErrno(EACCES)); |
| EXPECT_THAT(access(everyone_file_.c_str(), R_OK), SyscallSucceeds()); |
| }); |
| ASSERT_TRUE(helper.WaitForChildren()); |
| } |
| |
| TEST_F(AccessTest, ChecksAgainstRealCredsOwner) { |
| test_helper::ForkHelper helper; |
| helper.RunInForkedProcess([&] { |
| ASSERT_THAT(setresgid(kOwnerGid, kNonOwnerGid, 0), SyscallSucceeds()); |
| ASSERT_THAT(setresuid(kOwnerUid, kNonOwnerUid, 0), SyscallSucceeds()); |
| |
| EXPECT_THAT(access(only_user_file_.c_str(), R_OK), SyscallSucceeds()); |
| EXPECT_THAT(access(everyone_file_.c_str(), R_OK), SyscallSucceeds()); |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| } |
| |
| TEST_F(AccessTest, EaccessChecksAgainstEffectiveCredsNonOwner) { |
| test_helper::ForkHelper helper; |
| helper.RunInForkedProcess([&] { |
| ASSERT_THAT(setresgid(kOwnerGid, kNonOwnerGid, 0), SyscallSucceeds()); |
| ASSERT_THAT(setresuid(kOwnerUid, kNonOwnerUid, 0), SyscallSucceeds()); |
| |
| EXPECT_THAT(faccessat(AT_FDCWD, only_user_file_.c_str(), R_OK, AT_EACCESS), |
| SyscallFailsWithErrno(EACCES)); |
| EXPECT_THAT(faccessat(AT_FDCWD, everyone_file_.c_str(), R_OK, AT_EACCESS), SyscallSucceeds()); |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| } |
| |
| TEST_F(AccessTest, EaccessChecksAgainstEffectiveCredsOwner) { |
| test_helper::ForkHelper helper; |
| helper.RunInForkedProcess([&] { |
| ASSERT_THAT(setresgid(kNonOwnerGid, kOwnerGid, 0), SyscallSucceeds()); |
| ASSERT_THAT(setresuid(kNonOwnerUid, kOwnerUid, 0), SyscallSucceeds()); |
| |
| EXPECT_THAT(faccessat(AT_FDCWD, only_user_file_.c_str(), R_OK, AT_EACCESS), SyscallSucceeds()); |
| EXPECT_THAT(faccessat(AT_FDCWD, everyone_file_.c_str(), R_OK, AT_EACCESS), SyscallSucceeds()); |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| } |
| |
| TEST_F(AccessTest, FsUidIgnoredUnlessEaccess) { |
| test_helper::ForkHelper helper; |
| helper.RunInForkedProcess([&] { |
| ASSERT_THAT(setresgid(kNonOwnerGid, kNonOwnerGid, kOwnerUid), SyscallSucceeds()); |
| ASSERT_THAT(setresuid(kNonOwnerUid, kNonOwnerUid, kOwnerUid), SyscallSucceeds()); |
| |
| ASSERT_THAT(faccessat(AT_FDCWD, only_user_file_.c_str(), R_OK, AT_EACCESS), |
| SyscallFailsWithErrno(EACCES)); |
| ASSERT_THAT(faccessat(AT_FDCWD, everyone_file_.c_str(), R_OK, AT_EACCESS), SyscallSucceeds()); |
| |
| ASSERT_EQ(setfsuid(kOwnerUid), static_cast<int>(kNonOwnerUid)); |
| ASSERT_EQ(setfsuid(-1), static_cast<int>(kOwnerUid)); |
| |
| // Even though the "fsuid" is the owning UID, access to the only-user access file is denied. |
| EXPECT_THAT(faccessat(AT_FDCWD, only_user_file_.c_str(), R_OK, AT_EACCESS), SyscallSucceeds()); |
| EXPECT_THAT(faccessat(AT_FDCWD, only_user_file_.c_str(), R_OK, 0), |
| SyscallFailsWithErrno(EACCES)); |
| EXPECT_THAT(access(only_user_file_.c_str(), R_OK), SyscallFailsWithErrno(EACCES)); |
| EXPECT_THAT(faccessat(AT_FDCWD, everyone_file_.c_str(), R_OK, AT_EACCESS), SyscallSucceeds()); |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| |
| helper.RunInForkedProcess([&] { |
| ASSERT_THAT(setresgid(kOwnerGid, kOwnerGid, kNonOwnerUid), SyscallSucceeds()); |
| ASSERT_THAT(setresuid(kOwnerUid, kOwnerUid, kNonOwnerUid), SyscallSucceeds()); |
| |
| ASSERT_THAT(faccessat(AT_FDCWD, only_user_file_.c_str(), R_OK, AT_EACCESS), SyscallSucceeds()); |
| ASSERT_THAT(faccessat(AT_FDCWD, everyone_file_.c_str(), R_OK, AT_EACCESS), SyscallSucceeds()); |
| |
| ASSERT_EQ(setfsuid(kNonOwnerUid), static_cast<int>(kOwnerUid)); |
| ASSERT_EQ(setfsuid(-1), static_cast<int>(kNonOwnerUid)); |
| |
| // Even though the "fsuid" is the owning UID, access to the only-user access file is denied. |
| EXPECT_THAT(faccessat(AT_FDCWD, only_user_file_.c_str(), R_OK, AT_EACCESS), |
| SyscallFailsWithErrno(EACCES)); |
| EXPECT_THAT(faccessat(AT_FDCWD, only_user_file_.c_str(), R_OK, 0), SyscallSucceeds()); |
| EXPECT_THAT(access(only_user_file_.c_str(), R_OK), SyscallSucceeds()); |
| EXPECT_THAT(faccessat(AT_FDCWD, everyone_file_.c_str(), R_OK, AT_EACCESS), SyscallSucceeds()); |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| } |
| |
| std::optional<std::string> MountOverlayFs(const std::string &temp_dir) { |
| EXPECT_FALSE(temp_dir.empty()); |
| |
| std::string overlay = temp_dir + "/overlay"; |
| EXPECT_THAT(mkdir(overlay.c_str(), S_IRWXU), SyscallSucceeds()); |
| |
| std::string lower = temp_dir + "/lower"; |
| EXPECT_THAT(mkdir(lower.c_str(), S_IRWXU), SyscallSucceeds()); |
| |
| std::string upper = temp_dir + "/upper"; |
| EXPECT_THAT(mkdir(upper.c_str(), S_IRWXU), SyscallSucceeds()); |
| |
| std::string work = temp_dir + "/work"; |
| EXPECT_THAT(mkdir(work.c_str(), S_IRWXU), SyscallSucceeds()); |
| |
| std::string options = fxl::StringPrintf("lowerdir=%s,upperdir=%s,workdir=%s", lower.c_str(), |
| upper.c_str(), work.c_str()); |
| |
| int res = mount(nullptr, overlay.c_str(), "overlay", 0, options.c_str()); |
| EXPECT_EQ(res, 0) << "mount: " << std::strerror(errno); |
| |
| if (res != 0) { |
| return std::nullopt; |
| } |
| |
| return overlay; |
| } |
| |
| std::optional<std::string> MountTmpFs(const std::string &temp_dir) { |
| std::string temp = temp_dir + "/tmp"; |
| EXPECT_THAT(mkdir(temp.c_str(), S_IRWXU), SyscallSucceeds()); |
| |
| int res = mount(nullptr, temp.c_str(), "tmpfs", 0, ""); |
| EXPECT_EQ(res, 0) << "mount: " << std::strerror(errno); |
| |
| if (res != 0) { |
| return std::nullopt; |
| } |
| |
| return temp; |
| } |
| |
| class FsMountTest |
| : public testing::TestWithParam<std::optional<std::string> (*)(const std::string &)> { |
| protected: |
| void SetUp() override { |
| // TODO(https://fxbug.dev/317285180) don't skip on baseline |
| if (!test_helper::HasSysAdmin()) { |
| GTEST_SKIP() << "Not running with sysadmin capabilities, skipping suite."; |
| } |
| auto mounter = GetParam(); |
| auto mounted = mounter(temp_dir_.path()); |
| ASSERT_TRUE(mounted.has_value()) << "failed to mount fs"; |
| mount_path_ = mounted.value(); |
| |
| // Directory Permissions: owner can do everything, user and other can search. |
| constexpr int kDirPerms = S_IRWXU | S_IXGRP | S_IXOTH; |
| |
| ASSERT_THAT(chmod(mount_path_.c_str(), kDirPerms), SyscallSucceeds()); |
| ASSERT_THAT(chmod(temp_dir_.path().c_str(), kDirPerms), SyscallSucceeds()); |
| } |
| |
| test_helper::ScopedTempDir temp_dir_; |
| std::string mount_path_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(TmpFs, FsMountTest, ::testing::Values(MountTmpFs)); |
| INSTANTIATE_TEST_SUITE_P(OverlayFs, FsMountTest, ::testing::Values(MountOverlayFs)); |
| |
| TEST_P(FsMountTest, CantBypassDirectoryPermissions) { |
| std::string user1_folder = mount_path_ + "/user1"; |
| ASSERT_THAT(mkdir(user1_folder.c_str(), S_IRWXU), SyscallSucceeds()); |
| ASSERT_THAT(chown(user1_folder.c_str(), kUser1Uid, kUser1Gid), SyscallSucceeds()); |
| |
| std::string user2_folder = mount_path_ + "/user2"; |
| ASSERT_THAT(mkdir(user2_folder.c_str(), S_IRWXU), SyscallSucceeds()); |
| ASSERT_THAT(chown(user2_folder.c_str(), kUser2Uid, kUser2Gid), SyscallSucceeds()); |
| |
| test_helper::ForkHelper helper; |
| helper.RunInForkedProcess([&] { |
| ASSERT_TRUE(change_ids(kUser2Uid, kUser2Gid)); |
| test_helper::DropAllCapabilities(); |
| |
| // We should be able to create files in user2's directory. |
| std::string file_path = user2_folder + "/test_file"; |
| int fd = open(file_path.c_str(), O_RDWR | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR); |
| EXPECT_NE(fd, -1) << "open " << file_path << ": " << std::strerror(errno); |
| if (fd != -1) { |
| close(fd); |
| EXPECT_EQ(unlink(file_path.c_str()), 0); |
| } |
| |
| // We shouldn't be able to create files in user1's directory. |
| file_path = user1_folder + "/test_file"; |
| fd = open(file_path.c_str(), O_RDWR | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR); |
| EXPECT_EQ(fd, -1); |
| EXPECT_EQ(errno, EACCES); |
| if (fd != -1) { |
| close(fd); |
| EXPECT_EQ(unlink(file_path.c_str()), 0); |
| } |
| }); |
| } |
| |
| TEST_P(FsMountTest, CreateWithDifferentModes) { |
| std::string user1_folder = mount_path_ + "/user1"; |
| ASSERT_THAT(mkdir(user1_folder.c_str(), S_IRWXU), SyscallSucceeds()); |
| ASSERT_THAT(chown(user1_folder.c_str(), kUser1Uid, kUser1Gid), SyscallSucceeds()); |
| |
| test_helper::ForkHelper helper; |
| helper.RunInForkedProcess([user1_folder] { |
| ASSERT_TRUE(change_ids(kUser1Uid, kUser1Gid)); |
| test_helper::DropAllCapabilities(); |
| |
| const mode_t old_umask = umask(0); |
| constexpr mode_t kModeMask = 0777; |
| auto clean_umask = fit::defer([old_umask]() { umask(old_umask); }); |
| |
| for (mode_t mode = 0000; mode <= 0777; mode++) { |
| SCOPED_TRACE(fxl::StringPrintf("Mode: %o", mode)); |
| |
| std::string file_path = fxl::StringPrintf("%s/create.%o", user1_folder.c_str(), mode); |
| { |
| fbl::unique_fd fd(open(file_path.c_str(), O_RDWR | O_CREAT | O_EXCL, mode)); |
| EXPECT_TRUE(fd.is_valid()) << "open: " << std::strerror(errno); |
| } |
| |
| auto cleanup = |
| fit::defer([file_path]() { EXPECT_THAT(unlink(file_path.c_str()), SyscallSucceeds()); }); |
| |
| struct stat file_stat; |
| EXPECT_THAT(stat(file_path.c_str(), &file_stat), SyscallSucceeds()); |
| EXPECT_TRUE(file_stat.st_mode & S_IFREG) << "not a regular file"; |
| EXPECT_EQ(file_stat.st_mode & kModeMask, mode) << "wrong permissions"; |
| } |
| }); |
| } |
| |
| TEST_P(FsMountTest, ChmodWithDifferentModes) { |
| std::string user1_folder = mount_path_ + "/user1"; |
| ASSERT_THAT(mkdir(user1_folder.c_str(), S_IRWXU), SyscallSucceeds()); |
| ASSERT_THAT(chown(user1_folder.c_str(), kUser1Uid, kUser1Gid), SyscallSucceeds()); |
| |
| test_helper::ForkHelper helper; |
| helper.RunInForkedProcess([user1_folder] { |
| ASSERT_TRUE(change_ids(kUser1Uid, kUser1Gid)); |
| test_helper::DropAllCapabilities(); |
| const mode_t old_umask = umask(0); |
| constexpr mode_t kModeMask = 0777; |
| auto clean_umask = fit::defer([old_umask]() { umask(old_umask); }); |
| |
| for (mode_t mode = 0000; mode <= 0777; mode++) { |
| SCOPED_TRACE(fxl::StringPrintf("Mode: %o", mode)); |
| |
| std::string file_path = fxl::StringPrintf("%s/chmod.%o", user1_folder.c_str(), mode); |
| { |
| fbl::unique_fd fd(open(file_path.c_str(), O_RDWR | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR)); |
| EXPECT_TRUE(fd.is_valid()) << "open: " << std::strerror(errno); |
| } |
| auto cleanup = |
| fit::defer([file_path]() { EXPECT_THAT(unlink(file_path.c_str()), SyscallSucceeds()); }); |
| |
| EXPECT_THAT(chmod(file_path.c_str(), mode), SyscallSucceeds()); |
| |
| struct stat file_stat; |
| EXPECT_THAT(stat(file_path.c_str(), &file_stat), SyscallSucceeds()); |
| EXPECT_TRUE(file_stat.st_mode & S_IFREG) << "not a regular file"; |
| EXPECT_EQ(file_stat.st_mode & kModeMask, mode) << "wrong permissions"; |
| } |
| }); |
| } |
| |
| TEST_P(FsMountTest, ChownMinusOneSucceeds) { |
| // Executing chown(file, -1, -1) should almost always work. |
| std::string user1_file = files::JoinPath(mount_path_, "user1_file"); |
| close(SAFE_SYSCALL(creat(user1_file.c_str(), S_IRWXU))); |
| SAFE_SYSCALL(chown(user1_file.c_str(), kUser1Uid, kUser1Gid)); |
| |
| test_helper::ForkHelper helper; |
| |
| // Running as the same user. |
| helper.RunInForkedProcess([user1_file] { |
| ASSERT_TRUE(change_ids(kUser1Uid, kUser1Gid)); |
| test_helper::DropAllCapabilities(); |
| |
| EXPECT_THAT(chown(user1_file.c_str(), -1, -1), SyscallSucceeds()); |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| |
| // Running as a different user. |
| helper.RunInForkedProcess([user1_file] { |
| ASSERT_TRUE(change_ids(kUser2Uid, kUser2Gid)); |
| test_helper::DropAllCapabilities(); |
| |
| EXPECT_THAT(chown(user1_file.c_str(), -1, -1), SyscallSucceeds()); |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| |
| SAFE_SYSCALL(unlink(user1_file.c_str())); |
| } |
| |
| TEST_P(FsMountTest, ChownMinusOneNoPathAccessFails) { |
| // Executing chown(file, -1, -1) fails if we can't resolve the path. |
| std::string user1_folder = files::JoinPath(mount_path_, "user1_folder"); |
| std::string user1_file = files::JoinPath(user1_folder, "user1_file"); |
| SAFE_SYSCALL(mkdir(user1_folder.c_str(), S_IRWXU)); // user2 can't access. |
| |
| SAFE_SYSCALL(chown(user1_folder.c_str(), kUser1Uid, kUser1Gid)); |
| close(SAFE_SYSCALL(creat(user1_file.c_str(), S_IRWXU))); |
| SAFE_SYSCALL(chown(user1_file.c_str(), kUser1Uid, kUser1Gid)); |
| |
| test_helper::ForkHelper helper; |
| |
| helper.RunInForkedProcess([user1_folder, user1_file] { |
| ASSERT_TRUE(change_ids(kUser2Uid, kUser2Gid)); |
| test_helper::DropAllCapabilities(); |
| |
| EXPECT_THAT(chown(user1_folder.c_str(), -1, -1), SyscallSucceeds()); |
| EXPECT_THAT(chown(user1_file.c_str(), -1, -1), SyscallFailsWithErrno(EACCES)); |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| |
| SAFE_SYSCALL(unlink(user1_file.c_str())); |
| } |
| |
| TEST_P(FsMountTest, ChownMinusOneOnSIDFileFails) { |
| // Executing chown(file, -1, -1) fails if the file is set-ID. |
| std::string user1_file = files::JoinPath(mount_path_, "user1_file"); |
| close(SAFE_SYSCALL(creat(user1_file.c_str(), 0))); |
| SAFE_SYSCALL(chown(user1_file.c_str(), kUser1Uid, kUser1Gid)); |
| |
| test_helper::ForkHelper helper; |
| |
| helper.RunInForkedProcess([user1_file] { |
| SAFE_SYSCALL(chmod(user1_file.c_str(), S_ISUID)); |
| ASSERT_TRUE(change_ids(kUser2Uid, kUser2Gid)); |
| test_helper::DropAllCapabilities(); |
| |
| EXPECT_THAT(chown(user1_file.c_str(), -1, -1), SyscallFailsWithErrno(EPERM)); |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| |
| // The file should still be set-user-ID even after failure. |
| struct stat file_stat {}; |
| SAFE_SYSCALL(stat(user1_file.c_str(), &file_stat)); |
| EXPECT_NE(file_stat.st_mode & S_ISUID, 0U); |
| |
| helper.RunInForkedProcess([user1_file] { |
| SAFE_SYSCALL(chmod(user1_file.c_str(), S_ISGID | S_IXGRP)); |
| ASSERT_TRUE(change_ids(kUser2Uid, kUser2Gid)); |
| test_helper::DropAllCapabilities(); |
| |
| EXPECT_THAT(chown(user1_file.c_str(), -1, -1), SyscallFailsWithErrno(EPERM)); |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| |
| // The file should still be set-group-ID even after failure. |
| SAFE_SYSCALL(stat(user1_file.c_str(), &file_stat)); |
| EXPECT_EQ(file_stat.st_mode & (S_ISGID | S_IXGRP), (unsigned int)(S_ISGID | S_IXGRP)); |
| |
| // But not if we are the owners. |
| helper.RunInForkedProcess([user1_file] { |
| ASSERT_TRUE(change_ids(kUser1Uid, kUser1Gid)); |
| test_helper::DropAllCapabilities(); |
| |
| EXPECT_THAT(chown(user1_file.c_str(), -1, -1), SyscallSucceeds()); |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| |
| // Doing a successful chown should have dropped the set-user-ID bit of the file. |
| SAFE_SYSCALL(stat(user1_file.c_str(), &file_stat)); |
| EXPECT_EQ(file_stat.st_mode & S_ISUID, 0U); |
| |
| SAFE_SYSCALL(unlink(user1_file.c_str())); |
| } |
| |
| TEST_P(FsMountTest, ChownSameOwnerAndGroupFails) { |
| // Executing chown explicitly specifying owner and gid (instead of -1), fails |
| // if we are not owners. |
| std::string user1_file = files::JoinPath(mount_path_, "user1_file"); |
| close(SAFE_SYSCALL(creat(user1_file.c_str(), S_IRWXU))); |
| SAFE_SYSCALL(chmod(user1_file.c_str(), S_IRWXU)); |
| SAFE_SYSCALL(chown(user1_file.c_str(), kUser1Uid, kUser1Gid)); |
| |
| test_helper::ForkHelper helper; |
| helper.RunInForkedProcess([user1_file] { |
| ASSERT_TRUE(change_ids(kUser2Uid, kUser2Gid)); |
| test_helper::DropAllCapabilities(); |
| |
| EXPECT_THAT(chown(user1_file.c_str(), kUser1Uid, kUser1Gid), SyscallFailsWithErrno(EPERM)); |
| EXPECT_THAT(chown(user1_file.c_str(), -1, kUser1Gid), SyscallFailsWithErrno(EPERM)); |
| EXPECT_THAT(chown(user1_file.c_str(), kUser1Uid, -1), SyscallFailsWithErrno(EPERM)); |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| |
| SAFE_SYSCALL(unlink(user1_file.c_str())); |
| } |
| |
| TEST_P(FsMountTest, ChownOnSUIDFileDropsSUIDBit) { |
| std::string user1_file = files::JoinPath(mount_path_, "user1_file"); |
| close(SAFE_SYSCALL(creat(user1_file.c_str(), 0))); |
| SAFE_SYSCALL(chown(user1_file.c_str(), kUser1Uid, kUser1Gid)); |
| |
| test_helper::ForkHelper helper; |
| |
| helper.RunInForkedProcess([user1_file] { |
| ASSERT_TRUE(change_ids(kUser1Uid, kUser1Gid)); |
| test_helper::DropAllCapabilities(); |
| |
| for (mode_t mode = 0000; mode <= 0777; mode++) { |
| SCOPED_TRACE(fxl::StringPrintf("Mode: %o", mode)); |
| SAFE_SYSCALL(chmod(user1_file.c_str(), S_ISUID | mode)); |
| SAFE_SYSCALL(chown(user1_file.c_str(), -1, -1)); |
| |
| struct stat file_stat {}; |
| SAFE_SYSCALL(stat(user1_file.c_str(), &file_stat)); |
| EXPECT_EQ(file_stat.st_mode & S_ISUID, 0U); |
| } |
| }); |
| |
| EXPECT_TRUE(helper.WaitForChildren()); |
| } |
| TEST_P(FsMountTest, ChownOnSGIDFileDropsSGIDBit) { |
| std::string user1_file = files::JoinPath(mount_path_, "user1_file"); |
| close(SAFE_SYSCALL(creat(user1_file.c_str(), 0))); |
| SAFE_SYSCALL(chown(user1_file.c_str(), kUser1Uid, kUser1Gid)); |
| |
| test_helper::ForkHelper helper; |
| |
| helper.RunInForkedProcess([user1_file] { |
| ASSERT_TRUE(change_ids(kUser1Uid, kUser1Gid)); |
| test_helper::DropAllCapabilities(); |
| |
| for (mode_t mode = 0000; mode <= 0777; mode++) { |
| SCOPED_TRACE(fxl::StringPrintf("Mode: %o", mode)); |
| SAFE_SYSCALL(chmod(user1_file.c_str(), S_ISGID | mode)); |
| SAFE_SYSCALL(chown(user1_file.c_str(), -1, -1)); |
| |
| struct stat file_stat {}; |
| SAFE_SYSCALL(stat(user1_file.c_str(), &file_stat)); |
| if (mode & S_IXGRP) { |
| // The set-group-ID bit only takes effect if the file is |
| // group-executable. Otherwise it has other meaning and should not drop |
| // that bit upon chown. |
| EXPECT_EQ(file_stat.st_mode & S_ISGID, 0U); |
| } else { |
| EXPECT_NE(file_stat.st_mode & S_ISGID, 0U); |
| } |
| } |
| }); |
| |
| EXPECT_TRUE(helper.WaitForChildren()); |
| } |
| |
| TEST_P(FsMountTest, OpenWithTruncAndCreatOnReadOnlyFsReturnsEROFS) { |
| std::string lock_file = mount_path_ + "/lock"; |
| int fd = SAFE_SYSCALL(open(lock_file.c_str(), O_CREAT | O_RDWR, 0600)); |
| close(fd); |
| |
| SAFE_SYSCALL(chown(lock_file.c_str(), kUser1Uid, kUser1Gid)); |
| |
| // Remount filesystem as read-only. |
| SAFE_SYSCALL( |
| mount(nullptr, mount_path_.c_str(), "ignored", MS_REMOUNT | MS_BIND | MS_RDONLY, "")); |
| auto cleanup = fit::defer([this]() { |
| SAFE_SYSCALL(mount(nullptr, mount_path_.c_str(), "ignored", MS_REMOUNT | MS_BIND, "")); |
| }); |
| |
| test_helper::ForkHelper helper; |
| helper.RunInForkedProcess([lock_file] { |
| ASSERT_TRUE(change_ids(kUser1Uid, kUser1Gid)); |
| test_helper::DropAllCapabilities(); |
| |
| int fd = open(lock_file.c_str(), O_RDWR | O_CREAT | O_TRUNC, 0666); |
| int saved_errno = errno; |
| EXPECT_EQ(fd, -1); |
| EXPECT_EQ(saved_errno, EROFS) << std::strerror(saved_errno); |
| |
| if (fd != -1) { |
| SAFE_SYSCALL(close(fd)); |
| } |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| } |
| |
| TEST_P(FsMountTest, OpenWithTruncAndCreatWithExistingFileSucceeds) { |
| std::string lock_file = mount_path_ + "/lock"; |
| int fd = SAFE_SYSCALL(open(lock_file.c_str(), O_CREAT | O_RDWR, 0600)); |
| close(fd); |
| |
| SAFE_SYSCALL(chown(lock_file.c_str(), kUser1Uid, kUser1Gid)); |
| |
| test_helper::ForkHelper helper; |
| helper.RunInForkedProcess([lock_file] { |
| ASSERT_TRUE(change_ids(kUser1Uid, kUser1Gid)); |
| test_helper::DropAllCapabilities(); |
| |
| int fd = SAFE_SYSCALL(open(lock_file.c_str(), O_RDWR | O_CREAT | O_TRUNC, 0600)); |
| SAFE_SYSCALL(close(fd)); |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| } |
| |
| TEST_P(FsMountTest, OpenWithTruncAndCreatWithNoPermsReturnsEACCES) { |
| std::string lock_file = mount_path_ + "/lock"; |
| int fd = SAFE_SYSCALL(open(lock_file.c_str(), O_CREAT | O_RDWR, 0600)); |
| close(fd); |
| |
| SAFE_SYSCALL(chown(lock_file.c_str(), kUser1Uid, kUser1Gid)); |
| |
| test_helper::ForkHelper helper; |
| helper.RunInForkedProcess([lock_file] { |
| ASSERT_TRUE(change_ids(kUser2Uid, kUser2Gid)); |
| test_helper::DropAllCapabilities(); |
| |
| int fd = open(lock_file.c_str(), O_RDWR | O_CREAT | O_TRUNC, 0600); |
| int saved_errno = errno; |
| EXPECT_EQ(fd, -1); |
| EXPECT_EQ(saved_errno, EACCES) << std::strerror(saved_errno); |
| if (fd != -1) { |
| SAFE_SYSCALL(close(fd)); |
| } |
| }); |
| EXPECT_TRUE(helper.WaitForChildren()); |
| } |
| |
| TEST_P(FsMountTest, CreateAndRenameDirectory) { |
| std::string old_name = mount_path_ + "/old"; |
| std::string new_name = mount_path_ + "/new"; |
| |
| ASSERT_THAT(mkdir(old_name.c_str(), 0700), SyscallSucceeds()); |
| EXPECT_THAT(rename(old_name.c_str(), new_name.c_str()), SyscallSucceeds()); |
| } |
| |
| class OtmpfileTest : public ::testing::Test { |
| protected: |
| void SetUp() override { |
| char *dir = getenv("MUTABLE_STORAGE"); |
| test_folder_ = dir == nullptr ? "/tmp/XXXXXX" : std::string(dir) + "/XXXXXX"; |
| ASSERT_NE(mkdtemp(test_folder_.data()), nullptr) |
| << "failed to create test folder: " << std::strerror(errno); |
| |
| test_file1_ = test_folder_ + "/testfile1"; |
| test_file2_ = test_folder_ + "/testfile2"; |
| } |
| |
| void TearDown() override { |
| if (tmpfile_fd_ != -1) { |
| tmpfile_fd_.reset(); |
| } |
| // These files may have been created, attempt to remove them in case they were. |
| remove(test_file1_.c_str()); |
| remove(test_file2_.c_str()); |
| if (test_folder_.length() != 0) { |
| ASSERT_EQ(rmdir(test_folder_.c_str()), 0); |
| } |
| } |
| |
| fbl::unique_fd tmpfile_fd_; |
| std::string test_folder_; |
| std::string test_file1_; |
| std::string test_file2_; |
| }; |
| |
| void CheckLinkCount(int fd, unsigned count) { |
| uint64_t nlink = 0; |
| struct stat s; |
| if (fstat(fd, &s) == 0) { |
| nlink = s.st_nlink; |
| } else { |
| ASSERT_EQ(errno, EOVERFLOW); |
| struct stat64 s; |
| ASSERT_EQ(fstat64(fd, &s), 0); |
| nlink = s.st_nlink; |
| } |
| ASSERT_EQ(nlink, count); |
| } |
| |
| TEST_F(OtmpfileTest, TmpFileLinkIntoAfter) { |
| // CAP_DAC_READ_SEARCH capability is required to use AT_EMPTY_PATH with linkat |
| if (!test_helper::HasCapability(CAP_DAC_READ_SEARCH)) { |
| GTEST_SKIP() << "Not running with CAP_DAC_READ_SEARCH capabilities, skipping."; |
| } |
| tmpfile_fd_ = fbl::unique_fd(open(test_folder_.c_str(), O_RDWR | O_TMPFILE)); |
| ASSERT_TRUE(tmpfile_fd_.is_valid()) << "open() with O_TMPFILE failed:" << strerror(errno); |
| ASSERT_NO_FATAL_FAILURE(CheckLinkCount(tmpfile_fd_.get(), 0)); |
| |
| // Write to file. The contents are used later to verify that linkat worked. |
| ASSERT_EQ(write(tmpfile_fd_.get(), "hello", 5), 5) |
| << "Write to tmpfile failed:" << strerror(errno); |
| |
| // Test that we can link. |
| SAFE_SYSCALL(linkat(tmpfile_fd_.get(), "", AT_FDCWD, test_file1_.c_str(), AT_EMPTY_PATH)); |
| ASSERT_NO_FATAL_FAILURE(CheckLinkCount(tmpfile_fd_.get(), 1)); |
| |
| // Test that we can link again. |
| SAFE_SYSCALL(linkat(tmpfile_fd_.get(), "", AT_FDCWD, test_file2_.c_str(), AT_EMPTY_PATH)); |
| ASSERT_NO_FATAL_FAILURE(CheckLinkCount(tmpfile_fd_.get(), 2)); |
| |
| // Verify contents. |
| fbl::unique_fd test_file_fd(open(test_file1_.c_str(), O_RDONLY)); |
| ASSERT_TRUE(test_file_fd.is_valid()) << "Failed to open file:" << strerror(errno); |
| char buffer[10]; |
| ASSERT_EQ(read(test_file_fd.get(), buffer, 10), 5) |
| << "Failed to read from file:" << strerror(errno); |
| ASSERT_EQ(strncmp(buffer, "hello", 5), 0) |
| << "Contents do not match the contents written to the tmpfile."; |
| |
| // If we try to link into a path that is already used, this should fail with EEXIST. |
| int result = linkat(tmpfile_fd_.get(), "", AT_FDCWD, test_file1_.c_str(), AT_EMPTY_PATH); |
| int saved_errno = errno; |
| ASSERT_EQ(result, -1); |
| EXPECT_EQ(saved_errno, EEXIST) << "Link to an existing path should fail with EEXIST:" |
| << std::strerror(saved_errno); |
| } |
| |
| TEST_F(OtmpfileTest, TmpFileWithOExclShouldFailLinkInto) { |
| // CAP_DAC_READ_SEARCH capability is required to use AT_EMPTY_PATH with linkat |
| if (!test_helper::HasCapability(CAP_DAC_READ_SEARCH)) { |
| GTEST_SKIP() << "Not running with CAP_DAC_READ_SEARCH capabilities, skipping."; |
| } |
| |
| tmpfile_fd_ = fbl::unique_fd(open(test_folder_.c_str(), O_RDWR | O_TMPFILE | O_EXCL)); |
| ASSERT_TRUE(tmpfile_fd_.is_valid()) << "open() with O_TMPFILE failed:" << strerror(errno); |
| |
| int result = linkat(tmpfile_fd_.get(), "", AT_FDCWD, test_file1_.c_str(), AT_EMPTY_PATH); |
| int saved_errno = errno; |
| ASSERT_EQ(result, -1); |
| EXPECT_EQ(saved_errno, ENOENT) |
| << "linkat() should fail when file was opened with O_TMPFILE | O_EXCL with ENOENT:" |
| << std::strerror(saved_errno); |
| } |
| |
| TEST_F(OtmpfileTest, TmpFileFailWithRdOnlyAccessMode) { |
| tmpfile_fd_ = fbl::unique_fd(open(test_folder_.c_str(), O_RDONLY | O_TMPFILE)); |
| int saved_errno = errno; |
| ASSERT_FALSE(tmpfile_fd_.is_valid()); |
| EXPECT_EQ(saved_errno, EINVAL) |
| << "open() with O_TMPFILE not specified with O_RDWR and O_WRONLY should fail with EINVAL:" |
| << std::strerror(saved_errno); |
| } |
| |
| TEST_F(OtmpfileTest, TmpFileWithOCreatShouldFail) { |
| tmpfile_fd_ = fbl::unique_fd(open(test_folder_.c_str(), O_RDWR | O_CREAT | O_TMPFILE)); |
| int saved_errno = errno; |
| ASSERT_FALSE(tmpfile_fd_.is_valid()); |
| EXPECT_EQ(saved_errno, EINVAL) |
| << "open() with O_TMPFILE and O_CREAT are not compatible. Should fail with EINVAL:" |
| << std::strerror(saved_errno); |
| } |
| |
| TEST(LinkTest, FileLinkCount) { |
| // Create a temporary directory, store its absolute path and chdir to it. |
| char *dir = getenv("MUTABLE_STORAGE"); |
| std::string test_folder = |
| dir == nullptr ? "/tmp/linkcount.XXXXXX" : std::string(dir) + "/linkcount.XXXXXX"; |
| ASSERT_NE(mkdtemp(test_folder.data()), nullptr) |
| << "failed to create test folder: " << std::strerror(errno); |
| |
| std::string test_file = test_folder + "/foo"; |
| fbl::unique_fd foo_fd(creat(test_file.c_str(), S_IRWXU)); |
| ASSERT_TRUE(foo_fd.is_valid()) << "Failed to open file:" << strerror(errno); |
| ASSERT_NO_FATAL_FAILURE(CheckLinkCount(foo_fd.get(), 1)); |
| |
| // Create link to the file. We should see link count increment. |
| std::string bar = test_folder + "/bar"; |
| SAFE_SYSCALL(linkat(AT_FDCWD, test_file.c_str(), AT_FDCWD, bar.c_str(), 0)); |
| ASSERT_NO_FATAL_FAILURE(CheckLinkCount(foo_fd.get(), 2)); |
| |
| // Unlink should decrement the link count. |
| EXPECT_EQ(unlink(bar.c_str()), 0); |
| ASSERT_NO_FATAL_FAILURE(CheckLinkCount(foo_fd.get(), 1)); |
| EXPECT_EQ(unlink(test_file.c_str()), 0); |
| ASSERT_NO_FATAL_FAILURE(CheckLinkCount(foo_fd.get(), 0)); |
| |
| // Clean up. |
| ASSERT_EQ(rmdir(test_folder.c_str()), 0); |
| } |
| |
| } // namespace |