summaryrefslogtreecommitdiffhomepage
path: root/test/syscalls/linux/getdents.cc
diff options
context:
space:
mode:
authorBrian Geffon <bgeffon@google.com>2018-12-10 14:41:40 -0800
committerShentubot <shentubot@google.com>2018-12-10 14:42:34 -0800
commitd3bc79bc8438206ac6a14fde4eaa288fc07eee82 (patch)
treee820398591bfd1503456e877fa0c2bdd0f994959 /test/syscalls/linux/getdents.cc
parent833edbd10b49db1f934dcb2495dcb41c1310eea4 (diff)
Open source system call tests.
PiperOrigin-RevId: 224886231 Change-Id: I0fccb4d994601739d8b16b1d4e6b31f40297fb22
Diffstat (limited to 'test/syscalls/linux/getdents.cc')
-rw-r--r--test/syscalls/linux/getdents.cc485
1 files changed, 485 insertions, 0 deletions
diff --git a/test/syscalls/linux/getdents.cc b/test/syscalls/linux/getdents.cc
new file mode 100644
index 000000000..5db580aa0
--- /dev/null
+++ b/test/syscalls/linux/getdents.cc
@@ -0,0 +1,485 @@
+// Copyright 2018 Google LLC
+//
+// 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/eventfd.h>
+#include <sys/mman.h>
+#include <sys/types.h>
+#include <syscall.h>
+#include <unistd.h>
+#include <map>
+#include <string>
+#include <unordered_set>
+#include <utility>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/strings/numbers.h"
+#include "absl/strings/str_cat.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::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_;
+};
+
+// GUnit TYPED_TEST_CASE does not allow multiple template parameters, so we
+// must use explicit template specialization to set the syscall number.
+template <>
+int GetdentsTest<struct linux_dirent>::SyscallNum() {
+ return SYS_getdents;
+}
+
+template <>
+int GetdentsTest<struct linux_dirent64>::SyscallNum() {
+ return SYS_getdents64;
+}
+
+// Test both legacy getdents and getdents64.
+typedef ::testing::Types<struct linux_dirent, struct linux_dirent64>
+ GetdentsTypes;
+TYPED_TEST_CASE(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 betweek 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_set<int> fds;
+ std::vector<FileDescriptor> fd_closers;
+ fd_closers.reserve(fds.size());
+ for (int fd : fds) {
+ fd_closers.emplace_back(fd);
+ }
+ for (size_t i = 0; i < kNfds; i++) {
+ int fd;
+ ASSERT_THAT(fd = eventfd(0, 0), SyscallSucceeds());
+ fds.insert(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));
+ std::unordered_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;
+ auto it = fds.find(dfd);
+ if (it != fds.end()) {
+ fds.erase(it);
+ EXPECT_THAT(close(dfd), SyscallSucceeds());
+ }
+ }
+ }
+
+ // 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));
+}
+
+// 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()));
+}
+
+} // namespace
+
+} // namespace testing
+} // namespace gvisor