From 18693a1c1f85bc9fc675b5dcffe686955c4fe1c4 Mon Sep 17 00:00:00 2001 From: Arnaud Renevier Date: Tue, 21 Jan 2020 19:31:26 +0000 Subject: [PATCH] Bug 1607364 - CrashReporting API r=baku Implement Crash Report for Reporting API. Differential Revision: https://phabricator.services.mozilla.com/D58053 --HG-- extra : moz-landing-system : lando --- dom/ipc/BrowserParent.cpp | 28 +++++++ dom/reporting/CrashReport.cpp | 79 ++++++++++++++++++++ dom/reporting/CrashReport.h | 25 +++++++ dom/reporting/ReportDeliver.h | 2 + dom/reporting/ReportingHeader.cpp | 18 +++-- dom/reporting/ReportingHeader.h | 4 + dom/reporting/moz.build | 2 + dom/reporting/tests/browser.ini | 11 +++ dom/reporting/tests/browser_cleanup.js | 53 +++---------- dom/reporting/tests/browser_content_crash.js | 54 +++++++++++++ dom/reporting/tests/head.js | 37 +++++++++ modules/libpref/init/StaticPrefList.yaml | 5 ++ toolkit/crashreporter/nsExceptionHandler.cpp | 16 ++++ toolkit/crashreporter/nsExceptionHandler.h | 3 + 14 files changed, 290 insertions(+), 47 deletions(-) create mode 100644 dom/reporting/CrashReport.cpp create mode 100644 dom/reporting/CrashReport.h create mode 100644 dom/reporting/tests/browser_content_crash.js create mode 100644 dom/reporting/tests/head.js diff --git a/dom/ipc/BrowserParent.cpp b/dom/ipc/BrowserParent.cpp index 6c266a8efd9a..6d69d1beaa8d 100644 --- a/dom/ipc/BrowserParent.cpp +++ b/dom/ipc/BrowserParent.cpp @@ -100,6 +100,7 @@ #include "gfxUtils.h" #include "nsILoginManagerAuthPrompter.h" #include "nsPIWindowRoot.h" +#include "nsReadableUtils.h" #include "nsIAuthPrompt2.h" #include "gfxDrawable.h" #include "ImageOps.h" @@ -114,6 +115,7 @@ #include "mozilla/dom/CanonicalBrowsingContext.h" #include "MMPrinter.h" #include "SessionStoreFunctions.h" +#include "mozilla/dom/CrashReport.h" #ifdef XP_WIN # include "mozilla/plugins/PluginWidgetParent.h" @@ -705,6 +707,32 @@ void BrowserParent::ActorDestroy(ActorDestroyReason why) { // case of a crash. BrowserParent::PopFocus(this); + if (why == AbnormalShutdown) { + // dom_reporting_header must also be enabled for the report to be sent. + if (StaticPrefs::dom_reporting_crash_enabled()) { + nsCOMPtr principal = GetContentPrincipal(); + + if (principal) { + nsAutoCString crash_reason; + CrashReporter::GetAnnotation(OtherPid(), + CrashReporter::Annotation::MozCrashReason, + crash_reason); + // FIXME(arenevier): Find a less fragile way to identify that a crash + // was caused by OOM + bool is_oom = false; + if (crash_reason == "OOM" || crash_reason == "OOM!" || + StringBeginsWith(crash_reason, + NS_LITERAL_CSTRING("[unhandlable oom]")) || + StringBeginsWith(crash_reason, + NS_LITERAL_CSTRING("Unhandlable OOM"))) { + is_oom = true; + } + + CrashReport::Deliver(principal, is_oom); + } + } + } + // Prevent executing ContentParent::NotifyTabDestroying in // BrowserParent::Destroy() called by frameLoader->DestroyComplete() below // when tab crashes in contentprocess because ContentParent::ActorDestroy() diff --git a/dom/reporting/CrashReport.cpp b/dom/reporting/CrashReport.cpp new file mode 100644 index 000000000000..e41ffa976dce --- /dev/null +++ b/dom/reporting/CrashReport.cpp @@ -0,0 +1,79 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "mozilla/dom/CrashReport.h" + +#include "mozilla/dom/Navigator.h" +#include "mozilla/dom/ReportingHeader.h" +#include "mozilla/dom/ReportDeliver.h" +#include "nsIPrincipal.h" +#include "nsIURIMutator.h" +#include "nsString.h" + +namespace mozilla { +namespace dom { + +struct StringWriteFunc : public JSONWriteFunc { + nsCString& mCString; + explicit StringWriteFunc(nsCString& aCString) : mCString(aCString) {} + void Write(const char* aStr) override { mCString.Append(aStr); } +}; + +/* static */ +bool CrashReport::Deliver(nsIPrincipal* aPrincipal, bool aIsOOM) { + MOZ_ASSERT(aPrincipal); + + nsAutoCString endpoint_url; + ReportingHeader::GetEndpointForReport(NS_LITERAL_STRING("default"), + aPrincipal, endpoint_url); + if (endpoint_url.IsEmpty()) { + return false; + } + + nsCOMPtr origin_uri; + nsresult rv = aPrincipal->GetURI(getter_AddRefs(origin_uri)); + NS_ENSURE_SUCCESS(rv, false); + NS_ENSURE_TRUE(origin_uri, false); + + nsCOMPtr safe_origin_uri; + rv = NS_MutateURI(origin_uri) + .SetUserPass(EmptyCString()) + .Finalize(safe_origin_uri); + NS_ENSURE_SUCCESS(rv, false); + NS_ENSURE_TRUE(safe_origin_uri, false); + + nsAutoCString safe_origin_spec; + rv = safe_origin_uri->GetSpec(safe_origin_spec); + NS_ENSURE_SUCCESS(rv, false); + + ReportDeliver::ReportData data; + data.mType = NS_LITERAL_STRING("crash"); + data.mGroupName = NS_LITERAL_STRING("default"); + data.mURL = NS_ConvertUTF8toUTF16(safe_origin_spec); + data.mCreationTime = TimeStamp::Now(); + + Navigator::GetUserAgent(nullptr, aPrincipal, false, data.mUserAgent); + data.mPrincipal = aPrincipal; + data.mFailures = 0; + data.mEndpointURL = endpoint_url; + + nsCString body; + JSONWriter writer{MakeUnique(body)}; + + writer.Start(); + if (aIsOOM) { + writer.StringProperty("reason", "oom"); + } + writer.End(); + + data.mReportBodyJSON = body; + + ReportDeliver::Fetch(data); + return true; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/reporting/CrashReport.h b/dom/reporting/CrashReport.h new file mode 100644 index 000000000000..71429f673856 --- /dev/null +++ b/dom/reporting/CrashReport.h @@ -0,0 +1,25 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 mozilla_dom_CrashReport_h +#define mozilla_dom_CrashReport_h + +#include "nsCOMPtr.h" + +class nsIPrincipal; + +namespace mozilla { +namespace dom { + +class CrashReport { + public: + static bool Deliver(nsIPrincipal* aPrincipal, bool aIsOOM); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_CrashReport_h diff --git a/dom/reporting/ReportDeliver.h b/dom/reporting/ReportDeliver.h index d4bf80efb6a6..3a8c3714151c 100644 --- a/dom/reporting/ReportDeliver.h +++ b/dom/reporting/ReportDeliver.h @@ -16,6 +16,8 @@ class nsPIDOMWindowInner; namespace mozilla { namespace dom { +class ReportBody; + class ReportDeliver final : public nsIObserver, public nsITimerCallback { public: NS_DECL_ISUPPORTS diff --git a/dom/reporting/ReportingHeader.cpp b/dom/reporting/ReportingHeader.cpp index e01d0da97549..8319986cc86a 100644 --- a/dom/reporting/ReportingHeader.cpp +++ b/dom/reporting/ReportingHeader.cpp @@ -481,19 +481,25 @@ void ReportingHeader::GetEndpointForReport( const nsAString& aGroupName, const mozilla::ipc::PrincipalInfo& aPrincipalInfo, nsACString& aEndpointURI) { + nsCOMPtr principal = PrincipalInfoToPrincipal(aPrincipalInfo); + if (NS_WARN_IF(!principal)) { + return; + } + GetEndpointForReport(aGroupName, principal, aEndpointURI); +} + +/* static */ +void ReportingHeader::GetEndpointForReport(const nsAString& aGroupName, + nsIPrincipal* aPrincipal, + nsACString& aEndpointURI) { MOZ_ASSERT(aEndpointURI.IsEmpty()); if (!gReporting) { return; } - nsCOMPtr principal = PrincipalInfoToPrincipal(aPrincipalInfo); - if (NS_WARN_IF(!principal)) { - return; - } - nsAutoCString origin; - nsresult rv = principal->GetOrigin(origin); + nsresult rv = aPrincipal->GetOrigin(origin); if (NS_WARN_IF(NS_FAILED(rv))) { return; } diff --git a/dom/reporting/ReportingHeader.h b/dom/reporting/ReportingHeader.h index d33f0aa1c669..a0b6a4cf57d3 100644 --- a/dom/reporting/ReportingHeader.h +++ b/dom/reporting/ReportingHeader.h @@ -63,6 +63,10 @@ class ReportingHeader final : public nsIObserver, public nsITimerCallback { const mozilla::ipc::PrincipalInfo& aPrincipalInfo, nsACString& aEndpointURI); + static void GetEndpointForReport(const nsAString& aGroupName, + nsIPrincipal* aPrincipal, + nsACString& aEndpointURI); + static void RemoveEndpoint(const nsAString& aGroupName, const nsACString& aEndpointURL, const mozilla::ipc::PrincipalInfo& aPrincipalInfo); diff --git a/dom/reporting/moz.build b/dom/reporting/moz.build index 5c54b2ae8015..ff988ca592ac 100644 --- a/dom/reporting/moz.build +++ b/dom/reporting/moz.build @@ -5,6 +5,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. EXPORTS.mozilla.dom = [ + 'CrashReport.h', 'DeprecationReportBody.h', 'EndpointForReportChild.h', 'EndpointForReportParent.h', @@ -19,6 +20,7 @@ EXPORTS.mozilla.dom = [ ] UNIFIED_SOURCES += [ + 'CrashReport.cpp', 'DeprecationReportBody.cpp', 'EndpointForReportChild.cpp', 'EndpointForReportParent.cpp', diff --git a/dom/reporting/tests/browser.ini b/dom/reporting/tests/browser.ini index f9cf651f750b..475a29ba1288 100644 --- a/dom/reporting/tests/browser.ini +++ b/dom/reporting/tests/browser.ini @@ -1,6 +1,17 @@ [DEFAULT] +prefs = + dom.reporting.enabled=true + dom.reporting.crash.enabled=true + dom.reporting.header.enabled=true + dom.reporting.testing.enabled=true + dom.reporting.delivering.timeout=1 + dom.reporting.cleanup.timeout=1 + privacy.userContext.enabled=1 + support-files = delivering.sjs + head.js empty.html [browser_cleanup.js] +[browser_content_crash.js] diff --git a/dom/reporting/tests/browser_cleanup.js b/dom/reporting/tests/browser_cleanup.js index ab67a73a5fd6..0340c42734e3 100644 --- a/dom/reporting/tests/browser_cleanup.js +++ b/dom/reporting/tests/browser_cleanup.js @@ -2,42 +2,10 @@ const TEST_HOST = "example.org"; const TEST_DOMAIN = "https://" + TEST_HOST; -const TEST_PATH = "/browser/dom/reporting/tests/"; -const TEST_TOP_PAGE = TEST_DOMAIN + TEST_PATH + "empty.html"; -const TEST_SJS = TEST_DOMAIN + TEST_PATH + "delivering.sjs"; -async function storeReportingHeader(browser, extraParams = "") { - await SpecialPowers.spawn( - browser, - [{ url: TEST_SJS, extraParams }], - async obj => { - await content - .fetch( - obj.url + - "?task=header" + - (obj.extraParams.length ? "&" + obj.extraParams : "") - ) - .then(r => r.text()) - .then(text => { - is(text, "OK", "Report-to header sent"); - }); - } - ); -} - -add_task(async function() { - await SpecialPowers.flushPrefEnv(); - await SpecialPowers.pushPrefEnv({ - set: [ - ["dom.reporting.enabled", true], - ["dom.reporting.header.enabled", true], - ["dom.reporting.testing.enabled", true], - ["dom.reporting.delivering.timeout", 1], - ["dom.reporting.cleanup.timeout", 1], - ["privacy.userContext.enabled", true], - ], - }); -}); +const TEST_PATH = "/dom/reporting/tests/"; +const TEST_TOP_PAGE = TEST_DOMAIN + "/browser" + TEST_PATH + "empty.html"; +const TEST_SJS = TEST_DOMAIN + "/tests" + TEST_PATH + "delivering.sjs"; add_task(async function() { info("Testing a total cleanup"); @@ -53,7 +21,7 @@ add_task(async function() { "No data before the test" ); - await storeReportingHeader(browser); + await storeReportingHeader(browser, TEST_SJS); ok(ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN), "We have data"); await new Promise(resolve => { @@ -85,7 +53,7 @@ add_task(async function() { "No data before the test" ); - await storeReportingHeader(browser); + await storeReportingHeader(browser, TEST_SJS); ok(ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN), "We have data"); await new Promise(resolve => { @@ -117,7 +85,7 @@ add_task(async function() { "No data before the test" ); - await storeReportingHeader(browser); + await storeReportingHeader(browser, TEST_SJS); ok(ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN), "We have data"); await new Promise(resolve => { @@ -152,7 +120,7 @@ add_task(async function() { "No data before the test" ); - await storeReportingHeader(browser, "410=true"); + await storeReportingHeader(browser, TEST_SJS, "410=true"); ok(ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN), "We have data"); await SpecialPowers.spawn(browser, [], async _ => { @@ -210,7 +178,7 @@ add_task(async function() { "No data before the test" ); - await storeReportingHeader(browser); + await storeReportingHeader(browser, TEST_SJS); ok( !ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN), "We don't have data for the origin" @@ -249,7 +217,7 @@ add_task(async function() { "No data before the test" ); - await storeReportingHeader(browser); + await storeReportingHeader(browser, TEST_SJS); ok( ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN), "We have data for the origin" @@ -273,4 +241,7 @@ add_task(async function() { resolve() ); }); + + // need to check the reports, to cleanup the server state. + await checkReport(TEST_SJS); }); diff --git a/dom/reporting/tests/browser_content_crash.js b/dom/reporting/tests/browser_content_crash.js new file mode 100644 index 000000000000..aa592a712d3f --- /dev/null +++ b/dom/reporting/tests/browser_content_crash.js @@ -0,0 +1,54 @@ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ +"use strict"; + +const TEST_HOST = "example.org"; +const TEST_DOMAIN = "https://" + TEST_HOST; +const TEST_PATH = "/dom/reporting/tests/"; +const TEST_TOP_PAGE = TEST_DOMAIN + "/browser" + TEST_PATH + "empty.html"; +const TEST_SJS = TEST_DOMAIN + "/tests" + TEST_PATH + "delivering.sjs"; + +function crash_content(browser) { + browser.messageManager.loadFrameScript( + "data:,Components.classes['@mozilla.org/xpcom/debug;1'].getService(Components.interfaces.nsIDebug2).rustPanic('OH NO');", + false + ); +} + +add_task(async function() { + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + ok( + !ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN), + "No data before the test" + ); + + await storeReportingHeader(browser, TEST_SJS); + ok( + ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN), + "We have data for the origin" + ); + + crash_content(browser); + + const reports = await checkReport(TEST_SJS); + is(reports.length, 1, "We have 1 report"); + const report = reports[0]; + is(report.contentType, "application/reports+json", "Correct mime-type"); + is(report.origin, "https://example.org", "Origin correctly set"); + is( + report.url, + "https://example.org/tests/dom/reporting/tests/delivering.sjs", + "URL is correctly set" + ); + ok(!!report.body, "We have a report.body"); + ok(report.body.age > 0, "Age is correctly set"); + is(report.body.url, TEST_TOP_PAGE, "URL is correctly set"); + is(report.body.user_agent, navigator.userAgent, "User-agent matches"); + is(report.body.type, "crash", "Type is fine."); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/reporting/tests/head.js b/dom/reporting/tests/head.js new file mode 100644 index 000000000000..a8d264e0bd1b --- /dev/null +++ b/dom/reporting/tests/head.js @@ -0,0 +1,37 @@ +"use strict"; + +/* exported storeReportingHeader */ +async function storeReportingHeader(browser, reportingURL, extraParams = "") { + await SpecialPowers.spawn( + browser, + [{ url: reportingURL, extraParams }], + async obj => { + await content + .fetch( + obj.url + + "?task=header" + + (obj.extraParams.length ? "&" + obj.extraParams : "") + ) + .then(r => r.text()) + .then(text => { + is(text, "OK", "Report-to header sent"); + }); + } + ); +} + +/* exported checkReport */ +function checkReport(reportingURL) { + return new Promise(resolve => { + let id = setInterval(_ => { + fetch(reportingURL + "?task=check") + .then(r => r.text()) + .then(text => { + if (text) { + resolve(JSON.parse(text)); + clearInterval(id); + } + }); + }, 1000); + }); +} diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml index 741efef9f0db..c24d8470b19c 100644 --- a/modules/libpref/init/StaticPrefList.yaml +++ b/modules/libpref/init/StaticPrefList.yaml @@ -2143,6 +2143,11 @@ value: @IS_NIGHTLY_BUILD@ mirror: always +- name: dom.reporting.crash.enabled + type: RelaxedAtomicBool + value: false + mirror: always + - name: dom.reporting.header.enabled type: RelaxedAtomicBool value: false diff --git a/toolkit/crashreporter/nsExceptionHandler.cpp b/toolkit/crashreporter/nsExceptionHandler.cpp index dd4c815bf4df..8511ca527854 100644 --- a/toolkit/crashreporter/nsExceptionHandler.cpp +++ b/toolkit/crashreporter/nsExceptionHandler.cpp @@ -3634,6 +3634,22 @@ bool SetRemoteExceptionHandler(const nsACString& crashPipe) { } #endif // XP_WIN +void GetAnnotation(uint32_t childPid, Annotation annotation, + nsACString& outStr) { + if (!GetEnabled()) { + return; + } + + MutexAutoLock lock(*dumpMapLock); + + ChildProcessData* pd = pidToMinidump->GetEntry(childPid); + if (!pd) { + return; + } + + outStr = (*pd->annotations)[annotation]; +} + bool TakeMinidumpForChild(uint32_t childPid, nsIFile** dump, AnnotationTable& aAnnotations, uint32_t* aSequence) { if (!GetEnabled()) return false; diff --git a/toolkit/crashreporter/nsExceptionHandler.h b/toolkit/crashreporter/nsExceptionHandler.h index 67baf5254a82..d47c42cecf34 100644 --- a/toolkit/crashreporter/nsExceptionHandler.h +++ b/toolkit/crashreporter/nsExceptionHandler.h @@ -117,6 +117,9 @@ nsresult UnregisterAppMemory(void* ptr); // Include heap regions of the crash context. void SetIncludeContextHeap(bool aValue); +void GetAnnotation(uint32_t childPid, Annotation annotation, + nsACString& outStr); + // Functions for working with minidumps and .extras typedef mozilla::EnumeratedArray AnnotationTable;