diff options
author | Brian Geffon <bgeffon@google.com> | 2018-12-10 14:41:40 -0800 |
---|---|---|
committer | Shentubot <shentubot@google.com> | 2018-12-10 14:42:34 -0800 |
commit | d3bc79bc8438206ac6a14fde4eaa288fc07eee82 (patch) | |
tree | e820398591bfd1503456e877fa0c2bdd0f994959 /test/syscalls/linux/getdents.cc | |
parent | 833edbd10b49db1f934dcb2495dcb41c1310eea4 (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.cc | 485 |
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 |