diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/linux/minidump_writer/linux_ptrace_dumper_unittest.cc | 394 |
1 files changed, 203 insertions, 191 deletions
diff --git a/src/client/linux/minidump_writer/linux_ptrace_dumper_unittest.cc b/src/client/linux/minidump_writer/linux_ptrace_dumper_unittest.cc index 04da30b4..ec00255e 100644 --- a/src/client/linux/minidump_writer/linux_ptrace_dumper_unittest.cc +++ b/src/client/linux/minidump_writer/linux_ptrace_dumper_unittest.cc @@ -41,6 +41,7 @@ #include <stdint.h> #include <string.h> #include <sys/mman.h> +#include <sys/prctl.h> #include <sys/poll.h> #include <sys/stat.h> #include <sys/types.h> @@ -63,14 +64,60 @@ namespace { typedef testing::Test LinuxPtraceDumperTest; +/* Fixture for running tests in a child process. */ +class LinuxPtraceDumperChildTest : public testing::Test { + protected: + virtual void SetUp() { + child_pid_ = fork(); + prctl(PR_SET_PTRACER, child_pid_); + } + + /* Gtest is calling TestBody from this class, which sets up a child + * process in which the RealTestBody virtual member is called. + * As such, TestBody is not supposed to be overridden in derived classes. + */ + virtual void TestBody() /* final */ { + if (child_pid_ == 0) { + // child process + RealTestBody(); + exit(HasFatalFailure() ? kFatalFailure : + (HasNonfatalFailure() ? kNonFatalFailure : 0)); + } + + ASSERT_TRUE(child_pid_ > 0); + int status; + waitpid(child_pid_, &status, 0); + if (WEXITSTATUS(status) == kFatalFailure) { + GTEST_FATAL_FAILURE_("Test failed in child process"); + } else if (WEXITSTATUS(status) == kNonFatalFailure) { + GTEST_NONFATAL_FAILURE_("Test failed in child process"); + } + } + + /* Gtest defines TestBody functions through its macros, but classes + * derived from this one need to define RealTestBody instead. + * This is achieved by defining a TestBody macro further below. + */ + virtual void RealTestBody() = 0; + private: + static const int kFatalFailure = 1; + static const int kNonFatalFailure = 2; + + pid_t child_pid_; +}; + } // namespace -TEST(LinuxPtraceDumperTest, Setup) { - LinuxPtraceDumper dumper(getpid()); +/* Replace TestBody declarations within TEST*() with RealTestBody + * declarations */ +#define TestBody RealTestBody + +TEST_F(LinuxPtraceDumperChildTest, Setup) { + LinuxPtraceDumper dumper(getppid()); } -TEST(LinuxPtraceDumperTest, FindMappings) { - LinuxPtraceDumper dumper(getpid()); +TEST_F(LinuxPtraceDumperChildTest, FindMappings) { + LinuxPtraceDumper dumper(getppid()); ASSERT_TRUE(dumper.Init()); ASSERT_TRUE(dumper.FindMapping(reinterpret_cast<void*>(getpid))); @@ -78,14 +125,14 @@ TEST(LinuxPtraceDumperTest, FindMappings) { ASSERT_FALSE(dumper.FindMapping(NULL)); } -TEST(LinuxPtraceDumperTest, ThreadList) { - LinuxPtraceDumper dumper(getpid()); +TEST_F(LinuxPtraceDumperChildTest, ThreadList) { + LinuxPtraceDumper dumper(getppid()); ASSERT_TRUE(dumper.Init()); ASSERT_GE(dumper.threads().size(), (size_t)1); bool found = false; for (size_t i = 0; i < dumper.threads().size(); ++i) { - if (dumper.threads()[i] == getpid()) { + if (dumper.threads()[i] == getppid()) { ASSERT_FALSE(found); found = true; } @@ -97,12 +144,22 @@ TEST(LinuxPtraceDumperTest, ThreadList) { // a mmap'ed mapping. class StackHelper { public: - StackHelper(int fd, char* mapping, size_t size) - : fd_(fd), mapping_(mapping), size_(size) {} + StackHelper() + : fd_(-1), mapping_(NULL), size_(0) {} ~StackHelper() { - munmap(mapping_, size_); - close(fd_); + if (size_) + munmap(mapping_, size_); + if (fd_ >= 0) + close(fd_); } + void Init(int fd, char* mapping, size_t size) { + fd_ = fd; + mapping_ = mapping; + size_ = size; + } + + char* mapping() const { return mapping_; } + size_t size() const { return size_; } private: int fd_; @@ -110,19 +167,28 @@ class StackHelper { size_t size_; }; -TEST(LinuxPtraceDumperTest, MergedMappings) { - string helper_path(GetHelperBinary()); - if (helper_path.empty()) { +class LinuxPtraceDumperMappingsTest : public LinuxPtraceDumperChildTest { + protected: + virtual void SetUp(); + + string helper_path_; + size_t page_size_; + StackHelper helper_; +}; + +void LinuxPtraceDumperMappingsTest::SetUp() { + helper_path_ = GetHelperBinary(); + if (helper_path_.empty()) { FAIL() << "Couldn't find helper binary"; exit(1); } // mmap two segments out of the helper binary, one // enclosed in the other, but with different protections. - const size_t kPageSize = sysconf(_SC_PAGESIZE); - const size_t kMappingSize = 3 * kPageSize; - int fd = open(helper_path.c_str(), O_RDONLY); - ASSERT_NE(-1, fd) << "Failed to open file: " << helper_path + page_size_ = sysconf(_SC_PAGESIZE); + const size_t kMappingSize = 3 * page_size_; + int fd = open(helper_path_.c_str(), O_RDONLY); + ASSERT_NE(-1, fd) << "Failed to open file: " << helper_path_ << ", Error: " << strerror(errno); char* mapping = reinterpret_cast<char*>(mmap(NULL, @@ -133,34 +199,37 @@ TEST(LinuxPtraceDumperTest, MergedMappings) { 0)); ASSERT_TRUE(mapping); - const uintptr_t kMappingAddress = reinterpret_cast<uintptr_t>(mapping); - // Ensure that things get cleaned up. - StackHelper helper(fd, mapping, kMappingSize); + helper_.Init(fd, mapping, kMappingSize); // Carve a page out of the first mapping with different permissions. char* inside_mapping = reinterpret_cast<char*>( - mmap(mapping + 2 *kPageSize, - kPageSize, + mmap(mapping + 2 * page_size_, + page_size_, PROT_NONE, MAP_SHARED | MAP_FIXED, fd, // Map a different offset just to // better test real-world conditions. - kPageSize)); + page_size_)); ASSERT_TRUE(inside_mapping); + LinuxPtraceDumperChildTest::SetUp(); +} + +TEST_F(LinuxPtraceDumperMappingsTest, MergedMappings) { // Now check that LinuxPtraceDumper interpreted the mappings properly. - LinuxPtraceDumper dumper(getpid()); + LinuxPtraceDumper dumper(getppid()); ASSERT_TRUE(dumper.Init()); int mapping_count = 0; for (unsigned i = 0; i < dumper.mappings().size(); ++i) { const MappingInfo& mapping = *dumper.mappings()[i]; - if (strcmp(mapping.name, helper_path.c_str()) == 0) { + if (strcmp(mapping.name, this->helper_path_.c_str()) == 0) { // This mapping should encompass the entire original mapped // range. - EXPECT_EQ(kMappingAddress, mapping.start_addr); - EXPECT_EQ(kMappingSize, mapping.size); + EXPECT_EQ(reinterpret_cast<uintptr_t>(this->helper_.mapping()), + mapping.start_addr); + EXPECT_EQ(this->helper_.size(), mapping.size); EXPECT_EQ(0U, mapping.offset); mapping_count++; } @@ -168,104 +237,8 @@ TEST(LinuxPtraceDumperTest, MergedMappings) { EXPECT_EQ(1, mapping_count); } -TEST(LinuxPtraceDumperTest, VerifyStackReadWithMultipleThreads) { - static const int kNumberOfThreadsInHelperProgram = 5; - char kNumberOfThreadsArgument[2]; - sprintf(kNumberOfThreadsArgument, "%d", kNumberOfThreadsInHelperProgram); - - int fds[2]; - ASSERT_NE(-1, pipe(fds)); - - pid_t child_pid = fork(); - if (child_pid == 0) { - // In child process. - close(fds[0]); - - string helper_path(GetHelperBinary()); - if (helper_path.empty()) { - FAIL() << "Couldn't find helper binary"; - exit(1); - } - - // Pass the pipe fd and the number of threads as arguments. - char pipe_fd_string[8]; - sprintf(pipe_fd_string, "%d", fds[1]); - execl(helper_path.c_str(), - "linux_dumper_unittest_helper", - pipe_fd_string, - kNumberOfThreadsArgument, - NULL); - // Kill if we get here. - printf("Errno from exec: %d", errno); - FAIL() << "Exec of " << helper_path << " failed: " << strerror(errno); - exit(0); - } - close(fds[1]); - - // Wait for all child threads to indicate that they have started - for (int threads = 0; threads < kNumberOfThreadsInHelperProgram; threads++) { - struct pollfd pfd; - memset(&pfd, 0, sizeof(pfd)); - pfd.fd = fds[0]; - pfd.events = POLLIN | POLLERR; - - const int r = HANDLE_EINTR(poll(&pfd, 1, 1000)); - ASSERT_EQ(1, r); - ASSERT_TRUE(pfd.revents & POLLIN); - uint8_t junk; - ASSERT_EQ(read(fds[0], &junk, sizeof(junk)), - static_cast<ssize_t>(sizeof(junk))); - } - close(fds[0]); - - // There is a race here because we may stop a child thread before - // it is actually running the busy loop. Empirically this sleep - // is sufficient to avoid the race. - usleep(100000); - - // Children are ready now. - LinuxPtraceDumper dumper(child_pid); - ASSERT_TRUE(dumper.Init()); - EXPECT_EQ((size_t)kNumberOfThreadsInHelperProgram, dumper.threads().size()); - EXPECT_TRUE(dumper.ThreadsSuspend()); - - ThreadInfo one_thread; - for (size_t i = 0; i < dumper.threads().size(); ++i) { - EXPECT_TRUE(dumper.GetThreadInfoByIndex(i, &one_thread)); - const void* stack; - size_t stack_len; - EXPECT_TRUE(dumper.GetStackInfo(&stack, &stack_len, - one_thread.stack_pointer)); - // In the helper program, we stored a pointer to the thread id in a - // specific register. Check that we can recover its value. -#if defined(__ARM_EABI__) - pid_t *process_tid_location = (pid_t *)(one_thread.regs.uregs[3]); -#elif defined(__i386) - pid_t *process_tid_location = (pid_t *)(one_thread.regs.ecx); -#elif defined(__x86_64) - pid_t *process_tid_location = (pid_t *)(one_thread.regs.rcx); -#else -#error This test has not been ported to this platform. -#endif - pid_t one_thread_id; - dumper.CopyFromProcess(&one_thread_id, - dumper.threads()[i], - process_tid_location, - 4); - EXPECT_EQ(dumper.threads()[i], one_thread_id); - } - EXPECT_TRUE(dumper.ThreadsResume()); - kill(child_pid, SIGKILL); - - // Reap child - int status; - ASSERT_NE(-1, HANDLE_EINTR(waitpid(child_pid, &status, 0))); - ASSERT_TRUE(WIFSIGNALED(status)); - ASSERT_EQ(SIGKILL, WTERMSIG(status)); -} - -TEST(LinuxPtraceDumperTest, BuildProcPath) { - const pid_t pid = getpid(); +TEST_F(LinuxPtraceDumperChildTest, BuildProcPath) { + const pid_t pid = getppid(); LinuxPtraceDumper dumper(pid); char maps_path[NAME_MAX] = ""; @@ -289,8 +262,8 @@ TEST(LinuxPtraceDumperTest, BuildProcPath) { #if !defined(__ARM_EABI__) // Ensure that the linux-gate VDSO is included in the mapping list. -TEST(LinuxPtraceDumperTest, MappingsIncludeLinuxGate) { - LinuxPtraceDumper dumper(getpid()); +TEST_F(LinuxPtraceDumperChildTest, MappingsIncludeLinuxGate) { + LinuxPtraceDumper dumper(getppid()); ASSERT_TRUE(dumper.Init()); void* linux_gate_loc = @@ -313,51 +286,8 @@ TEST(LinuxPtraceDumperTest, MappingsIncludeLinuxGate) { } // Ensure that the linux-gate VDSO can generate a non-zeroed File ID. -TEST(LinuxPtraceDumperTest, LinuxGateMappingID) { - LinuxPtraceDumper dumper(getpid()); - ASSERT_TRUE(dumper.Init()); - - bool found_linux_gate = false; - const wasteful_vector<MappingInfo*> mappings = dumper.mappings(); - unsigned index = 0; - for (unsigned i = 0; i < mappings.size(); ++i) { - if (!strcmp(mappings[i]->name, kLinuxGateLibraryName)) { - found_linux_gate = true; - index = i; - break; - } - } - ASSERT_TRUE(found_linux_gate); - - uint8_t identifier[sizeof(MDGUID)]; - ASSERT_TRUE(dumper.ElfFileIdentifierForMapping(*mappings[index], - true, - index, - identifier)); - uint8_t empty_identifier[sizeof(MDGUID)]; - memset(empty_identifier, 0, sizeof(empty_identifier)); - EXPECT_NE(0, memcmp(empty_identifier, identifier, sizeof(identifier))); -} - -// Ensure that the linux-gate VDSO can generate a non-zeroed File ID -// from a child process. -TEST(LinuxPtraceDumperTest, LinuxGateMappingIDChild) { - int fds[2]; - ASSERT_NE(-1, pipe(fds)); - - // Fork a child so ptrace works. - const pid_t child = fork(); - if (child == 0) { - close(fds[1]); - // Now wait forever for the parent. - char b; - IGNORE_RET(HANDLE_EINTR(read(fds[0], &b, sizeof(b)))); - close(fds[0]); - syscall(__NR_exit); - } - close(fds[0]); - - LinuxPtraceDumper dumper(child); +TEST_F(LinuxPtraceDumperChildTest, LinuxGateMappingID) { + LinuxPtraceDumper dumper(getppid()); ASSERT_TRUE(dumper.Init()); bool found_linux_gate = false; @@ -383,33 +313,17 @@ TEST(LinuxPtraceDumperTest, LinuxGateMappingIDChild) { memset(empty_identifier, 0, sizeof(empty_identifier)); EXPECT_NE(0, memcmp(empty_identifier, identifier, sizeof(identifier))); EXPECT_TRUE(dumper.ThreadsResume()); - close(fds[1]); } #endif -TEST(LinuxPtraceDumperTest, FileIDsMatch) { +TEST_F(LinuxPtraceDumperChildTest, FileIDsMatch) { // Calculate the File ID of our binary using both // FileID::ElfFileIdentifier and LinuxDumper::ElfFileIdentifierForMapping // and ensure that we get the same result from both. char exe_name[PATH_MAX]; ASSERT_TRUE(SafeReadLink("/proc/self/exe", exe_name)); - int fds[2]; - ASSERT_NE(-1, pipe(fds)); - - // Fork a child so ptrace works. - const pid_t child = fork(); - if (child == 0) { - close(fds[1]); - // Now wait forever for the parent. - char b; - IGNORE_RET(HANDLE_EINTR(read(fds[0], &b, sizeof(b)))); - close(fds[0]); - syscall(__NR_exit); - } - close(fds[0]); - - LinuxPtraceDumper dumper(child); + LinuxPtraceDumper dumper(getppid()); ASSERT_TRUE(dumper.Init()); const wasteful_vector<MappingInfo*> mappings = dumper.mappings(); bool found_exe = false; @@ -436,5 +350,103 @@ TEST(LinuxPtraceDumperTest, FileIDsMatch) { FileID::ConvertIdentifierToString(identifier2, identifier_string2, 37); EXPECT_STREQ(identifier_string1, identifier_string2); +} + +/* Get back to normal behavior of TEST*() macros wrt TestBody. */ +#undef TestBody + +TEST(LinuxPtraceDumperTest, VerifyStackReadWithMultipleThreads) { + static const int kNumberOfThreadsInHelperProgram = 5; + char kNumberOfThreadsArgument[2]; + sprintf(kNumberOfThreadsArgument, "%d", kNumberOfThreadsInHelperProgram); + + int fds[2]; + ASSERT_NE(-1, pipe(fds)); + + pid_t child_pid = fork(); + if (child_pid == 0) { + // In child process. + close(fds[0]); + + string helper_path(GetHelperBinary()); + if (helper_path.empty()) { + FAIL() << "Couldn't find helper binary"; + exit(1); + } + + // Pass the pipe fd and the number of threads as arguments. + char pipe_fd_string[8]; + sprintf(pipe_fd_string, "%d", fds[1]); + execl(helper_path.c_str(), + "linux_dumper_unittest_helper", + pipe_fd_string, + kNumberOfThreadsArgument, + NULL); + // Kill if we get here. + printf("Errno from exec: %d", errno); + FAIL() << "Exec of " << helper_path << " failed: " << strerror(errno); + exit(0); + } close(fds[1]); + + // Wait for all child threads to indicate that they have started + for (int threads = 0; threads < kNumberOfThreadsInHelperProgram; threads++) { + struct pollfd pfd; + memset(&pfd, 0, sizeof(pfd)); + pfd.fd = fds[0]; + pfd.events = POLLIN | POLLERR; + + const int r = HANDLE_EINTR(poll(&pfd, 1, 1000)); + ASSERT_EQ(1, r); + ASSERT_TRUE(pfd.revents & POLLIN); + uint8_t junk; + ASSERT_EQ(read(fds[0], &junk, sizeof(junk)), + static_cast<ssize_t>(sizeof(junk))); + } + close(fds[0]); + + // There is a race here because we may stop a child thread before + // it is actually running the busy loop. Empirically this sleep + // is sufficient to avoid the race. + usleep(100000); + + // Children are ready now. + LinuxPtraceDumper dumper(child_pid); + ASSERT_TRUE(dumper.Init()); + EXPECT_EQ((size_t)kNumberOfThreadsInHelperProgram, dumper.threads().size()); + EXPECT_TRUE(dumper.ThreadsSuspend()); + + ThreadInfo one_thread; + for (size_t i = 0; i < dumper.threads().size(); ++i) { + EXPECT_TRUE(dumper.GetThreadInfoByIndex(i, &one_thread)); + const void* stack; + size_t stack_len; + EXPECT_TRUE(dumper.GetStackInfo(&stack, &stack_len, + one_thread.stack_pointer)); + // In the helper program, we stored a pointer to the thread id in a + // specific register. Check that we can recover its value. +#if defined(__ARM_EABI__) + pid_t* process_tid_location = (pid_t*)(one_thread.regs.uregs[3]); +#elif defined(__i386) + pid_t* process_tid_location = (pid_t*)(one_thread.regs.ecx); +#elif defined(__x86_64) + pid_t* process_tid_location = (pid_t*)(one_thread.regs.rcx); +#else +#error This test has not been ported to this platform. +#endif + pid_t one_thread_id; + dumper.CopyFromProcess(&one_thread_id, + dumper.threads()[i], + process_tid_location, + 4); + EXPECT_EQ(dumper.threads()[i], one_thread_id); + } + EXPECT_TRUE(dumper.ThreadsResume()); + kill(child_pid, SIGKILL); + + // Reap child + int status; + ASSERT_NE(-1, HANDLE_EINTR(waitpid(child_pid, &status, 0))); + ASSERT_TRUE(WIFSIGNALED(status)); + ASSERT_EQ(SIGKILL, WTERMSIG(status)); } |