// Copyright (c) 2010, Google Inc. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above // copyright notice, this list of conditions and the following disclaimer // in the documentation and/or other materials provided with the // distribution. // * Neither the name of Google Inc. nor the names of its // contributors may be used to endorse or promote products derived from // this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // exception_handler_test.cc: Unit tests for google_breakpad::ExceptionHandler #include <pthread.h> #include <sys/mman.h> #include <sys/stat.h> #include <unistd.h> #include "breakpad_googletest_includes.h" #include "client/mac/handler/exception_handler.h" #include "common/mac/MachIPC.h" #include "common/tests/auto_tempdir.h" #include "google_breakpad/processor/minidump.h" namespace google_breakpad { // This acts as the log sink for INFO logging from the processor // logging code. The logging output confuses XCode and makes it think // there are unit test failures. testlogging.h handles the overriding. std::ostringstream info_log; } namespace { using std::string; using google_breakpad::AutoTempDir; using google_breakpad::ExceptionHandler; using google_breakpad::MachPortSender; using google_breakpad::MachReceiveMessage; using google_breakpad::MachSendMessage; using google_breakpad::Minidump; using google_breakpad::MinidumpContext; using google_breakpad::MinidumpException; using google_breakpad::MinidumpMemoryList; using google_breakpad::MinidumpMemoryRegion; using google_breakpad::ReceivePort; using testing::Test; class ExceptionHandlerTest : public Test { public: void InProcessCrash(bool aborting); AutoTempDir tempDir; string lastDumpName; }; static void Crasher() { int *a = (int*)0x42; fprintf(stdout, "Going to crash...\n"); fprintf(stdout, "A = %d", *a); } static void AbortCrasher() { fprintf(stdout, "Going to crash...\n"); abort(); } static void SoonToCrash(void(*crasher)()) { crasher(); } static bool MDCallback(const char *dump_dir, const char *file_name, void *context, bool success) { string path(dump_dir); path.append("/"); path.append(file_name); path.append(".dmp"); int fd = *reinterpret_cast<int*>(context); (void)write(fd, path.c_str(), path.length() + 1); close(fd); exit(0); // not reached return true; } void ExceptionHandlerTest::InProcessCrash(bool aborting) { // Give the child process a pipe to report back on. int fds[2]; ASSERT_EQ(0, pipe(fds)); // Fork off a child process so it can crash. pid_t pid = fork(); if (pid == 0) { // In the child process. close(fds[0]); ExceptionHandler eh(tempDir.path(), NULL, MDCallback, &fds[1], true, NULL); // crash SoonToCrash(aborting ? &AbortCrasher : &Crasher); // not reached exit(1); } // In the parent process. ASSERT_NE(-1, pid); // Wait for the background process to return the minidump file. close(fds[1]); char minidump_file[PATH_MAX]; ssize_t nbytes = read(fds[0], minidump_file, sizeof(minidump_file)); ASSERT_NE(0, nbytes); Minidump minidump(minidump_file); ASSERT_TRUE(minidump.Read()); MinidumpException* exception = minidump.GetException(); ASSERT_TRUE(exception); const MDRawExceptionStream* raw_exception = exception->exception(); ASSERT_TRUE(raw_exception); if (aborting) { EXPECT_EQ(MD_EXCEPTION_MAC_SOFTWARE, raw_exception->exception_record.exception_code); EXPECT_EQ(MD_EXCEPTION_CODE_MAC_ABORT, raw_exception->exception_record.exception_flags); } else { EXPECT_EQ(MD_EXCEPTION_MAC_BAD_ACCESS, raw_exception->exception_record.exception_code); #if defined(__x86_64__) EXPECT_EQ(MD_EXCEPTION_CODE_MAC_INVALID_ADDRESS, raw_exception->exception_record.exception_flags); #elif defined(__i386__) EXPECT_EQ(MD_EXCEPTION_CODE_MAC_PROTECTION_FAILURE, raw_exception->exception_record.exception_flags); #endif } const MinidumpContext* context = exception->GetContext(); ASSERT_TRUE(context); uint64_t instruction_pointer; ASSERT_TRUE(context->GetInstructionPointer(&instruction_pointer)); // Ideally would like to sanity check that abort() is on the stack // but that's hard. MinidumpMemoryList* memory_list = minidump.GetMemoryList(); ASSERT_TRUE(memory_list); MinidumpMemoryRegion* region = memory_list->GetMemoryRegionForAddress(instruction_pointer); EXPECT_TRUE(region); // Child process should have exited with a zero status. int ret; ASSERT_EQ(pid, waitpid(pid, &ret, 0)); EXPECT_NE(0, WIFEXITED(ret)); EXPECT_EQ(0, WEXITSTATUS(ret)); } TEST_F(ExceptionHandlerTest, InProcess) { InProcessCrash(false); } TEST_F(ExceptionHandlerTest, InProcessAbort) { InProcessCrash(true); } static bool DumpNameMDCallback(const char *dump_dir, const char *file_name, void *context, bool success) { ExceptionHandlerTest *self = reinterpret_cast<ExceptionHandlerTest*>(context); if (dump_dir && file_name) { self->lastDumpName = dump_dir; self->lastDumpName += "/"; self->lastDumpName += file_name; self->lastDumpName += ".dmp"; } return true; } TEST_F(ExceptionHandlerTest, WriteMinidump) { ExceptionHandler eh(tempDir.path(), NULL, DumpNameMDCallback, this, true, NULL); ASSERT_TRUE(eh.WriteMinidump()); // Ensure that minidump file exists and is > 0 bytes. ASSERT_FALSE(lastDumpName.empty()); struct stat st; ASSERT_EQ(0, stat(lastDumpName.c_str(), &st)); ASSERT_LT(0, st.st_size); // The minidump should not contain an exception stream. Minidump minidump(lastDumpName); ASSERT_TRUE(minidump.Read()); MinidumpException* exception = minidump.GetException(); EXPECT_FALSE(exception); } TEST_F(ExceptionHandlerTest, WriteMinidumpWithException) { ExceptionHandler eh(tempDir.path(), NULL, DumpNameMDCallback, this, true, NULL); ASSERT_TRUE(eh.WriteMinidump(true)); // Ensure that minidump file exists and is > 0 bytes. ASSERT_FALSE(lastDumpName.empty()); struct stat st; ASSERT_EQ(0, stat(lastDumpName.c_str(), &st)); ASSERT_LT(0, st.st_size); // The minidump should contain an exception stream. Minidump minidump(lastDumpName); ASSERT_TRUE(minidump.Read()); MinidumpException* exception = minidump.GetException(); ASSERT_TRUE(exception); const MDRawExceptionStream* raw_exception = exception->exception(); ASSERT_TRUE(raw_exception); EXPECT_EQ(MD_EXCEPTION_MAC_BREAKPOINT, raw_exception->exception_record.exception_code); } TEST_F(ExceptionHandlerTest, DumpChildProcess) { const int kTimeoutMs = 2000; // Create a mach port to receive the child task on. char machPortName[128]; sprintf(machPortName, "ExceptionHandlerTest.%d", getpid()); ReceivePort parent_recv_port(machPortName); // Give the child process a pipe to block on. int fds[2]; ASSERT_EQ(0, pipe(fds)); // Fork off a child process to dump. pid_t pid = fork(); if (pid == 0) { // In the child process close(fds[1]); // Send parent process the task and thread ports. MachSendMessage child_message(0); child_message.AddDescriptor(mach_task_self()); child_message.AddDescriptor(mach_thread_self()); MachPortSender child_sender(machPortName); if (child_sender.SendMessage(child_message, kTimeoutMs) != KERN_SUCCESS) exit(1); // Wait for the parent process. uint8_t data; read(fds[0], &data, 1); exit(0); } // In the parent process. ASSERT_NE(-1, pid); close(fds[0]); // Read the child's task and thread ports. MachReceiveMessage child_message; ASSERT_EQ(KERN_SUCCESS, parent_recv_port.WaitForMessage(&child_message, kTimeoutMs)); mach_port_t child_task = child_message.GetTranslatedPort(0); mach_port_t child_thread = child_message.GetTranslatedPort(1); ASSERT_NE((mach_port_t)MACH_PORT_NULL, child_task); ASSERT_NE((mach_port_t)MACH_PORT_NULL, child_thread); // Write a minidump of the child process. bool result = ExceptionHandler::WriteMinidumpForChild(child_task, child_thread, tempDir.path(), DumpNameMDCallback, this); ASSERT_EQ(true, result); // Ensure that minidump file exists and is > 0 bytes. ASSERT_FALSE(lastDumpName.empty()); struct stat st; ASSERT_EQ(0, stat(lastDumpName.c_str(), &st)); ASSERT_LT(0, st.st_size); // Unblock child process uint8_t data = 1; (void)write(fds[1], &data, 1); // Child process should have exited with a zero status. int ret; ASSERT_EQ(pid, waitpid(pid, &ret, 0)); EXPECT_NE(0, WIFEXITED(ret)); EXPECT_EQ(0, WEXITSTATUS(ret)); } // Test that memory around the instruction pointer is written // to the dump as a MinidumpMemoryRegion. TEST_F(ExceptionHandlerTest, InstructionPointerMemory) { // Give the child process a pipe to report back on. int fds[2]; ASSERT_EQ(0, pipe(fds)); // These are defined here so the parent can use them to check the // data from the minidump afterwards. const uint32_t kMemorySize = 256; // bytes const int kOffset = kMemorySize / 2; // This crashes with SIGILL on x86/x86-64/arm. const unsigned char instructions[] = { 0xff, 0xff, 0xff, 0xff }; pid_t pid = fork(); if (pid == 0) { close(fds[0]); ExceptionHandler eh(tempDir.path(), NULL, MDCallback, &fds[1], true, NULL); // Get some executable memory. char* memory = reinterpret_cast<char*>(mmap(NULL, kMemorySize, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANON, -1, 0)); if (!memory) exit(0); // Write some instructions that will crash. Put them in the middle // of the block of memory, because the minidump should contain 128 // bytes on either side of the instruction pointer. memcpy(memory + kOffset, instructions, sizeof(instructions)); // Now execute the instructions, which should crash. typedef void (*void_function)(void); void_function memory_function = reinterpret_cast<void_function>(memory + kOffset); memory_function(); // not reached exit(1); } // In the parent process. ASSERT_NE(-1, pid); close(fds[1]); // Wait for the background process to return the minidump file. close(fds[1]); char minidump_file[PATH_MAX]; ssize_t nbytes = read(fds[0], minidump_file, sizeof(minidump_file)); ASSERT_NE(0, nbytes); // Ensure that minidump file exists and is > 0 bytes. struct stat st; ASSERT_EQ(0, stat(minidump_file, &st)); ASSERT_LT(0, st.st_size); // Child process should have exited with a zero status. int ret; ASSERT_EQ(pid, waitpid(pid, &ret, 0)); EXPECT_NE(0, WIFEXITED(ret)); EXPECT_EQ(0, WEXITSTATUS(ret)); // Read the minidump. Locate the exception record and the // memory list, and then ensure that there is a memory region // in the memory list that covers the instruction pointer from // the exception record. Minidump minidump(minidump_file); ASSERT_TRUE(minidump.Read()); MinidumpException* exception = minidump.GetException(); MinidumpMemoryList* memory_list = minidump.GetMemoryList(); ASSERT_TRUE(exception); ASSERT_TRUE(memory_list); ASSERT_NE((unsigned int)0, memory_list->region_count()); MinidumpContext* context = exception->GetContext(); ASSERT_TRUE(context); uint64_t instruction_pointer; ASSERT_TRUE(context->GetInstructionPointer(&instruction_pointer)); MinidumpMemoryRegion* region = memory_list->GetMemoryRegionForAddress(instruction_pointer); EXPECT_TRUE(region); EXPECT_EQ(kMemorySize, region->GetSize()); const uint8_t* bytes = region->GetMemory(); ASSERT_TRUE(bytes); uint8_t prefix_bytes[kOffset]; uint8_t suffix_bytes[kMemorySize - kOffset - sizeof(instructions)]; memset(prefix_bytes, 0, sizeof(prefix_bytes)); memset(suffix_bytes, 0, sizeof(suffix_bytes)); EXPECT_TRUE(memcmp(bytes, prefix_bytes, sizeof(prefix_bytes)) == 0); EXPECT_TRUE(memcmp(bytes + kOffset, instructions, sizeof(instructions)) == 0); EXPECT_TRUE(memcmp(bytes + kOffset + sizeof(instructions), suffix_bytes, sizeof(suffix_bytes)) == 0); } // Test that the memory region around the instruction pointer is // bounded correctly on the low end. TEST_F(ExceptionHandlerTest, InstructionPointerMemoryMinBound) { // Give the child process a pipe to report back on. int fds[2]; ASSERT_EQ(0, pipe(fds)); // These are defined here so the parent can use them to check the // data from the minidump afterwards. const uint32_t kMemorySize = 256; // bytes const int kOffset = 0; // This crashes with SIGILL on x86/x86-64/arm. const unsigned char instructions[] = { 0xff, 0xff, 0xff, 0xff }; pid_t pid = fork(); if (pid == 0) { close(fds[0]); ExceptionHandler eh(tempDir.path(), NULL, MDCallback, &fds[1], true, NULL); // Get some executable memory. char* memory = reinterpret_cast<char*>(mmap(NULL, kMemorySize, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANON, -1, 0)); if (!memory) exit(0); // Write some instructions that will crash. Put them at the start // of the block of memory, to ensure that the memory bounding // works properly. memcpy(memory + kOffset, instructions, sizeof(instructions)); // Now execute the instructions, which should crash. typedef void (*void_function)(void); void_function memory_function = reinterpret_cast<void_function>(memory + kOffset); memory_function(); // not reached exit(1); } // In the parent process. ASSERT_NE(-1, pid); close(fds[1]); // Wait for the background process to return the minidump file. close(fds[1]); char minidump_file[PATH_MAX]; ssize_t nbytes = read(fds[0], minidump_file, sizeof(minidump_file)); ASSERT_NE(0, nbytes); // Ensure that minidump file exists and is > 0 bytes. struct stat st; ASSERT_EQ(0, stat(minidump_file, &st)); ASSERT_LT(0, st.st_size); // Child process should have exited with a zero status. int ret; ASSERT_EQ(pid, waitpid(pid, &ret, 0)); EXPECT_NE(0, WIFEXITED(ret)); EXPECT_EQ(0, WEXITSTATUS(ret)); // Read the minidump. Locate the exception record and the // memory list, and then ensure that there is a memory region // in the memory list that covers the instruction pointer from // the exception record. Minidump minidump(minidump_file); ASSERT_TRUE(minidump.Read()); MinidumpException* exception = minidump.GetException(); MinidumpMemoryList* memory_list = minidump.GetMemoryList(); ASSERT_TRUE(exception); ASSERT_TRUE(memory_list); ASSERT_NE((unsigned int)0, memory_list->region_count()); MinidumpContext* context = exception->GetContext(); ASSERT_TRUE(context); uint64_t instruction_pointer; ASSERT_TRUE(context->GetInstructionPointer(&instruction_pointer)); MinidumpMemoryRegion* region = memory_list->GetMemoryRegionForAddress(instruction_pointer); EXPECT_TRUE(region); EXPECT_EQ(kMemorySize / 2, region->GetSize()); const uint8_t* bytes = region->GetMemory(); ASSERT_TRUE(bytes); uint8_t suffix_bytes[kMemorySize / 2 - sizeof(instructions)]; memset(suffix_bytes, 0, sizeof(suffix_bytes)); EXPECT_TRUE(memcmp(bytes + kOffset, instructions, sizeof(instructions)) == 0); EXPECT_TRUE(memcmp(bytes + kOffset + sizeof(instructions), suffix_bytes, sizeof(suffix_bytes)) == 0); } // Test that the memory region around the instruction pointer is // bounded correctly on the high end. TEST_F(ExceptionHandlerTest, InstructionPointerMemoryMaxBound) { // Give the child process a pipe to report back on. int fds[2]; ASSERT_EQ(0, pipe(fds)); // These are defined here so the parent can use them to check the // data from the minidump afterwards. // Use 4k here because the OS will hand out a single page even // if a smaller size is requested, and this test wants to // test the upper bound of the memory range. const uint32_t kMemorySize = 4096; // bytes // This crashes with SIGILL on x86/x86-64/arm. const unsigned char instructions[] = { 0xff, 0xff, 0xff, 0xff }; const int kOffset = kMemorySize - sizeof(instructions); pid_t pid = fork(); if (pid == 0) { close(fds[0]); ExceptionHandler eh(tempDir.path(), NULL, MDCallback, &fds[1], true, NULL); // Get some executable memory. char* memory = reinterpret_cast<char*>(mmap(NULL, kMemorySize, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANON, -1, 0)); if (!memory) exit(0); // Write some instructions that will crash. Put them at the start // of the block of memory, to ensure that the memory bounding // works properly. memcpy(memory + kOffset, instructions, sizeof(instructions)); // Now execute the instructions, which should crash. typedef void (*void_function)(void); void_function memory_function = reinterpret_cast<void_function>(memory + kOffset); memory_function(); // not reached exit(1); } // In the parent process. ASSERT_NE(-1, pid); close(fds[1]); // Wait for the background process to return the minidump file. close(fds[1]); char minidump_file[PATH_MAX]; ssize_t nbytes = read(fds[0], minidump_file, sizeof(minidump_file)); ASSERT_NE(0, nbytes); // Ensure that minidump file exists and is > 0 bytes. struct stat st; ASSERT_EQ(0, stat(minidump_file, &st)); ASSERT_LT(0, st.st_size); // Child process should have exited with a zero status. int ret; ASSERT_EQ(pid, waitpid(pid, &ret, 0)); EXPECT_NE(0, WIFEXITED(ret)); EXPECT_EQ(0, WEXITSTATUS(ret)); // Read the minidump. Locate the exception record and the // memory list, and then ensure that there is a memory region // in the memory list that covers the instruction pointer from // the exception record. Minidump minidump(minidump_file); ASSERT_TRUE(minidump.Read()); MinidumpException* exception = minidump.GetException(); MinidumpMemoryList* memory_list = minidump.GetMemoryList(); ASSERT_TRUE(exception); ASSERT_TRUE(memory_list); ASSERT_NE((unsigned int)0, memory_list->region_count()); MinidumpContext* context = exception->GetContext(); ASSERT_TRUE(context); uint64_t instruction_pointer; ASSERT_TRUE(context->GetInstructionPointer(&instruction_pointer)); MinidumpMemoryRegion* region = memory_list->GetMemoryRegionForAddress(instruction_pointer); EXPECT_TRUE(region); const size_t kPrefixSize = 128; // bytes EXPECT_EQ(kPrefixSize + sizeof(instructions), region->GetSize()); const uint8_t* bytes = region->GetMemory(); ASSERT_TRUE(bytes); uint8_t prefix_bytes[kPrefixSize]; memset(prefix_bytes, 0, sizeof(prefix_bytes)); EXPECT_TRUE(memcmp(bytes, prefix_bytes, sizeof(prefix_bytes)) == 0); EXPECT_TRUE(memcmp(bytes + kPrefixSize, instructions, sizeof(instructions)) == 0); } // Ensure that an extra memory block doesn't get added when the // instruction pointer is not in mapped memory. TEST_F(ExceptionHandlerTest, InstructionPointerMemoryNullPointer) { // Give the child process a pipe to report back on. int fds[2]; ASSERT_EQ(0, pipe(fds)); pid_t pid = fork(); if (pid == 0) { close(fds[0]); ExceptionHandler eh(tempDir.path(), NULL, MDCallback, &fds[1], true, NULL); // Try calling a NULL pointer. typedef void (*void_function)(void); void_function memory_function = reinterpret_cast<void_function>(NULL); memory_function(); // not reached exit(1); } // In the parent process. ASSERT_NE(-1, pid); close(fds[1]); // Wait for the background process to return the minidump file. close(fds[1]); char minidump_file[PATH_MAX]; ssize_t nbytes = read(fds[0], minidump_file, sizeof(minidump_file)); ASSERT_NE(0, nbytes); // Ensure that minidump file exists and is > 0 bytes. struct stat st; ASSERT_EQ(0, stat(minidump_file, &st)); ASSERT_LT(0, st.st_size); // Child process should have exited with a zero status. int ret; ASSERT_EQ(pid, waitpid(pid, &ret, 0)); EXPECT_NE(0, WIFEXITED(ret)); EXPECT_EQ(0, WEXITSTATUS(ret)); // Read the minidump. Locate the exception record and the // memory list, and then ensure that there is only one memory region // in the memory list (the thread memory from the single thread). Minidump minidump(minidump_file); ASSERT_TRUE(minidump.Read()); MinidumpException* exception = minidump.GetException(); MinidumpMemoryList* memory_list = minidump.GetMemoryList(); ASSERT_TRUE(exception); ASSERT_TRUE(memory_list); ASSERT_EQ((unsigned int)1, memory_list->region_count()); } static void *Junk(void *) { sleep(1000000); return NULL; } // Test that the memory list gets written correctly when multiple // threads are running. TEST_F(ExceptionHandlerTest, MemoryListMultipleThreads) { // Give the child process a pipe to report back on. int fds[2]; ASSERT_EQ(0, pipe(fds)); pid_t pid = fork(); if (pid == 0) { close(fds[0]); ExceptionHandler eh(tempDir.path(), NULL, MDCallback, &fds[1], true, NULL); // Run an extra thread so >2 memory regions will be written. pthread_t junk_thread; if (pthread_create(&junk_thread, NULL, Junk, NULL) == 0) pthread_detach(junk_thread); // Just crash. Crasher(); // not reached exit(1); } // In the parent process. ASSERT_NE(-1, pid); close(fds[1]); // Wait for the background process to return the minidump file. close(fds[1]); char minidump_file[PATH_MAX]; ssize_t nbytes = read(fds[0], minidump_file, sizeof(minidump_file)); ASSERT_NE(0, nbytes); // Ensure that minidump file exists and is > 0 bytes. struct stat st; ASSERT_EQ(0, stat(minidump_file, &st)); ASSERT_LT(0, st.st_size); // Child process should have exited with a zero status. int ret; ASSERT_EQ(pid, waitpid(pid, &ret, 0)); EXPECT_NE(0, WIFEXITED(ret)); EXPECT_EQ(0, WEXITSTATUS(ret)); // Read the minidump, and verify that the memory list can be read. Minidump minidump(minidump_file); ASSERT_TRUE(minidump.Read()); MinidumpMemoryList* memory_list = minidump.GetMemoryList(); ASSERT_TRUE(memory_list); // Verify that there are three memory regions: // one per thread, and one for the instruction pointer memory. ASSERT_EQ((unsigned int)3, memory_list->region_count()); } }