From 54fdfadaf704dda7730daee7ec720f7ab863153c Mon Sep 17 00:00:00 2001 From: Gabriele Svelto Date: Tue, 28 Jun 2016 15:30:29 +0200 Subject: [PATCH] Bug 1280477 - Write the stack traces extracted from a crash dump into the crash.main event file r=ted --- .../crashreporter/client/crashreporter.cpp | 15 + toolkit/crashreporter/client/crashreporter.h | 5 + .../client/crashreporter_unix_common.cpp | 22 +- .../client/crashreporter_win.cpp | 22 + toolkit/crashreporter/client/moz.build | 2 + toolkit/crashreporter/docs/index.rst | 12 +- .../minidump-analyzer/minidump-analyzer.cpp | 516 ++++++++++++++++++ .../crashreporter/minidump-analyzer/moz.build | 34 ++ toolkit/crashreporter/moz.build | 1 + 9 files changed, 624 insertions(+), 5 deletions(-) create mode 100644 toolkit/crashreporter/minidump-analyzer/minidump-analyzer.cpp create mode 100644 toolkit/crashreporter/minidump-analyzer/moz.build diff --git a/toolkit/crashreporter/client/crashreporter.cpp b/toolkit/crashreporter/client/crashreporter.cpp index 69d2c3686633..cd5ebad78cfe 100644 --- a/toolkit/crashreporter/client/crashreporter.cpp +++ b/toolkit/crashreporter/client/crashreporter.cpp @@ -492,6 +492,17 @@ bool CheckEndOfLifed(string version) return UIFileExists(reportPath); } +static string +GetMinidumpAnalyzerPath() +{ + string path = gArgv[0]; + size_t pos = path.rfind(UI_CRASH_REPORTER_FILENAME BIN_SUFFIX); + path.erase(pos); + path.append(UI_MINIDUMP_ANALYZER_FILENAME BIN_SUFFIX); + + return path; +} + int main(int argc, char** argv) { gArgc = argc; @@ -513,6 +524,10 @@ int main(int argc, char** argv) // no dump file specified, run the default UI UIShowDefaultUI(); } else { + // start by running minidump analyzer + UIRunMinidumpAnalyzer(GetMinidumpAnalyzerPath(), gReporterDumpFile); + + // go ahead with the crash reporter gExtraFile = GetAdditionalFilename(gReporterDumpFile, kExtraDataExtension); if (gExtraFile.empty()) { UIError(gStrings[ST_ERROR_BADARGUMENTS]); diff --git a/toolkit/crashreporter/client/crashreporter.h b/toolkit/crashreporter/client/crashreporter.h index c35b26e79a72..8c5ca3613d71 100644 --- a/toolkit/crashreporter/client/crashreporter.h +++ b/toolkit/crashreporter/client/crashreporter.h @@ -37,6 +37,9 @@ std::string WideToUTF8(const std::wstring& wide, bool* success = 0); #endif +#define UI_CRASH_REPORTER_FILENAME "crashreporter" +#define UI_MINIDUMP_ANALYZER_FILENAME "minidump-analyzer" + typedef std::map StringTable; #define ST_CRASHREPORTERTITLE "CrashReporterTitle" @@ -145,6 +148,8 @@ std::ofstream* UIOpenWrite(const std::string& filename, bool append=false, bool binary=false); void UIPruneSavedDumps(const std::string& directory); +void UIRunMinidumpAnalyzer(const std::string& exename, + const std::string& filename); #ifdef _MSC_VER # pragma warning( pop ) diff --git a/toolkit/crashreporter/client/crashreporter_unix_common.cpp b/toolkit/crashreporter/client/crashreporter_unix_common.cpp index 10d0cd495d65..f42a35616aac 100644 --- a/toolkit/crashreporter/client/crashreporter_unix_common.cpp +++ b/toolkit/crashreporter/client/crashreporter_unix_common.cpp @@ -4,11 +4,13 @@ #include "crashreporter.h" -#include -#include -#include #include +#include +#include +#include +#include + using namespace CrashReporter; using std::string; using std::vector; @@ -67,3 +69,17 @@ void UIPruneSavedDumps(const std::string& directory) dumpfiles.pop_back(); } } + +void UIRunMinidumpAnalyzer(const string& exename, const string& filename) +{ + // Run the minidump analyzer and wait for it to finish + pid_t pid = fork(); + + if (pid == -1) { + return; // Nothing to do upon failure + } else if (pid == 0) { + execl(exename.c_str(), exename.c_str(), filename.c_str(), nullptr); + } else { + waitpid(pid, nullptr, 0); + } +} diff --git a/toolkit/crashreporter/client/crashreporter_win.cpp b/toolkit/crashreporter/client/crashreporter_win.cpp index a49df3f898e9..57ca495badfd 100644 --- a/toolkit/crashreporter/client/crashreporter_win.cpp +++ b/toolkit/crashreporter/client/crashreporter_win.cpp @@ -1544,3 +1544,25 @@ void UIPruneSavedDumps(const std::string& directory) dumpfiles.pop_back(); } } + +void UIRunMinidumpAnalyzer(const string& exename, const string& filename) +{ + wstring cmdLine; + + cmdLine += L"\"" + UTF8ToWide(exename) + L"\" "; + cmdLine += L"\"" + UTF8ToWide(filename) + L"\" "; + + STARTUPINFO si = {}; + PROCESS_INFORMATION pi = {}; + + si.cb = sizeof(si); + si.dwFlags = STARTF_USESHOWWINDOW; + si.wShowWindow = SW_SHOWNORMAL; + + if (CreateProcess(nullptr, (LPWSTR)cmdLine.c_str(), nullptr, nullptr, FALSE, + 0, nullptr, nullptr, &si, &pi)) { + WaitForSingleObject(pi.hProcess, INFINITE); + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + } +} diff --git a/toolkit/crashreporter/client/moz.build b/toolkit/crashreporter/client/moz.build index acccc7756d22..456c794af31d 100644 --- a/toolkit/crashreporter/client/moz.build +++ b/toolkit/crashreporter/client/moz.build @@ -67,6 +67,8 @@ if CONFIG['OS_ARCH'] == 'Linux' or CONFIG['OS_ARCH'] == 'SunOS': '/toolkit/themes/windows/global/throbber/Throbber-small.gif', ] +DEFINES['BIN_SUFFIX'] = '"%s"' % CONFIG['BIN_SUFFIX'] + RCINCLUDE = 'crashreporter.rc' # Don't use the STL wrappers in the crashreporter clients; they don't diff --git a/toolkit/crashreporter/docs/index.rst b/toolkit/crashreporter/docs/index.rst index 34260a03e4ed..a588671d14f7 100644 --- a/toolkit/crashreporter/docs/index.rst +++ b/toolkit/crashreporter/docs/index.rst @@ -32,6 +32,11 @@ Crash Reporter Client to handle dump files. This application optionally submits crashes to Mozilla (or the configured server). +Minidump Analyzer + The minidump analyzer is a standalone executable that is launched by the + crash reporter client or by the browser itself to extract stack traces from + the dump files generated during a crash. + How Main-Process Crash Handling Works ===================================== @@ -83,8 +88,11 @@ argument. The *crash reporter client* performs a number of roles. There's a lot going on, so you may want to look at ``main()`` in ``crashreporter.cpp``. First, -it verifies the dump data is sane. If it isn't (e.g. required metadata is -missing), the dump data is ignored. If dump data looks sane, the dump data +stack traces are extracted from the dump via the *minidump analyzer* tool. +The resulting traces are appended to the main crash event file. Then, the +*crash reporter client* verifies that the dump data is sane. If it isn't +(e.g. required metadata is missing), the dump data is ignored. If dump data +looks sane, the dump data is moved into the *pending* directory for the configured data directory (defined via the ``MOZ_CRASHREPORTER_DATA_DIRECTORY`` environment variable or from the UI). Once this is done, the main crash reporter UI is displayed diff --git a/toolkit/crashreporter/minidump-analyzer/minidump-analyzer.cpp b/toolkit/crashreporter/minidump-analyzer/minidump-analyzer.cpp new file mode 100644 index 000000000000..0337ced3126f --- /dev/null +++ b/toolkit/crashreporter/minidump-analyzer/minidump-analyzer.cpp @@ -0,0 +1,516 @@ +/* -*- 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 +#include +#include +#include + +#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" + +#if defined(XP_WIN32) + +#include + +#define DIR_SEPARATOR "\\" + +#elif defined(XP_UNIX) || defined(XP_MACOSX) + +#include +#include +#include + +#define DIR_SEPARATOR "/" + +#endif + +namespace CrashReporter { + +using std::ios; +using std::ios_base; +using std::hex; +using std::ofstream; +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; + +#ifdef XP_WIN + +static string +WideToUTF8(const wstring& aWideStr, bool* aSuccess = nullptr) +{ + char* buffer = nullptr; + int buffer_size = WideCharToMultiByte(CP_UTF8, 0, aWideStr.c_str(), + -1, nullptr, 0, nullptr, nullptr); + if (buffer_size == 0) { + if (aSuccess) { + *aSuccess = false; + } + + return ""; + } + + buffer = new char[buffer_size]; + + if (buffer == nullptr) { + if (aSuccess) { + *aSuccess = false; + } + + return ""; + } + + WideCharToMultiByte(CP_UTF8, 0, aWideStr.c_str(), + -1, buffer, buffer_size, nullptr, nullptr); + string mb = buffer; + delete [] buffer; + + if (aSuccess) { + *aSuccess = true; + } + + return mb; +} + +static wstring UTF8ToWide(const string& aUtf8Str, bool *aSuccess = nullptr) +{ + wchar_t* buffer = nullptr; + int buffer_size = MultiByteToWideChar(CP_UTF8, 0, aUtf8Str.c_str(), + -1, nullptr, 0); + if (buffer_size == 0) { + if (aSuccess) { + *aSuccess = false; + } + + return L""; + } + + buffer = new wchar_t[buffer_size]; + + if (buffer == nullptr) { + if (aSuccess) { + *aSuccess = false; + } + + return L""; + } + + MultiByteToWideChar(CP_UTF8, 0, aUtf8Str.c_str(), + -1, buffer, buffer_size); + wstring str = buffer; + delete [] buffer; + + if (aSuccess) { + *aSuccess = true; + } + + return str; +} + +#endif + +static string gReporterDumpFile; + +struct ModuleCompare { + bool operator() (const CodeModule* aLhs, const CodeModule* aRhs) const { + return aLhs->base_address() < aRhs->base_address(); + } +}; + +typedef map OrderedModulesMap; + +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(); + unsigned int moduleIndex = 0; + + for (int frameIndex = 0; frameIndex < frameCount; ++frameIndex) { + const StackFrame *frame = aStack->frames()->at(frameIndex); + Json::Value frameNode; + + if (frame->module) { + auto itr = aOrderedModules.find(frame->module); + + if (itr != aOrderedModules.end()) { + moduleIndex = (*itr).second; + frameNode["module_index"] = moduleIndex; + } + } + + 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, + OrderedModulesMap& aOrderedModules, + Json::Value& aNode) +{ + const CodeModules* modules = aProcessState.modules(); + + if (!modules) { + return -1; + } + + // Create a sorted set of modules so that we'll be able to lookup the index + // of a particular module. + for (unsigned int i = 0; i < modules->module_count(); ++i) { + aOrderedModules.insert( + std::pair( + modules->GetModuleAtSequence(i), i + ) + ); + } + + uint64_t mainAddress = 0; + const CodeModule *mainModule = modules->GetMainModule(); + + if (mainModule) { + mainAddress = mainModule->base_address(); + } + + unsigned int moduleCount = modules->module_count(); + int mainModuleIndex = -1; + + for (unsigned int moduleSequence = 0; + moduleSequence < moduleCount; + ++moduleSequence) + { + const CodeModule *module = modules->GetModuleAtSequence(moduleSequence); + + if (module->base_address() == mainAddress) { + mainModuleIndex = moduleSequence; + } + + 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& aRoot) +{ + // We use this map to get the index of a module when listed by address + OrderedModulesMap orderedModules; + + // 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) { + crashInfo["crashing_thread"] = requestingThread; + } + } else { + crashInfo["type"] = Json::Value(Json::nullValue); + // Add assertion info, if available + string assertion = aProcessState.assertion(); + + if (!assertion.empty()) { + crashInfo["assertion"] = assertion; + } + } + + aRoot["crash_info"] = crashInfo; + + // Modules + Json::Value modules(Json::arrayValue); + int mainModule = ConvertModulesToJSON(aProcessState, orderedModules, modules); + + if (mainModule != -1) { + aRoot["main_module"] = mainModule; + } + + aRoot["modules"] = modules; + + // Threads + Json::Value threads(Json::arrayValue); + int threadCount = aProcessState.threads()->size(); + + 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); + } + + aRoot["threads"] = threads; +} + +// Process the minidump file and append the JSON-formatted stack traces to +// the node specified in |aRoot| + +static bool +ProcessMinidump(Json::Value& aRoot) { + BasicSourceLineResolver resolver; + // We don't have a valid symbol resolver so we pass nullptr instead. + MinidumpProcessor minidumpProcessor(nullptr, &resolver); + + // Process the minidump. + Minidump dump(gReporterDumpFile); + if (!dump.Read()) { + return false; + } + + ProcessResult rv; + ProcessState processState; + rv = minidumpProcessor.Process(&dump, &processState); + aRoot["status"] = ResultString(rv); + + ConvertProcessStateToJSON(processState, aRoot); + + return true; +} + +static string +Basename(const string& file) +{ + string::size_type slashIndex = file.rfind(DIR_SEPARATOR); + if (slashIndex != string::npos) + return file.substr(slashIndex + 1); + else + return file; +} + +// Return the ID of the crash report being processed + +static string +GetDumpLocalID() +{ + string localId = Basename(gReporterDumpFile); + string::size_type dot = localId.rfind('.'); + + if (dot == string::npos) + return ""; + + return localId.substr(0, dot); +} + +// Open the specified file in append mode + +static ofstream* +OpenAppend(const string& aFilename) +{ + ios_base::openmode mode = ios::out | ios::app; + +#if defined(XP_WIN) +#if defined(_MSC_VER) + ofstream* file = new ofstream(); + file->open(UTF8ToWide(aFilename).c_str(), mode); +#else // GCC + ofstream* file = + new ofstream(WideToMBCP(UTF8ToWide(aFilename), CP_ACP).c_str(), mode); +#endif // _MSC_VER +#else // Non-Windows + ofstream* file = new ofstream(aFilename.c_str(), mode); +#endif // XP_WIN + return file; +} + +// Check if a file exists at the specified path + +static bool +FileExists(const string& aPath) +{ +#if defined(XP_WIN) + DWORD attrs = GetFileAttributes(UTF8ToWide(aPath).c_str()); + return (attrs != INVALID_FILE_ATTRIBUTES); +#else // Non-Windows + struct stat sb; + int ret = stat(aPath.c_str(), &sb); + if (ret == -1 || !(sb.st_mode & S_IFREG)) { + return false; + } + + return true; +#endif // XP_WIN +} + +// Update the crash event file by adding the StackTraces field holding the +// JSON output of this program. + +static void +UpdateCrashEvent(string& aEventsPath, const Json::Value& aRoot) +{ + if (aEventsPath.empty()) { + // If there is no path to find the crash event in, skip it. + return; + } + + string localId = GetDumpLocalID(); + string fpath = aEventsPath + DIR_SEPARATOR + localId; + ofstream* f = OpenAppend(fpath.c_str()); + + if (f->is_open()) { + Json::FastWriter writer; + + *f << "StackTraces=" << writer.write(aRoot); + + f->close(); + } + + delete f; +} + +} // namespace CrashReporter + +using namespace CrashReporter; + +int main(int argc, char** argv) +{ + if (argc > 1) { + gReporterDumpFile = argv[1]; + } + + if (gReporterDumpFile.empty()) { + exit(EXIT_FAILURE); + } + + string eventsPath; +#ifdef XP_WIN32 + static const wchar_t kEventsDirKey[] = L"MOZ_CRASHREPORTER_EVENTS_DIRECTORY"; + const wchar_t *eventsPathEnv = _wgetenv(kEventsDirKey); + if (eventsPathEnv && *eventsPathEnv) { + eventsPath = WideToUTF8(eventsPathEnv); + } +#else + static const char kEventsDirKey[] = "MOZ_CRASHREPORTER_EVENTS_DIRECTORY"; + const char *eventsPathEnv = getenv(kEventsDirKey); + if (eventsPathEnv && *eventsPathEnv) { + eventsPath = eventsPathEnv; + } +#endif + else { + eventsPath.clear(); + } + + if (!FileExists(gReporterDumpFile)) { + // The dump file does not exist + return 1; + } + + // Try processing the minidump + Json::Value root; + if (ProcessMinidump(root)) { + UpdateCrashEvent(eventsPath, root); + } + + exit(EXIT_SUCCESS); +} diff --git a/toolkit/crashreporter/minidump-analyzer/moz.build b/toolkit/crashreporter/minidump-analyzer/moz.build new file mode 100644 index 000000000000..bd4aa762fb67 --- /dev/null +++ b/toolkit/crashreporter/minidump-analyzer/moz.build @@ -0,0 +1,34 @@ +# -*- 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': + Program('minidump-analyzer') + + DEFINES['UNICODE'] = True + DEFINES['_UNICODE'] = True + + UNIFIED_SOURCES += [ + 'minidump-analyzer.cpp', + ] + + USE_LIBS += [ + 'breakpad_processor', + 'jsoncpp', + ] + + LOCAL_INCLUDES += [ + '/toolkit/crashreporter/jsoncpp/include', + ] + + + if CONFIG['OS_TARGET'] == 'Darwin': + DIST_SUBDIR = 'crashreporter.app/Contents/MacOS' + +# Don't use the STL wrappers in the crashreporter clients; they don't +# link with -lmozalloc, and it really doesn't matter here anyway. +DISABLE_STL_WRAPPING = True + +include('/toolkit/crashreporter/crashreporter.mozbuild') diff --git a/toolkit/crashreporter/moz.build b/toolkit/crashreporter/moz.build index 193c7de3cffe..a54c1f970606 100644 --- a/toolkit/crashreporter/moz.build +++ b/toolkit/crashreporter/moz.build @@ -56,6 +56,7 @@ DIRS += [ if CONFIG['NIGHTLY_BUILD']: DIRS += [ 'jsoncpp/src/lib_json', + 'minidump-analyzer', ] if CONFIG['MOZ_CRASHREPORTER_INJECTOR']: