diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /toolkit/crashreporter/minidump-analyzer | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/crashreporter/minidump-analyzer')
9 files changed, 1237 insertions, 0 deletions
diff --git a/toolkit/crashreporter/minidump-analyzer/MinidumpAnalyzerUtils.h b/toolkit/crashreporter/minidump-analyzer/MinidumpAnalyzerUtils.h new file mode 100644 index 0000000000..2fb8319be8 --- /dev/null +++ b/toolkit/crashreporter/minidump-analyzer/MinidumpAnalyzerUtils.h @@ -0,0 +1,115 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef MinidumpAnalyzerUtils_h +#define MinidumpAnalyzerUtils_h + +#ifdef XP_WIN +# include <windows.h> +#endif + +#include <algorithm> +#include <map> +#include <memory> +#include <string> + +namespace CrashReporter { + +struct MinidumpAnalyzerOptions { + bool fullMinidump; + std::string forceUseModule; +}; + +extern MinidumpAnalyzerOptions gMinidumpAnalyzerOptions; + +#ifdef XP_WIN + +static inline std::string WideToMBCP(const std::wstring& wide, unsigned int cp, + bool* success = nullptr) { + int bufferCharLen = WideCharToMultiByte(cp, 0, wide.c_str(), wide.length(), + nullptr, 0, nullptr, nullptr); + if (!bufferCharLen) { + if (success) { + *success = false; + } + + return ""; + } + + auto buffer = std::make_unique<char[]>(bufferCharLen); + if (!buffer) { + if (success) { + *success = false; + } + + return ""; + } + + int result = + WideCharToMultiByte(cp, 0, wide.c_str(), wide.length(), buffer.get(), + bufferCharLen, nullptr, nullptr); + if (success) { + *success = result > 0; + } + + return std::string(buffer.get(), result); +} + +static inline std::wstring MBCPToWide(const std::string& aMbStr, + unsigned int aCodepage, + bool* aSuccess = nullptr) { + int bufferCharLen = MultiByteToWideChar(aCodepage, 0, aMbStr.c_str(), + aMbStr.length(), nullptr, 0); + if (!bufferCharLen) { + if (aSuccess) { + *aSuccess = false; + } + + return L""; + } + + auto buffer = std::make_unique<wchar_t[]>(bufferCharLen); + if (!buffer) { + if (aSuccess) { + *aSuccess = false; + } + + return L""; + } + + int result = + MultiByteToWideChar(aCodepage, 0, aMbStr.c_str(), aMbStr.length(), + buffer.get(), bufferCharLen); + if (aSuccess) { + *aSuccess = result > 0; + } + + return std::wstring(buffer.get(), result); +} + +static inline std::string WideToUTF8(const std::wstring& aWide, + bool* aSuccess = nullptr) { + return WideToMBCP(aWide, CP_UTF8, aSuccess); +} + +static inline std::wstring UTF8ToWide(const std::string& aUtf8Str, + bool* aSuccess = nullptr) { + return MBCPToWide(aUtf8Str, CP_UTF8, aSuccess); +} + +static inline std::string WideToMBCS(const std::wstring& aWide, + bool* aSuccess = nullptr) { + return WideToMBCP(aWide, CP_ACP, aSuccess); +} + +static inline std::string UTF8ToMBCS(const std::string& aUtf8) { + return WideToMBCS(UTF8ToWide(aUtf8)); +} + +#endif // XP_WIN + +} // namespace CrashReporter + +#endif // MinidumpAnalyzerUtils_h diff --git a/toolkit/crashreporter/minidump-analyzer/MozStackFrameSymbolizer.cpp b/toolkit/crashreporter/minidump-analyzer/MozStackFrameSymbolizer.cpp new file mode 100644 index 0000000000..dbc4183429 --- /dev/null +++ b/toolkit/crashreporter/minidump-analyzer/MozStackFrameSymbolizer.cpp @@ -0,0 +1,117 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#if XP_WIN && HAVE_64BIT_BUILD + +# include "MozStackFrameSymbolizer.h" + +# include "MinidumpAnalyzerUtils.h" + +# include "processor/cfi_frame_info.h" + +# include <iostream> +# include <sstream> +# include <fstream> + +namespace CrashReporter { + +extern MinidumpAnalyzerOptions gMinidumpAnalyzerOptions; + +using google_breakpad::CFIFrameInfo; + +MozStackFrameSymbolizer::MozStackFrameSymbolizer() + : StackFrameSymbolizer(nullptr, nullptr) {} + +MozStackFrameSymbolizer::SymbolizerResult +MozStackFrameSymbolizer::FillSourceLineInfo(const CodeModules* modules, + const CodeModules* unloaded_modules, + const SystemInfo* system_info, + StackFrame* stack_frame) { + SymbolizerResult ret = StackFrameSymbolizer::FillSourceLineInfo( + modules, unloaded_modules, system_info, stack_frame); + + if (ret == kNoError && this->HasImplementation() && + stack_frame->function_name.empty()) { + // Breakpad's Stackwalker::InstructionAddressSeemsValid only considers an + // address valid if it has associated symbols. + // + // This makes sense for complete & accurate symbols, but ours may be + // incomplete or wrong. Returning a function name tells Breakpad we + // recognize this address as code, so it's OK to use in stack scanning. + // This function is only called with addresses that land in this module. + // + // This allows us to fall back to stack scanning in the case where we were + // unable to provide CFI. + stack_frame->function_name = "<unknown code>"; + } + return ret; +} + +CFIFrameInfo* MozStackFrameSymbolizer::FindCFIFrameInfo( + const StackFrame* frame) { + std::string modulePath; + + // For unit testing, support loading a specified module instead of + // the real one. + bool moduleHasBeenReplaced = false; + if (gMinidumpAnalyzerOptions.forceUseModule.size() > 0) { + modulePath = gMinidumpAnalyzerOptions.forceUseModule; + moduleHasBeenReplaced = true; + } else { + if (!frame->module) { + return nullptr; + } + modulePath = frame->module->code_file(); + } + + // Get/create the unwind parser. + auto itMod = mModuleMap.find(modulePath); + std::shared_ptr<ModuleUnwindParser> unwindParser; + if (itMod != mModuleMap.end()) { + unwindParser = itMod->second; + } else { + unwindParser.reset(new ModuleUnwindParser(modulePath)); + mModuleMap[modulePath] = unwindParser; + } + + UnwindCFI cfi; + DWORD offsetAddr; + + if (moduleHasBeenReplaced) { + // If we are replacing a module, addresses will never line up. + // So just act like the 1st entry is correct. + offsetAddr = unwindParser->GetAnyOffsetAddr(); + } else { + offsetAddr = frame->instruction - frame->module->base_address(); + } + + if (!unwindParser->GetCFI(offsetAddr, cfi)) { + return nullptr; + } + + std::unique_ptr<CFIFrameInfo> rules(new CFIFrameInfo()); + + static const size_t exprSize = 50; + char expr[exprSize]; + if (cfi.stackSize == 0) { + snprintf(expr, exprSize, "$rsp"); + } else { + snprintf(expr, exprSize, "$rsp %d +", cfi.stackSize); + } + rules->SetCFARule(expr); + + if (cfi.ripOffset == 0) { + snprintf(expr, exprSize, ".cfa ^"); + } else { + snprintf(expr, exprSize, ".cfa %d - ^", cfi.ripOffset); + } + rules->SetRARule(expr); + + return rules.release(); +} + +} // namespace CrashReporter + +#endif // XP_WIN && HAVE_64BIT_BUILD diff --git a/toolkit/crashreporter/minidump-analyzer/MozStackFrameSymbolizer.h b/toolkit/crashreporter/minidump-analyzer/MozStackFrameSymbolizer.h new file mode 100644 index 0000000000..dfe4299d85 --- /dev/null +++ b/toolkit/crashreporter/minidump-analyzer/MozStackFrameSymbolizer.h @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef MozStackFrameSymbolizer_h +#define MozStackFrameSymbolizer_h + +#if XP_WIN && HAVE_64BIT_BUILD + +# include "Win64ModuleUnwindMetadata.h" + +# include "google_breakpad/processor/stack_frame_symbolizer.h" +# include "google_breakpad/processor/stack_frame.h" + +# include <memory> + +namespace CrashReporter { + +using google_breakpad::CodeModule; +using google_breakpad::CodeModules; +using google_breakpad::SourceLineResolverInterface; +using google_breakpad::StackFrame; +using google_breakpad::StackFrameSymbolizer; +using google_breakpad::SymbolSupplier; +using google_breakpad::SystemInfo; + +class MozStackFrameSymbolizer : public StackFrameSymbolizer { + using google_breakpad::StackFrameSymbolizer::SymbolizerResult; + + std::map<std::string, std::shared_ptr<ModuleUnwindParser>> mModuleMap; + + public: + MozStackFrameSymbolizer(); + + virtual SymbolizerResult FillSourceLineInfo( + const CodeModules* modules, const CodeModules* unloaded_modules, + const SystemInfo* system_info, StackFrame* stack_frame); + + virtual class google_breakpad::CFIFrameInfo* FindCFIFrameInfo( + const StackFrame* frame); +}; + +} // namespace CrashReporter + +#endif // XP_WIN && HAVE_64BIT_BUILD + +#endif // MozStackFrameSymbolizer_h diff --git a/toolkit/crashreporter/minidump-analyzer/Win64ModuleUnwindMetadata.cpp b/toolkit/crashreporter/minidump-analyzer/Win64ModuleUnwindMetadata.cpp new file mode 100644 index 0000000000..22e08ed1af --- /dev/null +++ b/toolkit/crashreporter/minidump-analyzer/Win64ModuleUnwindMetadata.cpp @@ -0,0 +1,258 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#if XP_WIN && HAVE_64BIT_BUILD + +# include "Win64ModuleUnwindMetadata.h" + +# include "MinidumpAnalyzerUtils.h" + +# include <windows.h> +# include <winnt.h> +# include <imagehlp.h> +# include <iostream> +# include <set> +# include <sstream> +# include <string> + +namespace CrashReporter { + +union UnwindCode { + struct { + uint8_t offset_in_prolog; + uint8_t unwind_operation_code : 4; + uint8_t operation_info : 4; + }; + USHORT frame_offset; +}; + +enum UnwindOperationCodes { + UWOP_PUSH_NONVOL = 0, // info == register number + UWOP_ALLOC_LARGE = 1, // no info, alloc size in next 2 slots + UWOP_ALLOC_SMALL = 2, // info == size of allocation / 8 - 1 + UWOP_SET_FPREG = 3, // no info, FP = RSP + UNWIND_INFO.FPRegOffset*16 + UWOP_SAVE_NONVOL = 4, // info == register number, offset in next slot + UWOP_SAVE_NONVOL_FAR = 5, // info == register number, offset in next 2 slots + UWOP_SAVE_XMM = 6, // Version 1; undocumented + UWOP_EPILOG = 6, // Version 2; undocumented + UWOP_SAVE_XMM_FAR = 7, // Version 1; undocumented + UWOP_SPARE = 7, // Version 2; undocumented + UWOP_SAVE_XMM128 = 8, // info == XMM reg number, offset in next slot + UWOP_SAVE_XMM128_FAR = 9, // info == XMM reg number, offset in next 2 slots + UWOP_PUSH_MACHFRAME = 10 // info == 0: no error-code, 1: error-code +}; + +struct UnwindInfo { + uint8_t version : 3; + uint8_t flags : 5; + uint8_t size_of_prolog; + uint8_t count_of_codes; + uint8_t frame_register : 4; + uint8_t frame_offset : 4; + UnwindCode unwind_code[1]; +}; + +ModuleUnwindParser::~ModuleUnwindParser() { + if (mImg) { + ImageUnload(mImg); + } +} + +void* ModuleUnwindParser::RvaToVa(ULONG aRva) { + return ImageRvaToVa(mImg->FileHeader, mImg->MappedAddress, aRva, + &mImg->LastRvaSection); +} + +ModuleUnwindParser::ModuleUnwindParser(const std::string& aPath) + : mPath(aPath) { + // Convert wchar to native charset because ImageLoad only takes + // a PSTR as input. + std::string code_file = UTF8ToMBCS(aPath); + + mImg = ImageLoad((PSTR)code_file.c_str(), NULL); + if (!mImg || !mImg->FileHeader) { + return; + } + + PIMAGE_OPTIONAL_HEADER64 optional_header = &mImg->FileHeader->OptionalHeader; + if (optional_header->Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC) { + return; + } + + DWORD exception_rva = + optional_header->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION] + .VirtualAddress; + + DWORD exception_size = + optional_header->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION].Size; + + auto funcs = (PIMAGE_RUNTIME_FUNCTION_ENTRY)RvaToVa(exception_rva); + if (!funcs) { + return; + } + + for (DWORD i = 0; i < exception_size / sizeof(*funcs); i++) { + mUnwindMap[funcs[i].BeginAddress] = &funcs[i]; + } +} + +bool ModuleUnwindParser::GenerateCFIForFunction( + IMAGE_RUNTIME_FUNCTION_ENTRY& aFunc, UnwindCFI& aRet) { + DWORD unwind_rva = aFunc.UnwindInfoAddress; + // Holds RVA to all visited IMAGE_RUNTIME_FUNCTION_ENTRY, to avoid + // circular references. + std::set<DWORD> visited; + + // Follow chained function entries + while (unwind_rva & 0x1) { + unwind_rva ^= 0x1; + + if (visited.end() != visited.find(unwind_rva)) { + return false; + } + visited.insert(unwind_rva); + + auto chained_func = (PIMAGE_RUNTIME_FUNCTION_ENTRY)RvaToVa(unwind_rva); + if (!chained_func) { + return false; + } + unwind_rva = chained_func->UnwindInfoAddress; + } + + visited.insert(unwind_rva); + + auto unwind_info = (UnwindInfo*)RvaToVa(unwind_rva); + if (!unwind_info) { + return false; + } + + DWORD stack_size = 8; // minimal stack size is 8 for RIP + DWORD rip_offset = 8; + do { + for (uint8_t c = 0; c < unwind_info->count_of_codes; c++) { + UnwindCode* unwind_code = &unwind_info->unwind_code[c]; + switch (unwind_code->unwind_operation_code) { + case UWOP_PUSH_NONVOL: { + stack_size += 8; + break; + } + case UWOP_ALLOC_LARGE: { + if (unwind_code->operation_info == 0) { + c++; + if (c < unwind_info->count_of_codes) { + stack_size += (unwind_code + 1)->frame_offset * 8; + } + } else { + c += 2; + if (c < unwind_info->count_of_codes) { + stack_size += (unwind_code + 1)->frame_offset | + ((unwind_code + 2)->frame_offset << 16); + } + } + break; + } + case UWOP_ALLOC_SMALL: { + stack_size += unwind_code->operation_info * 8 + 8; + break; + } + case UWOP_SET_FPREG: + // To correctly track RSP when it's been transferred to another + // register, we would need to emit CFI records for every unwind op. + // For simplicity, don't emit CFI records for this function as + // we know it will be incorrect after this point. + return false; + case UWOP_SAVE_NONVOL: + case UWOP_SAVE_XMM: // also v2 UWOP_EPILOG + case UWOP_SAVE_XMM128: { + c++; // skip slot with offset + break; + } + case UWOP_SAVE_NONVOL_FAR: + case UWOP_SAVE_XMM_FAR: // also v2 UWOP_SPARE + case UWOP_SAVE_XMM128_FAR: { + c += 2; // skip 2 slots with offset + break; + } + case UWOP_PUSH_MACHFRAME: { + if (unwind_code->operation_info) { + stack_size += 88; + } else { + stack_size += 80; + } + rip_offset += 80; + break; + } + default: { + return false; + } + } + } + + if (unwind_info->flags & UNW_FLAG_CHAININFO) { + auto chained_func = (PIMAGE_RUNTIME_FUNCTION_ENTRY)(( + unwind_info->unwind_code + ((unwind_info->count_of_codes + 1) & ~1))); + + if (visited.end() != visited.find(chained_func->UnwindInfoAddress)) { + return false; // Circular reference + } + + visited.insert(chained_func->UnwindInfoAddress); + + unwind_info = (UnwindInfo*)RvaToVa(chained_func->UnwindInfoAddress); + } else { + unwind_info = nullptr; + } + } while (unwind_info); + + aRet.beginAddress = aFunc.BeginAddress; + aRet.size = aFunc.EndAddress - aFunc.BeginAddress; + aRet.stackSize = stack_size; + aRet.ripOffset = rip_offset; + return true; +} + +// For unit testing we sometimes need any address that's valid in this module. +// Just return the first address we know of. +DWORD +ModuleUnwindParser::GetAnyOffsetAddr() const { + if (mUnwindMap.size() < 1) { + return 0; + } + return mUnwindMap.begin()->first; +} + +bool ModuleUnwindParser::GetCFI(DWORD aAddress, UnwindCFI& aRet) { + // Figure out the begin address of the requested address. + auto itUW = mUnwindMap.lower_bound(aAddress + 1); + if (itUW == mUnwindMap.begin()) { + return false; // address before this module. + } + --itUW; + + // Ensure that the function entry is big enough to contain this address. + IMAGE_RUNTIME_FUNCTION_ENTRY& func = *itUW->second; + if (aAddress > func.EndAddress) { + return false; + } + + // Do we have CFI for this function already? + auto itCFI = mCFIMap.find(aAddress); + if (itCFI != mCFIMap.end()) { + aRet = itCFI->second; + return true; + } + + // No, generate it. + if (!GenerateCFIForFunction(func, aRet)) { + return false; + } + + mCFIMap[func.BeginAddress] = aRet; + return true; +} + +} // namespace CrashReporter + +#endif // XP_WIN && HAVE_64BIT_BUILD diff --git a/toolkit/crashreporter/minidump-analyzer/Win64ModuleUnwindMetadata.h b/toolkit/crashreporter/minidump-analyzer/Win64ModuleUnwindMetadata.h new file mode 100644 index 0000000000..3ce9dd7bb4 --- /dev/null +++ b/toolkit/crashreporter/minidump-analyzer/Win64ModuleUnwindMetadata.h @@ -0,0 +1,56 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef Win64ModuleUnwindMetadata_h +#define Win64ModuleUnwindMetadata_h + +#if XP_WIN && HAVE_64BIT_BUILD + +# include <functional> +# include <map> +# include <string> + +# include <windows.h> +# include <winnt.h> +# include <imagehlp.h> + +namespace CrashReporter { + +struct UnwindCFI { + uint32_t beginAddress; + uint32_t size; + uint32_t stackSize; + uint32_t ripOffset; +}; + +// Does lazy-parsing of unwind info. +class ModuleUnwindParser { + PLOADED_IMAGE mImg; + std::string mPath; + + // Maps begin address to exception record. + // Populated upon construction. + std::map<DWORD, PIMAGE_RUNTIME_FUNCTION_ENTRY> mUnwindMap; + + // Maps begin address to CFI. + // Populated as needed. + std::map<DWORD, UnwindCFI> mCFIMap; + + bool GenerateCFIForFunction(IMAGE_RUNTIME_FUNCTION_ENTRY& aFunc, + UnwindCFI& aRet); + void* RvaToVa(ULONG aRva); + + public: + explicit ModuleUnwindParser(const std::string& aPath); + ~ModuleUnwindParser(); + bool GetCFI(DWORD aAddress, UnwindCFI& aRet); + DWORD GetAnyOffsetAddr() const; +}; + +} // namespace CrashReporter + +#endif // XP_WIN && HAVE_64BIT_BUILD + +#endif // Win64ModuleUnwindMetadata_h diff --git a/toolkit/crashreporter/minidump-analyzer/minidump-analyzer.cpp b/toolkit/crashreporter/minidump-analyzer/minidump-analyzer.cpp new file mode 100644 index 0000000000..1aa2db19de --- /dev/null +++ b/toolkit/crashreporter/minidump-analyzer/minidump-analyzer.cpp @@ -0,0 +1,556 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "minidump-analyzer.h" + +#include <cstdio> +#include <cstring> +#include <string> +#include <sstream> + +#include "json/json.h" +#include "google_breakpad/processor/basic_source_line_resolver.h" +#include "google_breakpad/processor/call_stack.h" +#include "google_breakpad/processor/code_module.h" +#include "google_breakpad/processor/code_modules.h" +#include "google_breakpad/processor/minidump.h" +#include "google_breakpad/processor/minidump_processor.h" +#include "google_breakpad/processor/process_state.h" +#include "google_breakpad/processor/stack_frame.h" +#include "processor/pathname_stripper.h" + +#include "mozilla/FStream.h" +#include "mozilla/Unused.h" + +#if defined(XP_WIN) + +# include <windows.h> +# include "mozilla/glue/WindowsDllServices.h" + +#elif defined(XP_UNIX) || defined(XP_MACOSX) + +# include <sys/types.h> +# include <sys/stat.h> +# include <unistd.h> + +#endif + +#include "MinidumpAnalyzerUtils.h" + +#if XP_WIN && HAVE_64BIT_BUILD && defined(_M_X64) +# include "MozStackFrameSymbolizer.h" +#endif + +namespace CrashReporter { + +#if defined(XP_WIN) + +static mozilla::glue::BasicDllServices gDllServices; + +#endif + +using std::hex; +using std::ios; +using std::ios_base; +using std::map; +using std::showbase; +using std::string; +using std::stringstream; +using std::wstring; + +using google_breakpad::BasicSourceLineResolver; +using google_breakpad::CallStack; +using google_breakpad::CodeModule; +using google_breakpad::CodeModules; +using google_breakpad::Minidump; +using google_breakpad::MinidumpProcessor; +using google_breakpad::PathnameStripper; +using google_breakpad::ProcessResult; +using google_breakpad::ProcessState; +using google_breakpad::StackFrame; + +using mozilla::IFStream; +using mozilla::OFStream; +using mozilla::Unused; + +MinidumpAnalyzerOptions gMinidumpAnalyzerOptions; + +// Path of the minidump to be analyzed. +static string gMinidumpPath; + +struct ModuleCompare { + bool operator()(const CodeModule* aLhs, const CodeModule* aRhs) const { + return aLhs->base_address() < aRhs->base_address(); + } +}; + +typedef map<const CodeModule*, unsigned int, ModuleCompare> OrderedModulesMap; + +static void AddModulesFromCallStack(OrderedModulesMap& aOrderedModules, + const CallStack* aStack) { + int frameCount = aStack->frames()->size(); + + for (int frameIndex = 0; frameIndex < frameCount; ++frameIndex) { + const StackFrame* frame = aStack->frames()->at(frameIndex); + + if (frame->module) { + aOrderedModules.insert( + std::pair<const CodeModule*, unsigned int>(frame->module, 0)); + } + } +} + +static void PopulateModuleList(const ProcessState& aProcessState, + OrderedModulesMap& aOrderedModules, + bool aFullStacks) { + int threadCount = aProcessState.threads()->size(); + int requestingThread = aProcessState.requesting_thread(); + + if (!aFullStacks && (requestingThread != -1)) { + AddModulesFromCallStack(aOrderedModules, + aProcessState.threads()->at(requestingThread)); + } else { + for (int threadIndex = 0; threadIndex < threadCount; ++threadIndex) { + AddModulesFromCallStack(aOrderedModules, + aProcessState.threads()->at(threadIndex)); + } + } + + int moduleCount = 0; + for (auto& itr : aOrderedModules) { + itr.second = moduleCount++; + } +} + +static const char kExtraDataExtension[] = ".extra"; + +static string ToHex(uint64_t aValue) { + stringstream output; + + output << hex << showbase << aValue; + + return output.str(); +} + +// Convert the stack frame trust value into a readable string. + +static string FrameTrust(const StackFrame::FrameTrust aTrust) { + switch (aTrust) { + case StackFrame::FRAME_TRUST_NONE: + return "none"; + case StackFrame::FRAME_TRUST_SCAN: + return "scan"; + case StackFrame::FRAME_TRUST_CFI_SCAN: + return "cfi_scan"; + case StackFrame::FRAME_TRUST_FP: + return "frame_pointer"; + case StackFrame::FRAME_TRUST_CFI: + return "cfi"; + case StackFrame::FRAME_TRUST_PREWALKED: + return "prewalked"; + case StackFrame::FRAME_TRUST_CONTEXT: + return "context"; + } + + return "none"; +} + +// Convert the result value of the minidump processing step into a readable +// string. + +static string ResultString(ProcessResult aResult) { + switch (aResult) { + case google_breakpad::PROCESS_OK: + return "OK"; + case google_breakpad::PROCESS_ERROR_MINIDUMP_NOT_FOUND: + return "ERROR_MINIDUMP_NOT_FOUND"; + case google_breakpad::PROCESS_ERROR_NO_MINIDUMP_HEADER: + return "ERROR_NO_MINIDUMP_HEADER"; + case google_breakpad::PROCESS_ERROR_NO_THREAD_LIST: + return "ERROR_NO_THREAD_LIST"; + case google_breakpad::PROCESS_ERROR_GETTING_THREAD: + return "ERROR_GETTING_THREAD"; + case google_breakpad::PROCESS_ERROR_GETTING_THREAD_ID: + return "ERROR_GETTING_THREAD_ID"; + case google_breakpad::PROCESS_ERROR_DUPLICATE_REQUESTING_THREADS: + return "ERROR_DUPLICATE_REQUESTING_THREADS"; + case google_breakpad::PROCESS_SYMBOL_SUPPLIER_INTERRUPTED: + return "SYMBOL_SUPPLIER_INTERRUPTED"; + default: + return ""; + } +} + +// Convert the list of stack frames to JSON and append them to the array +// specified in the |aNode| parameter. + +static void ConvertStackToJSON(const ProcessState& aProcessState, + const OrderedModulesMap& aOrderedModules, + const CallStack* aStack, Json::Value& aNode) { + int frameCount = aStack->frames()->size(); + + for (int frameIndex = 0; frameIndex < frameCount; ++frameIndex) { + const StackFrame* frame = aStack->frames()->at(frameIndex); + Json::Value frameNode; + + if (frame->module) { + const auto& itr = aOrderedModules.find(frame->module); + + if (itr != aOrderedModules.end()) { + frameNode["module_index"] = (*itr).second; + } + } + + frameNode["trust"] = FrameTrust(frame->trust); + // The 'ip' field is equivalent to socorro's 'offset' field + frameNode["ip"] = ToHex(frame->instruction); + + aNode.append(frameNode); + } +} + +// Convert the list of modules to JSON and append them to the array specified +// in the |aNode| parameter. + +static int ConvertModulesToJSON(const ProcessState& aProcessState, + const OrderedModulesMap& aOrderedModules, + Json::Value& aNode, + Json::Value& aCertSubjects) { + const CodeModules* modules = aProcessState.modules(); + + if (!modules) { + return -1; + } + + uint64_t mainAddress = 0; + const CodeModule* mainModule = modules->GetMainModule(); + + if (mainModule) { + mainAddress = mainModule->base_address(); + } + + int mainModuleIndex = -1; + + for (const auto& itr : aOrderedModules) { + const CodeModule* module = itr.first; + + if ((module->base_address() == mainAddress) && mainModule) { + mainModuleIndex = itr.second; + } + +#if defined(XP_WIN) + auto certSubject = + gDllServices.GetBinaryOrgName(UTF8ToWide(module->code_file()).c_str()); + if (certSubject) { + string strSubject(WideToUTF8(certSubject.get())); + // Json::Value::operator[] creates and returns a null member if the key + // does not exist. + Json::Value& subjectNode = aCertSubjects[strSubject]; + if (!subjectNode) { + // If the member is null, we want to convert that to an array. + subjectNode = Json::Value(Json::arrayValue); + } + + // Now we're guaranteed that subjectNode is an array. Add the new entry. + subjectNode.append(PathnameStripper::File(module->code_file())); + } +#endif + + Json::Value moduleNode; + moduleNode["filename"] = PathnameStripper::File(module->code_file()); + moduleNode["code_id"] = PathnameStripper::File(module->code_identifier()); + moduleNode["version"] = module->version(); + moduleNode["debug_file"] = PathnameStripper::File(module->debug_file()); + moduleNode["debug_id"] = module->debug_identifier(); + moduleNode["base_addr"] = ToHex(module->base_address()); + moduleNode["end_addr"] = ToHex(module->base_address() + module->size()); + + aNode.append(moduleNode); + } + + return mainModuleIndex; +} + +// Convert the process state to JSON, this includes information about the +// crash, the module list and stack traces for every thread + +static void ConvertProcessStateToJSON(const ProcessState& aProcessState, + Json::Value& aStackTraces, + const bool aFullStacks, + Json::Value& aCertSubjects) { + // Crash info + Json::Value crashInfo; + int requestingThread = aProcessState.requesting_thread(); + + if (aProcessState.crashed()) { + crashInfo["type"] = aProcessState.crash_reason(); + crashInfo["address"] = ToHex(aProcessState.crash_address()); + + if (requestingThread != -1) { + // Record the crashing thread index only if this is a full minidump + // and all threads' stacks are present, otherwise only the crashing + // thread stack is written out and this field is set to 0. + crashInfo["crashing_thread"] = aFullStacks ? requestingThread : 0; + } + } else { + crashInfo["type"] = Json::Value(Json::nullValue); + // Add assertion info, if available + string assertion = aProcessState.assertion(); + + if (!assertion.empty()) { + crashInfo["assertion"] = assertion; + } + } + + aStackTraces["crash_info"] = crashInfo; + + // Modules + OrderedModulesMap orderedModules; + PopulateModuleList(aProcessState, orderedModules, aFullStacks); + + Json::Value modules(Json::arrayValue); + int mainModule = ConvertModulesToJSON(aProcessState, orderedModules, modules, + aCertSubjects); + + if (mainModule != -1) { + aStackTraces["main_module"] = mainModule; + } + + aStackTraces["modules"] = modules; + + // Threads + Json::Value threads(Json::arrayValue); + int threadCount = aProcessState.threads()->size(); + + if (!aFullStacks && (requestingThread != -1)) { + // Only add the crashing thread + Json::Value thread; + Json::Value stack(Json::arrayValue); + const CallStack* rawStack = aProcessState.threads()->at(requestingThread); + + ConvertStackToJSON(aProcessState, orderedModules, rawStack, stack); + thread["frames"] = stack; + threads.append(thread); + } else { + for (int threadIndex = 0; threadIndex < threadCount; ++threadIndex) { + Json::Value thread; + Json::Value stack(Json::arrayValue); + const CallStack* rawStack = aProcessState.threads()->at(threadIndex); + + ConvertStackToJSON(aProcessState, orderedModules, rawStack, stack); + thread["frames"] = stack; + threads.append(thread); + } + } + + aStackTraces["threads"] = threads; +} + +// Process the minidump file and append the JSON-formatted stack traces to +// the node specified in |aStackTraces|. We also populate |aCertSubjects| with +// information about the certificates used to sign modules, when present and +// supported by the underlying OS. +static bool ProcessMinidump(Json::Value& aStackTraces, + Json::Value& aCertSubjects, const string& aDumpFile, + const bool aFullStacks) { +#if XP_WIN && HAVE_64BIT_BUILD && defined(_M_X64) + MozStackFrameSymbolizer symbolizer; + MinidumpProcessor minidumpProcessor(&symbolizer, false); +#else + BasicSourceLineResolver resolver; + // We don't have a valid symbol resolver so we pass nullptr instead. + MinidumpProcessor minidumpProcessor(nullptr, &resolver); +#endif + + // Process the minidump. +#if defined(XP_WIN) + // Breakpad invokes std::ifstream directly, so this path needs to be ANSI + Minidump dump(UTF8ToMBCS(aDumpFile)); +#else + Minidump dump(aDumpFile); +#endif // defined(XP_WIN) + if (!dump.Read()) { + return false; + } + + ProcessResult rv; + ProcessState processState; + rv = minidumpProcessor.Process(&dump, &processState); + aStackTraces["status"] = ResultString(rv); + + ConvertProcessStateToJSON(processState, aStackTraces, aFullStacks, + aCertSubjects); + + return true; +} + +static bool ReadExtraFile(const string& aExtraDataPath, Json::Value& aExtra) { + IFStream f( +#if defined(XP_WIN) + UTF8ToWide(aExtraDataPath).c_str(), +#else + aExtraDataPath.c_str(), +#endif // defined(XP_WIN) + ios::in); + if (!f.is_open()) { + return false; + } + + Json::CharReaderBuilder builder; + return parseFromStream(builder, f, &aExtra, nullptr); +} + +// Update the extra data file by adding the StackTraces and ModuleSignatureInfo +// fields that contain the JSON outputs of this program. +static bool UpdateExtraDataFile(const string& aDumpPath, + const Json::Value& aStackTraces, + const Json::Value& aCertSubjects) { + string extraDataPath(aDumpPath); + int dot = extraDataPath.rfind('.'); + + if (dot < 0) { + return false; // Not a valid dump path + } + + extraDataPath.replace(dot, extraDataPath.length() - dot, kExtraDataExtension); + + Json::Value extra; + if (!ReadExtraFile(extraDataPath, extra)) { + return false; + } + + OFStream f( +#if defined(XP_WIN) + UTF8ToWide(extraDataPath).c_str(), +#else + extraDataPath.c_str(), +#endif // defined(XP_WIN) + ios::out | ios::trunc); + + bool res = false; + if (f.is_open()) { + Json::StreamWriterBuilder builder; + builder["indentation"] = ""; + + // The StackTraces field is not stored as a string because it's not a + // crash annotation. It's only used by the crash reporter client which + // strips it before submitting the other annotations to Socorro. + extra["StackTraces"] = aStackTraces; + + if (!!aCertSubjects) { + extra["ModuleSignatureInfo"] = Json::writeString(builder, aCertSubjects); + } + + std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter()); + writer->write(extra, &f); + f << "\n"; + res = !f.fail(); + f.close(); + } + + return res; +} + +bool GenerateStacks(const string& aDumpPath, const bool aFullStacks) { + Json::Value stackTraces; + Json::Value certSubjects; + + if (!ProcessMinidump(stackTraces, certSubjects, aDumpPath, aFullStacks)) { + return false; + } + + return UpdateExtraDataFile(aDumpPath, stackTraces, certSubjects); +} + +} // namespace CrashReporter + +using namespace CrashReporter; + +#if defined(XP_WIN) +# define XP_LITERAL(s) L##s +#else +# define XP_LITERAL(s) s +#endif + +template <typename CharT> +struct CharTraits; + +template <> +struct CharTraits<char> { + static int compare(const char* left, const char* right) { + return strcmp(left, right); + } + + static string& assign(string& left, const char* right) { + left = right; + return left; + } +}; + +#if defined(XP_WIN) + +template <> +struct CharTraits<wchar_t> { + static int compare(const wchar_t* left, const wchar_t* right) { + return wcscmp(left, right); + } + + static string& assign(string& left, const wchar_t* right) { + left = WideToUTF8(right); + return left; + } +}; + +#endif // defined(XP_WIN) + +static void LowerPriority() { +#if defined(XP_WIN) + Unused << SetPriorityClass(GetCurrentProcess(), + PROCESS_MODE_BACKGROUND_BEGIN); +#else // Linux, MacOS X, etc... + Unused << nice(20); +#endif +} + +template <typename CharT, typename Traits = CharTraits<CharT>> +static void ParseArguments(int argc, CharT** argv) { + if (argc <= 1) { + exit(EXIT_FAILURE); + } + + for (int i = 1; i < argc - 1; i++) { + if (!Traits::compare(argv[i], XP_LITERAL("--full"))) { + gMinidumpAnalyzerOptions.fullMinidump = true; + } else if (!Traits::compare(argv[i], XP_LITERAL("--force-use-module")) && + (i < argc - 2)) { + Traits::assign(gMinidumpAnalyzerOptions.forceUseModule, argv[i + 1]); + ++i; + } else { + exit(EXIT_FAILURE); + } + } + + Traits::assign(gMinidumpPath, argv[argc - 1]); +} + +#if defined(XP_WIN) +// WARNING: Windows does *NOT* use UTF8 for char strings off the command line! +// Using wmain here so that the CRT doesn't need to perform a wasteful and +// lossy UTF-16 to MBCS conversion; ParseArguments will convert to UTF8 +// directly. +extern "C" int wmain(int argc, wchar_t** argv) +#else +int main(int argc, char** argv) +#endif +{ + LowerPriority(); + ParseArguments(argc, argv); + + if (!GenerateStacks(gMinidumpPath, gMinidumpAnalyzerOptions.fullMinidump)) { + exit(EXIT_FAILURE); + } + + exit(EXIT_SUCCESS); +} diff --git a/toolkit/crashreporter/minidump-analyzer/minidump-analyzer.exe.manifest b/toolkit/crashreporter/minidump-analyzer/minidump-analyzer.exe.manifest new file mode 100644 index 0000000000..731502e805 --- /dev/null +++ b/toolkit/crashreporter/minidump-analyzer/minidump-analyzer.exe.manifest @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> +<assemblyIdentity + version="1.0.0.0" + processorArchitecture="*" + name="minidump-analyzer" + type="win32" +/> +<dependency> + <dependentAssembly> + <assemblyIdentity + type="win32" + name="mozglue" + version="1.0.0.0" + language="*" + /> + </dependentAssembly> +</dependency> +</assembly> diff --git a/toolkit/crashreporter/minidump-analyzer/minidump-analyzer.h b/toolkit/crashreporter/minidump-analyzer/minidump-analyzer.h new file mode 100644 index 0000000000..0c2edb10d7 --- /dev/null +++ b/toolkit/crashreporter/minidump-analyzer/minidump-analyzer.h @@ -0,0 +1,17 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef MINIDUMP_ANALYZER_H__ +#define MINIDUMP_ANALYZER_H__ + +#include <string> + +namespace CrashReporter { + +bool GenerateStacks(const std::string& aDumpPath, const bool aFullStacks); + +} + +#endif // MINIDUMP_ANALYZER_H__ diff --git a/toolkit/crashreporter/minidump-analyzer/moz.build b/toolkit/crashreporter/minidump-analyzer/moz.build new file mode 100644 index 0000000000..9d505353a4 --- /dev/null +++ b/toolkit/crashreporter/minidump-analyzer/moz.build @@ -0,0 +1,51 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +if CONFIG["OS_TARGET"] != "Android": + GeckoProgram("minidump-analyzer", linkage=None) + + if CONFIG["OS_TARGET"] == "WINNT": + DEFINES["UNICODE"] = True + DEFINES["_UNICODE"] = True + + if CONFIG["CPU_ARCH"] == "x86_64": + UNIFIED_SOURCES += [ + "MozStackFrameSymbolizer.cpp", + "Win64ModuleUnwindMetadata.cpp", + ] + + OS_LIBS += ["dbghelp", "imagehlp"] + + if CONFIG["OS_TARGET"] == "WINNT" and CONFIG["CC_TYPE"] in ("gcc", "clang"): + # This allows us to use wmain as the entry point on mingw + LDFLAGS += [ + "-municode", + ] + +else: + Library("minidump-analyzer") + + EXPORTS += [ + "minidump-analyzer.h", + ] + +UNIFIED_SOURCES += [ + "minidump-analyzer.cpp", +] + +USE_LIBS += [ + "breakpad_processor", + "jsoncpp", +] + +LOCAL_INCLUDES += [ + "/toolkit/components/jsoncpp/include", +] + +if CONFIG["OS_TARGET"] != "WINNT": + DisableStlWrapping() + +include("/toolkit/crashreporter/crashreporter.mozbuild") |