diff --git a/src/client/linux/handler/exception_handler.cc b/src/client/linux/handler/exception_handler.cc index 592c9e5b..4c7436ea 100644 --- a/src/client/linux/handler/exception_handler.cc +++ b/src/client/linux/handler/exception_handler.cc @@ -88,10 +88,12 @@ #include #include +#include #include #include "common/linux/linux_libc_support.h" #include "common/memory.h" +#include "client/linux/minidump_writer/linux_dumper.h" #include "client/linux/minidump_writer/minidump_writer.h" #include "common/linux/guid_creator.h" #include "common/linux/eintr_wrapper.h" @@ -449,8 +451,11 @@ void ExceptionHandler::WaitForContinueSignal() { // Runs on the cloned process. bool ExceptionHandler::DoDump(pid_t crashing_process, const void* context, size_t context_size) { - return google_breakpad::WriteMinidump( - next_minidump_path_c_, crashing_process, context, context_size); + return google_breakpad::WriteMinidump(next_minidump_path_c_, + crashing_process, + context, + context_size, + mapping_list_); } // static @@ -482,4 +487,21 @@ bool ExceptionHandler::WriteMinidump() { #endif // !defined(__ARM_EABI__) } +void ExceptionHandler::AddMappingInfo(const std::string& name, + const u_int8_t identifier[sizeof(MDGUID)], + uintptr_t start_address, + size_t mapping_size, + size_t file_offset) { + MappingInfo info; + info.start_addr = start_address; + info.size = mapping_size; + info.offset = file_offset; + strncpy(info.name, name.c_str(), std::min(name.size(), sizeof(info))); + + MappingEntry mapping; + mapping.first = info; + memcpy(mapping.second, identifier, sizeof(MDGUID)); + mapping_list_.push_back(mapping); +} + } // namespace google_breakpad diff --git a/src/client/linux/handler/exception_handler.h b/src/client/linux/handler/exception_handler.h index 82f7fb4c..e75517ea 100644 --- a/src/client/linux/handler/exception_handler.h +++ b/src/client/linux/handler/exception_handler.h @@ -30,17 +30,20 @@ #ifndef CLIENT_LINUX_HANDLER_EXCEPTION_HANDLER_H_ #define CLIENT_LINUX_HANDLER_EXCEPTION_HANDLER_H_ -#include #include +#include #include #include +#include #include #if defined(__ANDROID__) #include "client/linux/android_ucontext.h" #endif #include "client/linux/crash_generation/crash_generation_client.h" +#include "client/linux/minidump_writer/minidump_writer.h" +#include "google_breakpad/common/minidump_format.h" #include "processor/scoped_ptr.h" struct sigaction; @@ -181,6 +184,15 @@ class ExceptionHandler { return crash_generation_client_.get() != NULL; } + // Add information about a memory mapping. This can be used if + // a custom library loader is used that maps things in a way + // that the linux dumper can't handle by reading the maps file. + void AddMappingInfo(const std::string& name, + const u_int8_t identifier[sizeof(MDGUID)], + uintptr_t start_address, + size_t mapping_size, + size_t file_offset); + private: void Init(const std::string &dump_path, const int server_fd); @@ -236,6 +248,10 @@ class ExceptionHandler { // cloned process after creating it, until we have explicitly enabled // ptrace. This is used to store the file descriptors for the pipe int fdes[2]; + + // Callers can add extra info about mappings for cases where the + // dumper code cannot extract enough information from /proc//maps. + MappingList mapping_list_; }; } // namespace google_breakpad diff --git a/src/client/linux/handler/exception_handler_unittest.cc b/src/client/linux/handler/exception_handler_unittest.cc index 988de063..67c05f6b 100644 --- a/src/client/linux/handler/exception_handler_unittest.cc +++ b/src/client/linux/handler/exception_handler_unittest.cc @@ -42,6 +42,7 @@ #include "client/linux/handler/exception_handler.h" #include "client/linux/minidump_writer/minidump_writer.h" #include "common/linux/eintr_wrapper.h" +#include "common/linux/file_id.h" #include "common/linux/linux_libc_support.h" #include "third_party/lss/linux_syscall_support.h" #include "google_breakpad/processor/minidump.h" @@ -54,6 +55,10 @@ using namespace google_breakpad; #define TEMPDIR "/data/local/tmp" #endif +// Length of a formatted GUID string = +// sizeof(MDGUID) * 2 + 4 (for dashes) + 1 (null terminator) +const int kGUIDStringSize = 37; + static void sigchld_handler(int signo) { } class ExceptionHandlerTest : public ::testing::Test { @@ -573,6 +578,87 @@ TEST(ExceptionHandlerTest, InstructionPointerMemoryNullPointer) { free(filename); } +static bool SimpleCallback(const char* dump_path, + const char* minidump_id, + void* context, + bool succeeded) { + if (!succeeded) + return succeeded; + + string* minidump_file = reinterpret_cast(context); + minidump_file->append(dump_path); + minidump_file->append("/"); + minidump_file->append(minidump_id); + minidump_file->append(".dmp"); + return true; +} + +// Test that anonymous memory maps can be annotated with names and IDs. +TEST(ExceptionHandlerTest, ModuleInfo) { + // These are defined here so the parent can use them to check the + // data from the minidump afterwards. + const u_int32_t kMemorySize = sysconf(_SC_PAGESIZE); + const char* kMemoryName = "a fake module"; + const u_int8_t kModuleGUID[sizeof(MDGUID)] = { + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, + 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF + }; + char module_identifier_buffer[kGUIDStringSize]; + FileID::ConvertIdentifierToString(kModuleGUID, + module_identifier_buffer, + sizeof(module_identifier_buffer)); + string module_identifier(module_identifier_buffer); + // Strip out dashes + size_t pos; + while ((pos = module_identifier.find('-')) != string::npos) { + module_identifier.erase(pos, 1); + } + // And append a zero, because module IDs include an "age" field + // which is always zero on Linux. + module_identifier += "0"; + + // Get some memory. + char* memory = + reinterpret_cast(mmap(NULL, + kMemorySize, + PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANON, + -1, + 0)); + const u_int64_t kMemoryAddress = reinterpret_cast(memory); + ASSERT_TRUE(memory); + + string minidump_filename; + ExceptionHandler handler(TEMPDIR, NULL, SimpleCallback, + (void*)&minidump_filename, true); + // Add info about the anonymous memory mapping. + handler.AddMappingInfo(kMemoryName, + kModuleGUID, + kMemoryAddress, + kMemorySize, + 0); + handler.WriteMinidump(); + + // Read the minidump. Load the module list, and ensure that + // the mmap'ed |memory| is listed with the given module name + // and debug ID. + Minidump minidump(minidump_filename); + ASSERT_TRUE(minidump.Read()); + + MinidumpModuleList* module_list = minidump.GetModuleList(); + ASSERT_TRUE(module_list); + const MinidumpModule* module = + module_list->GetModuleForAddress(kMemoryAddress); + ASSERT_TRUE(module); + + EXPECT_EQ(kMemoryAddress, module->base_address()); + EXPECT_EQ(kMemorySize, module->size()); + EXPECT_EQ(kMemoryName, module->code_file()); + EXPECT_EQ(module_identifier, module->debug_identifier()); + + unlink(minidump_filename.c_str()); +} + static const unsigned kControlMsgSize = CMSG_SPACE(sizeof(int)) + CMSG_SPACE(sizeof(struct ucred)); diff --git a/src/client/linux/minidump_writer/linux_dumper.cc b/src/client/linux/minidump_writer/linux_dumper.cc index 6e6f4cfa..5187120f 100644 --- a/src/client/linux/minidump_writer/linux_dumper.cc +++ b/src/client/linux/minidump_writer/linux_dumper.cc @@ -104,12 +104,12 @@ static bool ResumeThread(pid_t pid) { } inline static bool IsMappedFileOpenUnsafe( - const google_breakpad::MappingInfo* mapping) { + const google_breakpad::MappingInfo& mapping) { // It is unsafe to attempt to open a mapped file that lives under /dev, // because the semantics of the open may be driver-specific so we'd risk // hanging the crash dumper. And a file in /dev/ almost certainly has no // ELF file identifier anyways. - return my_strncmp(mapping->name, + return my_strncmp(mapping.name, kMappedFileUnsafePrefix, sizeof(kMappedFileUnsafePrefix) - 1) == 0; } @@ -203,21 +203,21 @@ LinuxDumper::BuildProcPath(char* path, pid_t pid, const char* node) const { } bool -LinuxDumper::ElfFileIdentifierForMapping(unsigned int mapping_id, +LinuxDumper::ElfFileIdentifierForMapping(const MappingInfo& mapping, + unsigned int mapping_id, uint8_t identifier[sizeof(MDGUID)]) { - assert(mapping_id < mappings_.size()); + assert(mapping_id == -1 || mapping_id < mappings_.size()); my_memset(identifier, 0, sizeof(MDGUID)); - MappingInfo* mapping = mappings_[mapping_id]; if (IsMappedFileOpenUnsafe(mapping)) return false; char filename[NAME_MAX]; - size_t filename_len = my_strlen(mapping->name); + size_t filename_len = my_strlen(mapping.name); assert(filename_len < NAME_MAX); if (filename_len >= NAME_MAX) return false; - memcpy(filename, mapping->name, filename_len); + memcpy(filename, mapping.name, filename_len); filename[filename_len] = '\0'; bool filename_modified = HandleDeletedFileInMapping(filename); @@ -239,8 +239,11 @@ LinuxDumper::ElfFileIdentifierForMapping(unsigned int mapping_id, bool success = FileID::ElfFileIdentifierFromMappedFile(base, identifier); sys_munmap(base, st.st_size); - if (success && filename_modified) - mapping->name[filename_len - sizeof(kDeletedSuffix) + 1] = '\0'; + if (success && mapping_id != -1 && filename_modified) { + mappings_[mapping_id]->name[filename_len - + sizeof(kDeletedSuffix) + 1] = '\0'; + } + return success; } @@ -310,7 +313,7 @@ LinuxDumper::EnumerateMappings(wasteful_vector* result) const { module->offset = offset; const char* name = NULL; // Only copy name if the name is a valid path name, or if - // we've found the VDSO image + // it's the VDSO image. if ((name = my_strchr(line, '/')) != NULL) { const unsigned l = my_strlen(name); if (l < sizeof(module->name)) @@ -500,7 +503,7 @@ const MappingInfo* LinuxDumper::FindMapping(const void* address) const { return NULL; } -bool LinuxDumper::HandleDeletedFileInMapping(char* path) { +bool LinuxDumper::HandleDeletedFileInMapping(char* path) const { static const size_t kDeletedSuffixLen = sizeof(kDeletedSuffix) - 1; // Check for ' (deleted)' in |path|. diff --git a/src/client/linux/minidump_writer/linux_dumper.h b/src/client/linux/minidump_writer/linux_dumper.h index 16b15f3c..2233ec05 100644 --- a/src/client/linux/minidump_writer/linux_dumper.h +++ b/src/client/linux/minidump_writer/linux_dumper.h @@ -151,8 +151,10 @@ class LinuxDumper { // without any slashes. void BuildProcPath(char* path, pid_t pid, const char* node) const; - // Generate a File ID from the .text section of a mapped entry - bool ElfFileIdentifierForMapping(unsigned int mapping_id, + // Generate a File ID from the .text section of a mapped entry. + // mapping_id may be -1 if this is not a member of mappings_. + bool ElfFileIdentifierForMapping(const MappingInfo& mapping, + unsigned int mapping_id, uint8_t identifier[sizeof(MDGUID)]); // Utility method to find the location of where the kernel has @@ -174,7 +176,7 @@ class LinuxDumper { // For programs that don't end with ' (deleted)', this is a no-op. // This assumes |path| is a buffer with length NAME_MAX. // Returns true if |path| is modified. - bool HandleDeletedFileInMapping(char* path); + bool HandleDeletedFileInMapping(char* path) const; const pid_t pid_; diff --git a/src/client/linux/minidump_writer/linux_dumper_unittest.cc b/src/client/linux/minidump_writer/linux_dumper_unittest.cc index b81291b6..a3e84dbc 100644 --- a/src/client/linux/minidump_writer/linux_dumper_unittest.cc +++ b/src/client/linux/minidump_writer/linux_dumper_unittest.cc @@ -32,26 +32,19 @@ #include #include #include +#include +#include #include #include "breakpad_googletest_includes.h" #include "client/linux/minidump_writer/linux_dumper.h" +#include "common/linux/eintr_wrapper.h" #include "common/linux/file_id.h" #include "common/memory.h" using std::string; using namespace google_breakpad; -// This provides a wrapper around system calls which may be -// interrupted by a signal and return EINTR. See man 7 signal. -#define HANDLE_EINTR(x) ({ \ - typeof(x) __eintr_result__; \ - do { \ - __eintr_result__ = x; \ - } while (__eintr_result__ == -1 && errno == EINTR); \ - __eintr_result__;\ -}) - namespace { typedef testing::Test LinuxDumperTest; } @@ -88,8 +81,14 @@ TEST(LinuxDumperTest, VerifyStackReadWithMultipleThreads) { 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]); + // Locate helper binary next to the current binary. char self_path[PATH_MAX]; if (readlink("/proc/self/exe", self_path, sizeof(self_path) - 1) == -1) { @@ -105,9 +104,12 @@ TEST(LinuxDumperTest, VerifyStackReadWithMultipleThreads) { helper_path.erase(pos + 1); helper_path += "linux_dumper_unittest_helper"; - // Set the number of threads + // 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. @@ -115,9 +117,21 @@ TEST(LinuxDumperTest, VerifyStackReadWithMultipleThreads) { FAIL() << "Exec of " << helper_path << " failed: " << strerror(errno); exit(0); } - // The sleep is flaky, but prevents us from reading - // the child process before all threads have been created. - sleep(1); + close(fds[1]); + // Wait for the child process to signal that it's ready. + 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; + read(fds[0], &junk, sizeof(junk)); + close(fds[0]); + + // Child is ready now. LinuxDumper dumper(child_pid); ASSERT_TRUE(dumper.Init()); EXPECT_EQ((size_t)kNumberOfThreadsInHelperProgram, dumper.threads().size()); @@ -240,7 +254,8 @@ TEST(LinuxDumperTest, FileIDsMatch) { uint8_t identifier1[sizeof(MDGUID)]; uint8_t identifier2[sizeof(MDGUID)]; - EXPECT_TRUE(dumper.ElfFileIdentifierForMapping(i, identifier1)); + EXPECT_TRUE(dumper.ElfFileIdentifierForMapping(*mappings[i], i, + identifier1)); FileID fileid(exe_name); EXPECT_TRUE(fileid.ElfFileIdentifier(identifier2)); char identifier_string1[37]; diff --git a/src/client/linux/minidump_writer/linux_dumper_unittest_helper.cc b/src/client/linux/minidump_writer/linux_dumper_unittest_helper.cc index 6b7ad5a2..27c2e994 100644 --- a/src/client/linux/minidump_writer/linux_dumper_unittest_helper.cc +++ b/src/client/linux/minidump_writer/linux_dumper_unittest_helper.cc @@ -32,6 +32,7 @@ // id. #include +#include #include #include #include @@ -58,7 +59,13 @@ void *thread_function(void *data) { } int main(int argc, char *argv[]) { - int num_threads = atoi(argv[1]); + if (argc < 2) { + fprintf(stderr, + "usage: linux_dumper_unittest_helper <# of threads\n"); + return 1; + } + int pipefd = atoi(argv[1]); + int num_threads = atoi(argv[2]); if (num_threads < 1) { fprintf(stderr, "ERROR: number of threads is 0"); return 1; @@ -70,6 +77,9 @@ int main(int argc, char *argv[]) { for (int i = 1; i < num_threads; i++) { pthread_create(&threads[i], &thread_attributes, &thread_function, NULL); } + // Signal parent that this process has started all threads. + uint8_t byte = 1; + write(pipefd, &byte, sizeof(byte)); thread_function(NULL); return 0; } diff --git a/src/client/linux/minidump_writer/minidump_writer.cc b/src/client/linux/minidump_writer/minidump_writer.cc index 405ec4e8..3eb90953 100644 --- a/src/client/linux/minidump_writer/minidump_writer.cc +++ b/src/client/linux/minidump_writer/minidump_writer.cc @@ -368,7 +368,8 @@ class MinidumpWriter { public: MinidumpWriter(const char* filename, pid_t crashing_pid, - const ExceptionHandler::CrashContext* context) + const ExceptionHandler::CrashContext* context, + const MappingList& mappings) : filename_(filename), siginfo_(&context->siginfo), ucontext_(&context->context), @@ -380,7 +381,8 @@ class MinidumpWriter { #endif crashing_tid_(context->tid), dumper_(crashing_pid), - memory_blocks_(dumper_.allocator()) { + memory_blocks_(dumper_.allocator()), + mapping_list_(mappings) { } bool Init() { @@ -752,17 +754,34 @@ class MinidumpWriter { return true; } + // If there is caller-provided information about this mapping + // in the mapping_list_ list, return true. Otherwise, return false. + bool HaveMappingInfo(const MappingInfo& mapping) { + for (MappingList::const_iterator iter = mapping_list_.begin(); + iter != mapping_list_.end(); + ++iter) { + // Ignore any mappings that are wholly contained within + // mappings in the mapping_info_ list. + if (mapping.start_addr >= iter->first.start_addr && + (mapping.start_addr + mapping.size) <= + (iter->first.start_addr + iter->first.size)) { + return true; + } + } + return false; + } + // Write information about the mappings in effect. Because we are using the // minidump format, the information about the mappings is pretty limited. // Because of this, we also include the full, unparsed, /proc/$x/maps file in // another stream in the file. bool WriteMappings(MDRawDirectory* dirent) { const unsigned num_mappings = dumper_.mappings().size(); - unsigned num_output_mappings = 0; + unsigned num_output_mappings = mapping_list_.size(); for (unsigned i = 0; i < dumper_.mappings().size(); ++i) { const MappingInfo& mapping = *dumper_.mappings()[i]; - if (ShouldIncludeMapping(mapping)) + if (ShouldIncludeMapping(mapping) && !HaveMappingInfo(mapping)) num_output_mappings++; } @@ -774,59 +793,89 @@ class MinidumpWriter { dirent->location = list.location(); *list.get() = num_output_mappings; - for (unsigned i = 0, j = 0; i < num_mappings; ++i) { + // First write all the mappings from the dumper + unsigned int j = 0; + for (unsigned i = 0; i < num_mappings; ++i) { const MappingInfo& mapping = *dumper_.mappings()[i]; - if (!ShouldIncludeMapping(mapping)) + if (!ShouldIncludeMapping(mapping) || HaveMappingInfo(mapping)) continue; MDRawModule mod; - my_memset(&mod, 0, MD_MODULE_SIZE); - mod.base_of_image = mapping.start_addr; - mod.size_of_image = mapping.size; - const size_t filepath_len = my_strlen(mapping.name); - - // Figure out file name from path - const char* filename_ptr = mapping.name + filepath_len - 1; - while (filename_ptr >= mapping.name) { - if (*filename_ptr == '/') - break; - filename_ptr--; - } - filename_ptr++; - const size_t filename_len = mapping.name + filepath_len - filename_ptr; - - uint8_t cv_buf[MDCVInfoPDB70_minsize + NAME_MAX]; - uint8_t* cv_ptr = cv_buf; - UntypedMDRVA cv(&minidump_writer_); - if (!cv.Allocate(MDCVInfoPDB70_minsize + filename_len + 1)) + if (!FillRawModule(mapping, i, mod, NULL)) return false; - - const uint32_t cv_signature = MD_CVINFOPDB70_SIGNATURE; - memcpy(cv_ptr, &cv_signature, sizeof(cv_signature)); - cv_ptr += sizeof(cv_signature); - uint8_t* signature = cv_ptr; - cv_ptr += sizeof(MDGUID); - dumper_.ElfFileIdentifierForMapping(i, signature); - my_memset(cv_ptr, 0, sizeof(uint32_t)); // Set age to 0 on Linux. - cv_ptr += sizeof(uint32_t); - - // Write pdb_file_name - memcpy(cv_ptr, filename_ptr, filename_len + 1); - cv.Copy(cv_buf, MDCVInfoPDB70_minsize + filename_len + 1); - - mod.cv_record = cv.location(); - - MDLocationDescriptor ld; - if (!minidump_writer_.WriteString(mapping.name, filepath_len, &ld)) + list.CopyIndexAfterObject(j++, &mod, MD_MODULE_SIZE); + } + // Next write all the mappings provided by the caller + for (MappingList::const_iterator iter = mapping_list_.begin(); + iter != mapping_list_.end(); + ++iter) { + MDRawModule mod; + if (!FillRawModule(iter->first, -1, mod, iter->second)) return false; - mod.module_name_rva = ld.rva; - list.CopyIndexAfterObject(j++, &mod, MD_MODULE_SIZE); } return true; } + // Fill the MDRawModule |mod| with information about the provided + // |mapping|. If |identifier| is non-NULL, use it instead of calculating + // a file ID from the mapping. |mapping_id| can be -1 if this mapping + // is not from the LinuxDumper. + bool FillRawModule(const MappingInfo& mapping, + unsigned int mapping_id, + MDRawModule& mod, + const u_int8_t* identifier) { + my_memset(&mod, 0, MD_MODULE_SIZE); + + mod.base_of_image = mapping.start_addr; + mod.size_of_image = mapping.size; + const size_t filepath_len = my_strlen(mapping.name); + + // Figure out file name from path + const char* filename_ptr = mapping.name + filepath_len - 1; + while (filename_ptr >= mapping.name) { + if (*filename_ptr == '/') + break; + filename_ptr--; + } + filename_ptr++; + + const size_t filename_len = mapping.name + filepath_len - filename_ptr; + + uint8_t cv_buf[MDCVInfoPDB70_minsize + NAME_MAX]; + uint8_t* cv_ptr = cv_buf; + UntypedMDRVA cv(&minidump_writer_); + if (!cv.Allocate(MDCVInfoPDB70_minsize + filename_len + 1)) + return false; + + const uint32_t cv_signature = MD_CVINFOPDB70_SIGNATURE; + memcpy(cv_ptr, &cv_signature, sizeof(cv_signature)); + cv_ptr += sizeof(cv_signature); + uint8_t* signature = cv_ptr; + cv_ptr += sizeof(MDGUID); + if (identifier) { + // GUID was provided by caller. + memcpy(signature, identifier, sizeof(MDGUID)); + } else { + dumper_.ElfFileIdentifierForMapping(mapping, mapping_id, signature); + } + my_memset(cv_ptr, 0, sizeof(uint32_t)); // Set age to 0 on Linux. + cv_ptr += sizeof(uint32_t); + + // Write pdb_file_name + memcpy(cv_ptr, filename_ptr, filename_len + 1); + cv.Copy(cv_buf, MDCVInfoPDB70_minsize + filename_len + 1); + + mod.cv_record = cv.location(); + + MDLocationDescriptor ld; + if (!minidump_writer_.WriteString(mapping.name, filepath_len, &ld)) + return false; + mod.module_name_rva = ld.rva; + return true; + } + bool WriteMemoryListStream(MDRawDirectory* dirent) { TypedMDRVA list(&minidump_writer_); if (!list.AllocateObjectAndArray(memory_blocks_.size(), @@ -1233,15 +1282,24 @@ class MinidumpWriter { // written while writing the thread list stream, but saved here // so a memory list stream can be written afterwards. wasteful_vector memory_blocks_; + // Additional information about some mappings provided by the caller. + const MappingList& mapping_list_; }; bool WriteMinidump(const char* filename, pid_t crashing_process, const void* blob, size_t blob_size) { + MappingList m; + return WriteMinidump(filename, crashing_process, blob, blob_size, m); +} + +bool WriteMinidump(const char* filename, pid_t crashing_process, + const void* blob, size_t blob_size, + const MappingList& mappings) { if (blob_size != sizeof(ExceptionHandler::CrashContext)) return false; const ExceptionHandler::CrashContext* context = reinterpret_cast(blob); - MinidumpWriter writer(filename, crashing_process, context); + MinidumpWriter writer(filename, crashing_process, context, mappings); if (!writer.Init()) return false; return writer.Dump(); diff --git a/src/client/linux/minidump_writer/minidump_writer.h b/src/client/linux/minidump_writer/minidump_writer.h index 579f68cd..156ca35f 100644 --- a/src/client/linux/minidump_writer/minidump_writer.h +++ b/src/client/linux/minidump_writer/minidump_writer.h @@ -33,8 +33,17 @@ #include #include +#include +#include + +#include "google_breakpad/common/minidump_format.h" + namespace google_breakpad { +// A list of +typedef std::pair MappingEntry; +typedef std::list MappingList; + // Write a minidump to the filesystem. This function does not malloc nor use // libc functions which may. Thus, it can be used in contexts where the state // of the heap may be corrupt. @@ -48,6 +57,11 @@ namespace google_breakpad { bool WriteMinidump(const char* filename, pid_t crashing_process, const void* blob, size_t blob_size); +// This overload also allows passing a list of known mappings. +bool WriteMinidump(const char* filename, pid_t crashing_process, + const void* blob, size_t blob_size, + const MappingList& mappings); + } // namespace google_breakpad #endif // CLIENT_LINUX_MINIDUMP_WRITER_MINIDUMP_WRITER_H_ diff --git a/src/client/linux/minidump_writer/minidump_writer_unittest.cc b/src/client/linux/minidump_writer/minidump_writer_unittest.cc index 0a3b46f2..5377aaca 100644 --- a/src/client/linux/minidump_writer/minidump_writer_unittest.cc +++ b/src/client/linux/minidump_writer/minidump_writer_unittest.cc @@ -28,12 +28,17 @@ // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include +#include +#include #include +#include "breakpad_googletest_includes.h" #include "client/linux/handler/exception_handler.h" +#include "client/linux/minidump_writer/linux_dumper.h" #include "client/linux/minidump_writer/minidump_writer.h" #include "common/linux/eintr_wrapper.h" -#include "breakpad_googletest_includes.h" +#include "common/linux/file_id.h" +#include "google_breakpad/processor/minidump.h" using namespace google_breakpad; @@ -43,6 +48,10 @@ using namespace google_breakpad; #define TEMPDIR "/data/local/tmp" #endif +// Length of a formatted GUID string = +// sizeof(MDGUID) * 2 + 4 (for dashes) + 1 (null terminator) +const int kGUIDStringSize = 37; + namespace { typedef testing::Test MinidumpWriterTest; } @@ -76,3 +85,307 @@ TEST(MinidumpWriterTest, Setup) { close(fds[1]); } + +// Test that mapping info can be specified when writing a minidump, +// and that it ends up in the module list of the minidump. +TEST(MinidumpWriterTest, MappingInfo) { + int fds[2]; + ASSERT_NE(-1, pipe(fds)); + + // These are defined here so the parent can use them to check the + // data from the minidump afterwards. + const u_int32_t kMemorySize = sysconf(_SC_PAGESIZE); + const char* kMemoryName = "a fake module"; + const u_int8_t kModuleGUID[sizeof(MDGUID)] = { + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, + 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF + }; + char module_identifier_buffer[kGUIDStringSize]; + FileID::ConvertIdentifierToString(kModuleGUID, + module_identifier_buffer, + sizeof(module_identifier_buffer)); + string module_identifier(module_identifier_buffer); + // Strip out dashes + size_t pos; + while ((pos = module_identifier.find('-')) != string::npos) { + module_identifier.erase(pos, 1); + } + // And append a zero, because module IDs include an "age" field + // which is always zero on Linux. + module_identifier += "0"; + + // Get some memory. + char* memory = + reinterpret_cast(mmap(NULL, + kMemorySize, + PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANON, + -1, + 0)); + const u_int64_t kMemoryAddress = reinterpret_cast(memory); + ASSERT_TRUE(memory); + + const pid_t child = fork(); + if (child == 0) { + close(fds[1]); + char b; + HANDLE_EINTR(read(fds[0], &b, sizeof(b))); + close(fds[0]); + syscall(__NR_exit); + } + close(fds[0]); + + ExceptionHandler::CrashContext context; + memset(&context, 0, sizeof(context)); + context.tid = 1; + + char templ[] = TEMPDIR "/minidump-writer-unittest-XXXXXX"; + mktemp(templ); + + // Add information about the mapped memory. + MappingInfo info; + info.start_addr = kMemoryAddress; + info.size = kMemorySize; + info.offset = 0; + strcpy(info.name, kMemoryName); + + MappingList mappings; + MappingEntry mapping; + mapping.first = info; + memcpy(mapping.second, kModuleGUID, sizeof(MDGUID)); + mappings.push_back(mapping); + ASSERT_TRUE(WriteMinidump(templ, child, &context, sizeof(context), mappings)); + + // Read the minidump. Load the module list, and ensure that + // the mmap'ed |memory| is listed with the given module name + // and debug ID. + Minidump minidump(templ); + ASSERT_TRUE(minidump.Read()); + + MinidumpModuleList* module_list = minidump.GetModuleList(); + ASSERT_TRUE(module_list); + const MinidumpModule* module = + module_list->GetModuleForAddress(kMemoryAddress); + ASSERT_TRUE(module); + + EXPECT_EQ(kMemoryAddress, module->base_address()); + EXPECT_EQ(kMemorySize, module->size()); + EXPECT_EQ(kMemoryName, module->code_file()); + EXPECT_EQ(module_identifier, module->debug_identifier()); + + unlink(templ); + close(fds[1]); +} + +// Test that mapping info can be specified, and that it overrides +// existing mappings that are wholly contained within the specified +// range. +TEST(MinidumpWriterTest, MappingInfoContained) { + int fds[2]; + ASSERT_NE(-1, pipe(fds)); + + // These are defined here so the parent can use them to check the + // data from the minidump afterwards. + const u_int32_t kMemorySize = sysconf(_SC_PAGESIZE); + const char* kMemoryName = "a fake module"; + const u_int8_t kModuleGUID[sizeof(MDGUID)] = { + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, + 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF + }; + char module_identifier_buffer[kGUIDStringSize]; + FileID::ConvertIdentifierToString(kModuleGUID, + module_identifier_buffer, + sizeof(module_identifier_buffer)); + string module_identifier(module_identifier_buffer); + // Strip out dashes + size_t pos; + while ((pos = module_identifier.find('-')) != string::npos) { + module_identifier.erase(pos, 1); + } + // And append a zero, because module IDs include an "age" field + // which is always zero on Linux. + module_identifier += "0"; + + // mmap a file + char tempfile[] = TEMPDIR "/minidump-writer-unittest-temp-XXXXXX"; + mktemp(tempfile); + int fd = open(tempfile, O_RDWR | O_CREAT, 0); + ASSERT_NE(-1, fd); + unlink(tempfile); + // fill with zeros + char buffer[kMemorySize]; + memset(buffer, 0, kMemorySize); + ASSERT_EQ(kMemorySize, write(fd, buffer, kMemorySize)); + lseek(fd, 0, SEEK_SET); + + char* memory = + reinterpret_cast(mmap(NULL, + kMemorySize, + PROT_READ | PROT_WRITE, + MAP_PRIVATE, + fd, + 0)); + const u_int64_t kMemoryAddress = reinterpret_cast(memory); + ASSERT_TRUE(memory); + close(fd); + + const pid_t child = fork(); + if (child == 0) { + close(fds[1]); + char b; + HANDLE_EINTR(read(fds[0], &b, sizeof(b))); + close(fds[0]); + syscall(__NR_exit); + } + close(fds[0]); + + ExceptionHandler::CrashContext context; + memset(&context, 0, sizeof(context)); + context.tid = 1; + + char dumpfile[] = TEMPDIR "/minidump-writer-unittest-XXXXXX"; + mktemp(dumpfile); + + // Add information about the mapped memory. Report it as being larger than + // it actually is. + MappingInfo info; + info.start_addr = kMemoryAddress - kMemorySize; + info.size = kMemorySize * 3; + info.offset = 0; + strcpy(info.name, kMemoryName); + + MappingList mappings; + MappingEntry mapping; + mapping.first = info; + memcpy(mapping.second, kModuleGUID, sizeof(MDGUID)); + mappings.push_back(mapping); + ASSERT_TRUE(WriteMinidump(dumpfile, child, &context, sizeof(context), mappings)); + + // Read the minidump. Load the module list, and ensure that + // the mmap'ed |memory| is listed with the given module name + // and debug ID. + Minidump minidump(dumpfile); + ASSERT_TRUE(minidump.Read()); + + MinidumpModuleList* module_list = minidump.GetModuleList(); + ASSERT_TRUE(module_list); + const MinidumpModule* module = + module_list->GetModuleForAddress(kMemoryAddress); + ASSERT_TRUE(module); + + EXPECT_EQ(info.start_addr, module->base_address()); + EXPECT_EQ(info.size, module->size()); + EXPECT_EQ(kMemoryName, module->code_file()); + EXPECT_EQ(module_identifier, module->debug_identifier()); + + unlink(dumpfile); + close(fds[1]); +} + +TEST(MinidumpWriterTest, DeletedBinary) { + static const int kNumberOfThreadsInHelperProgram = 1; + char kNumberOfThreadsArgument[2]; + sprintf(kNumberOfThreadsArgument, "%d", kNumberOfThreadsInHelperProgram); + + // Locate helper binary next to the current binary. + char self_path[PATH_MAX]; + if (readlink("/proc/self/exe", self_path, sizeof(self_path) - 1) == -1) { + FAIL() << "readlink failed: " << strerror(errno); + exit(1); + } + string helper_path(self_path); + size_t pos = helper_path.rfind('/'); + if (pos == string::npos) { + FAIL() << "no trailing slash in path: " << helper_path; + exit(1); + } + helper_path.erase(pos + 1); + helper_path += "linux_dumper_unittest_helper"; + + // Copy binary to a temp file. + char binpath[] = TEMPDIR "/linux-dumper-unittest-helper-XXXXXX"; + mktemp(binpath); + char cmdline[2 * PATH_MAX]; + sprintf(cmdline, "/bin/cp \"%s\" \"%s\"", helper_path.c_str(), binpath); + ASSERT_EQ(0, system(cmdline)); + ASSERT_EQ(0, chmod(binpath, 0755)); + + int fds[2]; + ASSERT_NE(-1, pipe(fds)); + + pid_t child_pid = fork(); + if (child_pid == 0) { + // In child process. + close(fds[0]); + + // Pass the pipe fd and the number of threads as arguments. + char pipe_fd_string[8]; + sprintf(pipe_fd_string, "%d", fds[1]); + execl(binpath, + binpath, + pipe_fd_string, + kNumberOfThreadsArgument, + NULL); + } + close(fds[1]); + // Wait for the child process to signal that it's ready. + 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; + read(fds[0], &junk, sizeof(junk)); + close(fds[0]); + + // Child is ready now. + // Unlink the test binary. + unlink(binpath); + + ExceptionHandler::CrashContext context; + memset(&context, 0, sizeof(context)); + + char templ[] = TEMPDIR "/minidump-writer-unittest-XXXXXX"; + mktemp(templ); + // Set a non-zero tid to avoid tripping asserts. + context.tid = 1; + ASSERT_TRUE(WriteMinidump(templ, child_pid, &context, sizeof(context))); + kill(child_pid, SIGKILL); + + struct stat st; + ASSERT_EQ(stat(templ, &st), 0); + ASSERT_GT(st.st_size, 0u); + + + + Minidump minidump(templ); + ASSERT_TRUE(minidump.Read()); + + // Check that the main module filename is correct. + MinidumpModuleList* module_list = minidump.GetModuleList(); + ASSERT_TRUE(module_list); + const MinidumpModule* module = module_list->GetMainModule(); + EXPECT_STREQ(binpath, module->code_file().c_str()); + // Check that the file ID is correct. + FileID fileid(helper_path.c_str()); + uint8_t identifier[sizeof(MDGUID)]; + EXPECT_TRUE(fileid.ElfFileIdentifier(identifier)); + char identifier_string[kGUIDStringSize]; + FileID::ConvertIdentifierToString(identifier, + identifier_string, + kGUIDStringSize); + string module_identifier(identifier_string); + // Strip out dashes + while ((pos = module_identifier.find('-')) != string::npos) { + module_identifier.erase(pos, 1); + } + // And append a zero, because module IDs include an "age" field + // which is always zero on Linux. + module_identifier += "0"; + EXPECT_EQ(module_identifier, module->debug_identifier()); + + unlink(templ); +}