// Copyright 2018 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 <dirent.h> #include <errno.h> #include <fcntl.h> #include <stddef.h> #include <stdint.h> #include <stdio.h> #include <string.h> #include <sys/mman.h> #include <sys/types.h> #include <syscall.h> #include <unistd.h> #include <map> #include <string> #include <unordered_map> #include <unordered_set> #include <utility> #include "gmock/gmock.h" #include "gtest/gtest.h" #include "absl/container/node_hash_set.h" #include "absl/strings/numbers.h" #include "absl/strings/str_cat.h" #include "test/util/eventfd_util.h" #include "test/util/file_descriptor.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" using ::testing::Contains; using ::testing::IsEmpty; using ::testing::IsSupersetOf; using ::testing::Not; using ::testing::NotNull; namespace gvisor { namespace testing { namespace { // New Linux dirent format. struct linux_dirent64 { uint64_t d_ino; // Inode number int64_t d_off; // Offset to next linux_dirent64 unsigned short d_reclen; // NOLINT, Length of this linux_dirent64 unsigned char d_type; // NOLINT, File type char d_name[0]; // Filename (null-terminated) }; // Old Linux dirent format. struct linux_dirent { unsigned long d_ino; // NOLINT unsigned long d_off; // NOLINT unsigned short d_reclen; // NOLINT char d_name[0]; }; // Wraps a buffer to provide a set of dirents. // T is the underlying dirent type. template <typename T> class DirentBuffer { public: // DirentBuffer manages the buffer. explicit DirentBuffer(size_t size) : managed_(true), actual_size_(size), reported_size_(size) { data_ = new char[actual_size_]; } // The buffer is managed externally. DirentBuffer(char* data, size_t actual_size, size_t reported_size) : managed_(false), data_(data), actual_size_(actual_size), reported_size_(reported_size) {} ~DirentBuffer() { if (managed_) { delete[] data_; } } T* Data() { return reinterpret_cast<T*>(data_); } T* Start(size_t read) { read_ = read; if (read_) { return Data(); } else { return nullptr; } } T* Current() { return reinterpret_cast<T*>(&data_[off_]); } T* Next() { size_t new_off = off_ + Current()->d_reclen; if (new_off >= read_ || new_off >= actual_size_) { return nullptr; } off_ = new_off; return Current(); } size_t Size() { return reported_size_; } void Reset() { off_ = 0; read_ = 0; memset(data_, 0, actual_size_); } private: bool managed_; char* data_; size_t actual_size_; size_t reported_size_; size_t off_ = 0; size_t read_ = 0; }; // Test for getdents/getdents64. // T is the Linux dirent type. template <typename T> class GetdentsTest : public ::testing::Test { public: using LinuxDirentType = T; using DirentBufferType = DirentBuffer<T>; protected: void SetUp() override { dir_ = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); fd_ = ASSERT_NO_ERRNO_AND_VALUE(Open(dir_.path(), O_RDONLY | O_DIRECTORY)); } // Must be overridden with explicit specialization. See below. int SyscallNum(); int Getdents(LinuxDirentType* dirp, unsigned int count) { return RetryEINTR(syscall)(SyscallNum(), fd_.get(), dirp, count); } // Fill directory with num files, named by number starting at 0. void FillDirectory(size_t num) { for (size_t i = 0; i < num; i++) { auto name = JoinPath(dir_.path(), absl::StrCat(i)); TEST_CHECK(CreateWithContents(name, "").ok()); } } // Fill directory with a given list of filenames. void FillDirectoryWithFiles(const std::vector<std::string>& filenames) { for (const auto& filename : filenames) { auto name = JoinPath(dir_.path(), filename); TEST_CHECK(CreateWithContents(name, "").ok()); } } // Seek to the start of the directory. PosixError SeekStart() { constexpr off_t kStartOfFile = 0; off_t offset = lseek(fd_.get(), kStartOfFile, SEEK_SET); if (offset < 0) { return PosixError(errno, absl::StrCat("error seeking to ", kStartOfFile)); } if (offset != kStartOfFile) { return PosixError(EINVAL, absl::StrCat("tried to seek to ", kStartOfFile, " but got ", offset)); } return NoError(); } // Call getdents multiple times, reading all dirents and calling f on each. // f has the type signature PosixError f(T*). // If f returns a non-OK error, so does ReadDirents. template <typename F> PosixError ReadDirents(DirentBufferType* dirents, F const& f) { int n; do { dirents->Reset(); n = Getdents(dirents->Data(), dirents->Size()); MaybeSave(); if (n < 0) { return PosixError(errno, "getdents"); } for (auto d = dirents->Start(n); d; d = dirents->Next()) { RETURN_IF_ERRNO(f(d)); } } while (n > 0); return NoError(); } // Call Getdents successively and count all entries. int ReadAndCountAllEntries(DirentBufferType* dirents) { int found = 0; EXPECT_NO_ERRNO(ReadDirents(dirents, [&](LinuxDirentType* d) { found++; return NoError(); })); return found; } private: TempPath dir_; FileDescriptor fd_; }; // Multiple template parameters are not allowed, so we must use explicit // template specialization to set the syscall number. // SYS_getdents isn't defined on arm64. #ifdef __x86_64__ template <> int GetdentsTest<struct linux_dirent>::SyscallNum() { return SYS_getdents; } #endif template <> int GetdentsTest<struct linux_dirent64>::SyscallNum() { return SYS_getdents64; } #ifdef __x86_64__ // Test both legacy getdents and getdents64 on x86_64. typedef ::testing::Types<struct linux_dirent, struct linux_dirent64> GetdentsTypes; #elif __aarch64__ // Test only getdents64 on arm64. typedef ::testing::Types<struct linux_dirent64> GetdentsTypes; #endif TYPED_TEST_SUITE(GetdentsTest, GetdentsTypes); // N.B. TYPED_TESTs require explicitly using this-> to access members of // GetdentsTest, since we are inside of a derived class template. TYPED_TEST(GetdentsTest, VerifyEntries) { typename TestFixture::DirentBufferType dirents(1024); this->FillDirectory(2); // Map of all the entries we expect to find. std::map<std::string, bool> found; found["."] = false; found[".."] = false; found["0"] = false; found["1"] = false; EXPECT_NO_ERRNO(this->ReadDirents( &dirents, [&](typename TestFixture::LinuxDirentType* d) { auto kv = found.find(d->d_name); EXPECT_NE(kv, found.end()) << "Unexpected file: " << d->d_name; if (kv != found.end()) { EXPECT_FALSE(kv->second); } found[d->d_name] = true; return NoError(); })); for (auto& kv : found) { EXPECT_TRUE(kv.second) << "File not found: " << kv.first; } } TYPED_TEST(GetdentsTest, VerifyPadding) { typename TestFixture::DirentBufferType dirents(1024); // Create files with names of length 1 through 16. std::vector<std::string> files; std::string filename; for (int i = 0; i < 16; ++i) { absl::StrAppend(&filename, "a"); files.push_back(filename); } this->FillDirectoryWithFiles(files); // We expect to find all the files, plus '.' and '..'. const int expect_found = 2 + files.size(); int found = 0; EXPECT_NO_ERRNO(this->ReadDirents( &dirents, [&](typename TestFixture::LinuxDirentType* d) { EXPECT_EQ(d->d_reclen % 8, 0) << "Dirent " << d->d_name << " had reclen that was not byte aligned: " << d->d_name; found++; return NoError(); })); // Make sure we found all the files. EXPECT_EQ(found, expect_found); } // For a small directory, the provided buffer should be large enough // for all entries. TYPED_TEST(GetdentsTest, SmallDir) { // . and .. should be in an otherwise empty directory. int expect = 2; // Add some actual files. this->FillDirectory(2); expect += 2; typename TestFixture::DirentBufferType dirents(256); EXPECT_EQ(expect, this->ReadAndCountAllEntries(&dirents)); } // A directory with lots of files requires calling getdents multiple times. TYPED_TEST(GetdentsTest, LargeDir) { // . and .. should be in an otherwise empty directory. int expect = 2; // Add some actual files. this->FillDirectory(100); expect += 100; typename TestFixture::DirentBufferType dirents(256); EXPECT_EQ(expect, this->ReadAndCountAllEntries(&dirents)); } // If we lie about the size of the buffer, we should still be able to read the // entries with the available space. TYPED_TEST(GetdentsTest, PartialBuffer) { // . and .. should be in an otherwise empty directory. int expect = 2; // Add some actual files. this->FillDirectory(100); expect += 100; void* addr = mmap(0, 2 * kPageSize, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); ASSERT_NE(addr, MAP_FAILED); char* buf = reinterpret_cast<char*>(addr); // Guard page EXPECT_THAT( mprotect(reinterpret_cast<void*>(buf + kPageSize), kPageSize, PROT_NONE), SyscallSucceeds()); // Limit space in buf to 256 bytes. buf += kPageSize - 256; // Lie about the buffer. Even though we claim the buffer is 1 page, // we should still get all of the dirents in the first 256 bytes. typename TestFixture::DirentBufferType dirents(buf, 256, kPageSize); EXPECT_EQ(expect, this->ReadAndCountAllEntries(&dirents)); EXPECT_THAT(munmap(addr, 2 * kPageSize), SyscallSucceeds()); } // Open many file descriptors, then scan through /proc/self/fd to find and close // them all. (The latter is commonly used to handle races between fork/execve // and the creation of unwanted non-O_CLOEXEC file descriptors.) This tests that // getdents iterates correctly despite mutation of /proc/self/fd. TYPED_TEST(GetdentsTest, ProcSelfFd) { constexpr size_t kNfds = 10; std::unordered_map<int, FileDescriptor> fds; fds.reserve(kNfds); for (size_t i = 0; i < kNfds; i++) { FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(NewEventFD()); fds.emplace(fd.get(), std::move(fd)); } const FileDescriptor proc_self_fd = ASSERT_NO_ERRNO_AND_VALUE(Open("/proc/self/fd", O_RDONLY | O_DIRECTORY)); // Make the buffer very small since we want to iterate. typename TestFixture::DirentBufferType dirents( 2 * sizeof(typename TestFixture::LinuxDirentType)); absl::node_hash_set<int> prev_fds; while (true) { dirents.Reset(); int rv; ASSERT_THAT(rv = RetryEINTR(syscall)(this->SyscallNum(), proc_self_fd.get(), dirents.Data(), dirents.Size()), SyscallSucceeds()); if (rv == 0) { break; } for (auto* d = dirents.Start(rv); d; d = dirents.Next()) { int dfd; if (!absl::SimpleAtoi(d->d_name, &dfd)) continue; EXPECT_TRUE(prev_fds.insert(dfd).second) << "Repeated observation of /proc/self/fd/" << dfd; fds.erase(dfd); } } // Check that we closed every fd. EXPECT_THAT(fds, ::testing::IsEmpty()); } // Test that getdents returns ENOTDIR when called on a file. TYPED_TEST(GetdentsTest, NotDir) { auto file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile()); auto fd = ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_RDONLY)); typename TestFixture::DirentBufferType dirents(256); EXPECT_THAT(RetryEINTR(syscall)(this->SyscallNum(), fd.get(), dirents.Data(), dirents.Size()), SyscallFailsWithErrno(ENOTDIR)); } // Test that SEEK_SET to 0 causes getdents to re-read the entries. TYPED_TEST(GetdentsTest, SeekResetsCursor) { // . and .. should be in an otherwise empty directory. int expect = 2; // Add some files to the directory. this->FillDirectory(10); expect += 10; typename TestFixture::DirentBufferType dirents(256); // We should get all the expected entries. EXPECT_EQ(expect, this->ReadAndCountAllEntries(&dirents)); // Seek back to 0. ASSERT_NO_ERRNO(this->SeekStart()); // We should get all the expected entries again. EXPECT_EQ(expect, this->ReadAndCountAllEntries(&dirents)); } // Test that getdents() after SEEK_END succeeds. // This is a regression test for #128. TYPED_TEST(GetdentsTest, Issue128ProcSeekEnd) { auto fd = ASSERT_NO_ERRNO_AND_VALUE(Open("/proc/self", O_RDONLY | O_DIRECTORY)); typename TestFixture::DirentBufferType dirents(256); ASSERT_THAT(lseek(fd.get(), 0, SEEK_END), SyscallSucceeds()); ASSERT_THAT(RetryEINTR(syscall)(this->SyscallNum(), fd.get(), dirents.Data(), dirents.Size()), SyscallSucceeds()); } // Some tests using the glibc readdir interface. TEST(ReaddirTest, OpenDir) { DIR* dev; ASSERT_THAT(dev = opendir("/dev"), NotNull()); EXPECT_THAT(closedir(dev), SyscallSucceeds()); } TEST(ReaddirTest, RootContainsBasicDirectories) { EXPECT_THAT(ListDir("/", true), IsPosixErrorOkAndHolds(IsSupersetOf( {"bin", "dev", "etc", "lib", "proc", "sbin", "usr"}))); } TEST(ReaddirTest, Bug24096713Dev) { auto contents = ASSERT_NO_ERRNO_AND_VALUE(ListDir("/dev", true)); EXPECT_THAT(contents, Not(IsEmpty())); } TEST(ReaddirTest, Bug24096713ProcTid) { auto contents = ASSERT_NO_ERRNO_AND_VALUE( ListDir(absl::StrCat("/proc/", syscall(SYS_gettid), "/"), true)); EXPECT_THAT(contents, Not(IsEmpty())); } TEST(ReaddirTest, Bug33429925Proc) { auto contents = ASSERT_NO_ERRNO_AND_VALUE(ListDir("/proc", true)); EXPECT_THAT(contents, Not(IsEmpty())); } TEST(ReaddirTest, Bug35110122Root) { auto contents = ASSERT_NO_ERRNO_AND_VALUE(ListDir("/", true)); EXPECT_THAT(contents, Not(IsEmpty())); } // Unlink should invalidate getdents cache. TEST(ReaddirTest, GoneAfterRemoveCache) { TempPath dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); TempPath file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileIn(dir.path())); std::string name = std::string(Basename(file.path())); auto contents = ASSERT_NO_ERRNO_AND_VALUE(ListDir(dir.path(), true)); EXPECT_THAT(contents, Contains(name)); file.reset(); contents = ASSERT_NO_ERRNO_AND_VALUE(ListDir(dir.path(), true)); EXPECT_THAT(contents, Not(Contains(name))); } // Regression test for b/137398511. Rename should invalidate getdents cache. TEST(ReaddirTest, GoneAfterRenameCache) { TempPath src = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); TempPath dst = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); TempPath file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileIn(src.path())); std::string name = std::string(Basename(file.path())); auto contents = ASSERT_NO_ERRNO_AND_VALUE(ListDir(src.path(), true)); EXPECT_THAT(contents, Contains(name)); ASSERT_THAT(rename(file.path().c_str(), JoinPath(dst.path(), name).c_str()), SyscallSucceeds()); // Release file since it was renamed. dst cleanup will ultimately delete it. file.release(); contents = ASSERT_NO_ERRNO_AND_VALUE(ListDir(src.path(), true)); EXPECT_THAT(contents, Not(Contains(name))); contents = ASSERT_NO_ERRNO_AND_VALUE(ListDir(dst.path(), true)); EXPECT_THAT(contents, Contains(name)); } } // namespace } // namespace testing } // namespace gvisor