summaryrefslogtreecommitdiffstats
path: root/toolkit/crashreporter/test
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/crashreporter/test')
-rw-r--r--toolkit/crashreporter/test/CrashTestUtils.sys.mjs100
-rw-r--r--toolkit/crashreporter/test/ExceptionThrower.cpp3
-rw-r--r--toolkit/crashreporter/test/ExceptionThrower.h1
-rw-r--r--toolkit/crashreporter/test/browser/browser.ini15
-rw-r--r--toolkit/crashreporter/test/browser/browser_aboutCrashes.js82
-rw-r--r--toolkit/crashreporter/test/browser/browser_aboutCrashesResubmit.js202
-rw-r--r--toolkit/crashreporter/test/browser/browser_bug471404.js61
-rw-r--r--toolkit/crashreporter/test/browser/browser_clearReports.js134
-rw-r--r--toolkit/crashreporter/test/browser/browser_cpu_microcode.js25
-rw-r--r--toolkit/crashreporter/test/browser/browser_sandbox_crash.js77
-rw-r--r--toolkit/crashreporter/test/browser/crashreport.sjs176
-rw-r--r--toolkit/crashreporter/test/browser/head.js163
-rw-r--r--toolkit/crashreporter/test/dumputils.cpp83
-rw-r--r--toolkit/crashreporter/test/moz.build67
-rw-r--r--toolkit/crashreporter/test/nsTestCrasher.cpp364
-rw-r--r--toolkit/crashreporter/test/unit/crasher_subprocess_head.js29
-rw-r--r--toolkit/crashreporter/test/unit/crasher_subprocess_tail.js20
-rw-r--r--toolkit/crashreporter/test/unit/head_crashreporter.js347
-rw-r--r--toolkit/crashreporter/test/unit/head_win64cfi.js215
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_AsyncShutdown.js75
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_abort.js17
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_after_js_large_allocation_failure.js23
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_after_js_large_allocation_failure_reporting.js27
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_after_js_oom_recovered.js22
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_after_js_oom_reported.js36
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_after_js_oom_reported_2.js28
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_backgroundtask_moz_crash.js25
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_exc_guard.js30
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_heap_corruption.js27
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_modules.js44
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_moz_crash.js17
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_oom.js21
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_phc.js59
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_purevirtual.js29
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_rust_panic.js15
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_rust_panic_multiline.js15
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_stack_overflow.js21
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_terminator.js39
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_uncaught_exception.js17
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_win64cfi_alloc_large.js6
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_win64cfi_alloc_small.js6
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_win64cfi_epilog.js6
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_win64cfi_infinite_code_chain.exebin0 -> 1440 bytes
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_win64cfi_infinite_code_chain.js22
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_win64cfi_infinite_entry_chain.exebin0 -> 1440 bytes
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_win64cfi_infinite_entry_chain.js22
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_win64cfi_invalid_exception_rva.exebin0 -> 1440 bytes
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_win64cfi_invalid_exception_rva.js21
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_win64cfi_not_a_pe.exe1
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_win64cfi_not_a_pe.js20
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_win64cfi_push_nonvol.js6
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_win64cfi_save_nonvol.js6
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_win64cfi_save_nonvol_far.js6
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_win64cfi_save_xmm128.js6
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_win64cfi_save_xmm128_far.js6
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_win64cfi_unknown_op.js12
-rw-r--r--toolkit/crashreporter/test/unit/test_crash_with_memory_report.js49
-rw-r--r--toolkit/crashreporter/test/unit/test_crashreporter.js90
-rw-r--r--toolkit/crashreporter/test/unit/test_crashreporter_appmem.js13
-rw-r--r--toolkit/crashreporter/test/unit/test_crashreporter_crash.js223
-rw-r--r--toolkit/crashreporter/test/unit/test_event_files.js56
-rw-r--r--toolkit/crashreporter/test/unit/test_kill.js43
-rw-r--r--toolkit/crashreporter/test/unit/test_oom_annotation.js71
-rw-r--r--toolkit/crashreporter/test/unit/test_override_exception_handler.js11
-rw-r--r--toolkit/crashreporter/test/unit/xpcshell-phc.ini11
-rw-r--r--toolkit/crashreporter/test/unit/xpcshell.ini131
-rw-r--r--toolkit/crashreporter/test/unit_ipc/test_content_annotation.js33
-rw-r--r--toolkit/crashreporter/test/unit_ipc/test_content_exception_time_annotation.js21
-rw-r--r--toolkit/crashreporter/test/unit_ipc/test_content_large_annotation.js25
-rw-r--r--toolkit/crashreporter/test/unit_ipc/test_content_memory_list.js33
-rw-r--r--toolkit/crashreporter/test/unit_ipc/test_content_oom_annotation.js33
-rw-r--r--toolkit/crashreporter/test/unit_ipc/test_content_phc.js31
-rw-r--r--toolkit/crashreporter/test/unit_ipc/test_content_phc2.js34
-rw-r--r--toolkit/crashreporter/test/unit_ipc/test_content_phc3.js35
-rw-r--r--toolkit/crashreporter/test/unit_ipc/test_content_rust_panic.js23
-rw-r--r--toolkit/crashreporter/test/unit_ipc/test_content_rust_panic_multiline.js23
-rw-r--r--toolkit/crashreporter/test/unit_ipc/xpcshell-phc.ini11
-rw-r--r--toolkit/crashreporter/test/unit_ipc/xpcshell.ini17
-rw-r--r--toolkit/crashreporter/test/win64UnwindInfoTests.asm378
79 files changed, 4292 insertions, 0 deletions
diff --git a/toolkit/crashreporter/test/CrashTestUtils.sys.mjs b/toolkit/crashreporter/test/CrashTestUtils.sys.mjs
new file mode 100644
index 0000000000..99f5bf4a48
--- /dev/null
+++ b/toolkit/crashreporter/test/CrashTestUtils.sys.mjs
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+export var CrashTestUtils = {
+ // These will be defined using ctypes APIs below.
+ crash: null,
+ dumpHasStream: null,
+ dumpHasInstructionPointerMemory: null,
+ dumpWin64CFITestSymbols: null,
+
+ // Constants for crash()
+ // Keep these in sync with nsTestCrasher.cpp!
+ CRASH_INVALID_POINTER_DEREF: 0,
+ CRASH_PURE_VIRTUAL_CALL: 1,
+ CRASH_RUNTIMEABORT: 2,
+ CRASH_OOM: 3,
+ CRASH_MOZ_CRASH: 4,
+ CRASH_ABORT: 5,
+ CRASH_UNCAUGHT_EXCEPTION: 6,
+ CRASH_X64CFI_NO_MANS_LAND: 7,
+ CRASH_X64CFI_LAUNCHER: 8,
+ CRASH_X64CFI_UNKNOWN_OPCODE: 9,
+ CRASH_X64CFI_PUSH_NONVOL: 10,
+ CRASH_X64CFI_ALLOC_SMALL: 11,
+ CRASH_X64CFI_ALLOC_LARGE: 12,
+ CRASH_X64CFI_SAVE_NONVOL: 15,
+ CRASH_X64CFI_SAVE_NONVOL_FAR: 16,
+ CRASH_X64CFI_SAVE_XMM128: 17,
+ CRASH_X64CFI_SAVE_XMM128_FAR: 18,
+ CRASH_X64CFI_EPILOG: 19,
+ CRASH_X64CFI_EOF: 20,
+ CRASH_PHC_USE_AFTER_FREE: 21,
+ CRASH_PHC_DOUBLE_FREE: 22,
+ CRASH_PHC_BOUNDS_VIOLATION: 23,
+ CRASH_HEAP_CORRUPTION: 24,
+ CRASH_EXC_GUARD: 25,
+ CRASH_STACK_OVERFLOW: 26,
+
+ // Constants for dumpHasStream()
+ // From google_breakpad/common/minidump_format.h
+ MD_THREAD_LIST_STREAM: 3,
+ MD_MEMORY_INFO_LIST_STREAM: 16,
+};
+
+// Grab APIs from the testcrasher shared library
+import { ctypes } from "resource://gre/modules/ctypes.sys.mjs";
+
+var dir = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
+var file = dir.clone();
+file = file.parent;
+file.append(ctypes.libraryName("testcrasher"));
+var lib = ctypes.open(file.path);
+CrashTestUtils.crash = lib.declare(
+ "Crash",
+ ctypes.default_abi,
+ ctypes.void_t,
+ ctypes.int16_t
+);
+CrashTestUtils.saveAppMemory = lib.declare(
+ "SaveAppMemory",
+ ctypes.default_abi,
+ ctypes.uint64_t
+);
+
+try {
+ CrashTestUtils.TryOverrideExceptionHandler = lib.declare(
+ "TryOverrideExceptionHandler",
+ ctypes.default_abi,
+ ctypes.void_t
+ );
+} catch (ex) {}
+
+CrashTestUtils.dumpHasStream = lib.declare(
+ "DumpHasStream",
+ ctypes.default_abi,
+ ctypes.bool,
+ ctypes.char.ptr,
+ ctypes.uint32_t
+);
+
+CrashTestUtils.dumpHasInstructionPointerMemory = lib.declare(
+ "DumpHasInstructionPointerMemory",
+ ctypes.default_abi,
+ ctypes.bool,
+ ctypes.char.ptr
+);
+
+CrashTestUtils.dumpCheckMemory = lib.declare(
+ "DumpCheckMemory",
+ ctypes.default_abi,
+ ctypes.bool,
+ ctypes.char.ptr
+);
+
+CrashTestUtils.getWin64CFITestFnAddrOffset = lib.declare(
+ "GetWin64CFITestFnAddrOffset",
+ ctypes.default_abi,
+ ctypes.int32_t,
+ ctypes.int16_t
+);
diff --git a/toolkit/crashreporter/test/ExceptionThrower.cpp b/toolkit/crashreporter/test/ExceptionThrower.cpp
new file mode 100644
index 0000000000..7404d13c47
--- /dev/null
+++ b/toolkit/crashreporter/test/ExceptionThrower.cpp
@@ -0,0 +1,3 @@
+#include "ExceptionThrower.h"
+
+void ThrowException() { throw 1; }
diff --git a/toolkit/crashreporter/test/ExceptionThrower.h b/toolkit/crashreporter/test/ExceptionThrower.h
new file mode 100644
index 0000000000..e8d30aee4b
--- /dev/null
+++ b/toolkit/crashreporter/test/ExceptionThrower.h
@@ -0,0 +1 @@
+void ThrowException();
diff --git a/toolkit/crashreporter/test/browser/browser.ini b/toolkit/crashreporter/test/browser/browser.ini
new file mode 100644
index 0000000000..9db7711873
--- /dev/null
+++ b/toolkit/crashreporter/test/browser/browser.ini
@@ -0,0 +1,15 @@
+[DEFAULT]
+support-files =
+ head.js
+
+[browser_aboutCrashes.js]
+[browser_aboutCrashesResubmit.js]
+https_first_disabled = true
+[browser_bug471404.js]
+[browser_clearReports.js]
+[browser_cpu_microcode.js]
+skip-if = os != 'win'
+reason = Windows-specific crash annotation
+[browser_sandbox_crash.js]
+skip-if = (os != 'linux') || release_or_beta
+reason = Linux-specific crash type, release/beta builds do not crash on sandbox violations
diff --git a/toolkit/crashreporter/test/browser/browser_aboutCrashes.js b/toolkit/crashreporter/test/browser/browser_aboutCrashes.js
new file mode 100644
index 0000000000..1885be4ca3
--- /dev/null
+++ b/toolkit/crashreporter/test/browser/browser_aboutCrashes.js
@@ -0,0 +1,82 @@
+add_task(async function test() {
+ const appD = make_fake_appdir();
+ const crD = appD.clone();
+ crD.append("Crash Reports");
+ const crashes = add_fake_crashes(crD, 5);
+ // sanity check
+ const appDtest = Services.dirsvc.get("UAppData", Ci.nsIFile);
+ ok(appD.equals(appDtest), "directory service provider registered ok");
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:crashes" },
+ browser => {
+ info("about:crashes loaded");
+ return SpecialPowers.spawn(browser, [crashes], crashes => {
+ const doc = content.document;
+
+ const submitted = doc.getElementById("reportListSubmitted");
+ Assert.ok(
+ !submitted.classList.contains("hidden"),
+ "the submitted crash list is visible"
+ );
+ const unsubmitted = doc.getElementById("reportListUnsubmitted");
+ Assert.ok(
+ unsubmitted.classList.contains("hidden"),
+ "the unsubmitted crash list is hidden"
+ );
+
+ const crashIds = doc.getElementsByClassName("crash-id");
+ Assert.equal(
+ crashIds.length,
+ crashes.length,
+ "about:crashes lists correct number of crash reports"
+ );
+ for (let i = 0; i < crashes.length; i++) {
+ Assert.equal(
+ crashIds[i].textContent,
+ crashes[i].id,
+ i + ": crash ID is correct"
+ );
+ }
+ });
+ }
+ );
+
+ clear_fake_crashes(crD, crashes);
+ const pendingCrash = addPendingCrashreport(crD, Date.now(), { foo: "bar" });
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:crashes" },
+ browser => {
+ info("about:crashes loaded");
+ return SpecialPowers.spawn(browser, [pendingCrash], pendingCrash => {
+ const doc = content.document;
+
+ const submitted = doc.getElementById("reportListSubmitted");
+ Assert.ok(
+ submitted.classList.contains("hidden"),
+ "the submitted crash list is hidden"
+ );
+ const unsubmitted = doc.getElementById("reportListUnsubmitted");
+ Assert.ok(
+ !unsubmitted.classList.contains("hidden"),
+ "the unsubmitted crash list is visible"
+ );
+
+ const crashIds = doc.getElementsByClassName("crash-id");
+ Assert.equal(
+ crashIds.length,
+ 1,
+ "about:crashes lists correct number of crash reports"
+ );
+ const pendingRow = doc.getElementById(pendingCrash.id);
+ Assert.equal(
+ pendingRow.cells[0].textContent,
+ pendingCrash.id,
+ "about:crashes lists pending crash IDs correctly"
+ );
+ });
+ }
+ );
+
+ cleanup_fake_appdir();
+});
diff --git a/toolkit/crashreporter/test/browser/browser_aboutCrashesResubmit.js b/toolkit/crashreporter/test/browser/browser_aboutCrashesResubmit.js
new file mode 100644
index 0000000000..93a21d451d
--- /dev/null
+++ b/toolkit/crashreporter/test/browser/browser_aboutCrashesResubmit.js
@@ -0,0 +1,202 @@
+function cleanup_and_finish() {
+ try {
+ cleanup_fake_appdir();
+ } catch (ex) {}
+ Services.prefs.clearUserPref("breakpad.reportURL");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ finish();
+}
+
+/*
+ * check_crash_list
+ *
+ * Check that the list of crashes displayed by about:crashes matches
+ * the list of crashes that we placed in the pending+submitted directories.
+ *
+ * This function is run in a separate JS context via SpecialPowers.spawn, so
+ * it has no access to other functions or variables in this file.
+ */
+function check_crash_list(crashes) {
+ const doc = content.document;
+ const crashIdNodes = Array.from(doc.getElementsByClassName("crash-id"));
+ const pageCrashIds = new Set(crashIdNodes.map(node => node.textContent));
+ const crashIds = new Set(crashes.map(crash => crash.id));
+ Assert.deepEqual(
+ pageCrashIds,
+ crashIds,
+ "about:crashes lists the correct crash reports."
+ );
+}
+
+/*
+ * check_submit_pending
+ *
+ * Click on a pending crash in about:crashes, wait for it to be submitted (which
+ * should redirect us to the crash report page). Verify that the data provided
+ * by our test crash report server matches the data we submitted.
+ * Additionally, click "back" and verify that the link now points to our new
+ */
+function check_submit_pending(tab, crashes) {
+ const browser = gBrowser.getBrowserForTab(tab);
+ let SubmittedCrash = null;
+ let CrashID = null;
+ let CrashURL = null;
+ function csp_onsuccess() {
+ const crashLinks = content.document.getElementsByClassName("crash-link");
+ // Get the last link since it is appended to the end of the list
+ const link = crashLinks[crashLinks.length - 1];
+ link.click();
+ }
+ function csp_onload() {
+ // loaded the crash report page
+ ok(true, "got submission onload");
+
+ SpecialPowers.spawn(browser, [], function () {
+ // grab the Crash ID here to verify later
+ let CrashID = content.location.search.split("=")[1];
+ let CrashURL = content.location.toString();
+
+ // check the JSON content vs. what we submitted
+ let result = JSON.parse(content.document.documentElement.textContent);
+ Assert.equal(
+ result.upload_file_minidump,
+ "MDMP",
+ "minidump file sent properly"
+ );
+ Assert.equal(
+ result.memory_report,
+ "Let's pretend this is a memory report",
+ "memory report sent properly"
+ );
+ Assert.equal(
+ +result.Throttleable,
+ 0,
+ "correctly sent as non-throttleable"
+ );
+ Assert.equal(
+ result.SubmittedFrom,
+ "AboutCrashes",
+ "correctly flagged as sent from about:crashes"
+ );
+ // we checked these, they're set by the submission process,
+ // so they won't be in the "extra" data.
+ delete result.upload_file_minidump;
+ delete result.memory_report;
+ delete result.Throttleable;
+ delete result.SubmittedFrom;
+
+ return { id: CrashID, url: CrashURL, result };
+ }).then(({ id, url, result }) => {
+ // Likewise, this is discarded before it gets to the server
+ delete SubmittedCrash.extra.ServerURL;
+
+ CrashID = id;
+ CrashURL = url;
+ for (let x in result) {
+ if (x in SubmittedCrash.extra) {
+ is(
+ result[x],
+ SubmittedCrash.extra[x],
+ "submitted value for " + x + " matches expected"
+ );
+ } else {
+ ok(false, "property " + x + " missing from submitted data!");
+ }
+ }
+ for (let y in SubmittedCrash.extra) {
+ if (!(y in result)) {
+ ok(false, "property " + y + " missing from result data!");
+ }
+ }
+
+ // We can listen for pageshow like this because the tab is not remote.
+ BrowserTestUtils.waitForEvent(browser, "pageshow", true).then(
+ csp_pageshow
+ );
+
+ // now navigate back
+ browser.goBack();
+ });
+ }
+ function csp_fail() {
+ browser.removeEventListener("CrashSubmitFailed", csp_fail, true);
+ ok(false, "failed to submit crash report!");
+ cleanup_and_finish();
+ }
+ browser.addEventListener("CrashSubmitSucceeded", csp_onsuccess, true);
+ browser.addEventListener("CrashSubmitFailed", csp_fail, true);
+ BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ url => url !== "about:crashes"
+ ).then(csp_onload);
+ function csp_pageshow() {
+ SpecialPowers.spawn(
+ browser,
+ [{ CrashID, CrashURL }],
+ function ({ CrashID, CrashURL }) {
+ Assert.equal(
+ content.location.href,
+ "about:crashes",
+ "navigated back successfully"
+ );
+ const link = content.document
+ .getElementById(CrashID)
+ .getElementsByClassName("crash-link")[0];
+ Assert.notEqual(link, null, "crash report link changed correctly");
+ if (link) {
+ Assert.equal(
+ link.href,
+ CrashURL,
+ "crash report link points to correct href"
+ );
+ }
+ }
+ ).then(cleanup_and_finish);
+ }
+
+ // try submitting the pending report
+ for (const crash of crashes) {
+ if (crash.pending) {
+ SubmittedCrash = crash;
+ break;
+ }
+ }
+
+ SpecialPowers.spawn(browser, [SubmittedCrash.id], id => {
+ const submitButton = content.document
+ .getElementById(id)
+ .getElementsByClassName("submit-button")[0];
+ submitButton.click();
+ });
+}
+
+function test() {
+ waitForExplicitFinish();
+ const appD = make_fake_appdir();
+ const crD = appD.clone();
+ crD.append("Crash Reports");
+ const crashes = add_fake_crashes(crD, 1);
+ // we don't need much data here, it's not going to a real Socorro
+ crashes.push(
+ addPendingCrashreport(crD, crashes[crashes.length - 1].date + 60000, {
+ ServerURL:
+ "http://example.com/browser/toolkit/crashreporter/test/browser/crashreport.sjs",
+ ProductName: "Test App",
+ Foo: "ABC=XYZ", // test that we don't truncat eat = (bug 512853)
+ })
+ );
+ crashes.sort((a, b) => b.date - a.date);
+
+ // set this pref so we can link to our test server
+ Services.prefs.setCharPref(
+ "breakpad.reportURL",
+ "http://example.com/browser/toolkit/crashreporter/test/browser/crashreport.sjs?id="
+ );
+
+ BrowserTestUtils.openNewForegroundTab(gBrowser, "about:crashes").then(tab => {
+ SpecialPowers.spawn(tab.linkedBrowser, [crashes], check_crash_list).then(
+ () => check_submit_pending(tab, crashes)
+ );
+ });
+}
diff --git a/toolkit/crashreporter/test/browser/browser_bug471404.js b/toolkit/crashreporter/test/browser/browser_bug471404.js
new file mode 100644
index 0000000000..0ab7f19a4d
--- /dev/null
+++ b/toolkit/crashreporter/test/browser/browser_bug471404.js
@@ -0,0 +1,61 @@
+function check_clear_visible(browser, aVisible) {
+ return SpecialPowers.spawn(browser, [aVisible], function (aVisible) {
+ const doc = content.document;
+ let visible = false;
+ const reportListSubmitted = doc.getElementById("reportListSubmitted");
+ if (reportListSubmitted) {
+ const style = doc.defaultView.getComputedStyle(reportListSubmitted);
+ if (style.display !== "none" && style.visibility === "visible") {
+ visible = true;
+ }
+ }
+ Assert.equal(
+ visible,
+ aVisible,
+ "clear submitted reports button is " + (aVisible ? "visible" : "hidden")
+ );
+ });
+}
+
+// each test here has a setup (run before loading about:crashes) and onload (run after about:crashes loads)
+var _tests = [
+ {
+ setup: null,
+ onload(browser) {
+ return check_clear_visible(browser, false);
+ },
+ },
+ {
+ setup(crD) {
+ return add_fake_crashes(crD, 1);
+ },
+ onload(browser) {
+ return check_clear_visible(browser, true);
+ },
+ },
+];
+
+add_task(async function test() {
+ let appD = make_fake_appdir();
+ let crD = appD.clone();
+ crD.append("Crash Reports");
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async function (browser) {
+ for (let test of _tests) {
+ // Run setup before loading about:crashes.
+ if (test.setup) {
+ await test.setup(crD);
+ }
+
+ BrowserTestUtils.loadURIString(browser, "about:crashes");
+ await BrowserTestUtils.browserLoaded(browser).then(() =>
+ test.onload(browser)
+ );
+ }
+ }
+ );
+
+ cleanup_fake_appdir();
+});
diff --git a/toolkit/crashreporter/test/browser/browser_clearReports.js b/toolkit/crashreporter/test/browser/browser_clearReports.js
new file mode 100644
index 0000000000..2f780f1955
--- /dev/null
+++ b/toolkit/crashreporter/test/browser/browser_clearReports.js
@@ -0,0 +1,134 @@
+/* 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/. */
+
+function clickClearReports() {
+ const doc = content.document;
+ const reportListUnsubmitted = doc.getElementById("reportListUnsubmitted");
+ const reportListSubmitted = doc.getElementById("reportListSubmitted");
+ if (!reportListUnsubmitted || !reportListSubmitted) {
+ Assert.ok(false, "Report list not found");
+ }
+
+ const unsubmittedStyle = doc.defaultView.getComputedStyle(
+ reportListUnsubmitted
+ );
+ const submittedStyle = doc.defaultView.getComputedStyle(reportListSubmitted);
+ Assert.notEqual(
+ unsubmittedStyle.display,
+ "none",
+ "Unsubmitted report list is visible"
+ );
+ Assert.notEqual(
+ submittedStyle.display,
+ "none",
+ "Submitted report list is visible"
+ );
+
+ const clearUnsubmittedButton = doc.getElementById("clearUnsubmittedReports");
+ const clearSubmittedButton = doc.getElementById("clearSubmittedReports");
+ clearUnsubmittedButton.click();
+ clearSubmittedButton.click();
+}
+
+var promptShown = false;
+
+var oldPrompt = Services.prompt;
+Services.prompt = {
+ confirm() {
+ promptShown = true;
+ return true;
+ },
+};
+
+registerCleanupFunction(function () {
+ Services.prompt = oldPrompt;
+});
+
+add_task(async function test() {
+ let appD = make_fake_appdir();
+ let crD = appD.clone();
+ crD.append("Crash Reports");
+
+ // Add crashes to submitted dir
+ let submitdir = crD.clone();
+ submitdir.append("submitted");
+
+ let file1 = submitdir.clone();
+ file1.append("bp-nontxt");
+ file1.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
+ let file2 = submitdir.clone();
+ file2.append("nonbp-file.txt");
+ file2.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
+ add_fake_crashes(crD, 5);
+
+ // Add crashes to pending dir
+ let pendingdir = crD.clone();
+ pendingdir.append("pending");
+
+ let crashes = add_fake_crashes(crD, 2);
+ addPendingCrashreport(crD, crashes[0].date);
+ addPendingCrashreport(crD, crashes[1].date);
+
+ // Add crashes to reports dir
+ let report1 = crD.clone();
+ report1.append("NotInstallTime777");
+ report1.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
+ let report2 = crD.clone();
+ report2.append("InstallTime" + Services.appinfo.appBuildID);
+ report2.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
+ let report3 = crD.clone();
+ report3.append("InstallTimeNew");
+ report3.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
+ let report4 = crD.clone();
+ report4.append("InstallTimeOld");
+ report4.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
+ report4.lastModifiedTime = Date.now() - 63172000000;
+
+ registerCleanupFunction(function () {
+ cleanup_fake_appdir();
+ });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:crashes" },
+ async function (browser) {
+ let dirs = [submitdir, pendingdir, crD];
+ let existing = [
+ file1.path,
+ file2.path,
+ report1.path,
+ report2.path,
+ report3.path,
+ submitdir.path,
+ pendingdir.path,
+ ];
+
+ SpecialPowers.spawn(browser, [], clickClearReports);
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ content.document
+ .getElementById("reportListUnsubmitted")
+ .classList.contains("hidden") &&
+ content.document
+ .getElementById("reportListSubmitted")
+ .classList.contains("hidden")
+ );
+
+ for (let dir of dirs) {
+ let entries = dir.directoryEntries;
+ while (entries.hasMoreElements()) {
+ let file = entries.nextFile;
+ let index = existing.indexOf(file.path);
+ isnot(index, -1, file.leafName + " exists");
+
+ if (index != -1) {
+ existing.splice(index, 1);
+ }
+ }
+ }
+
+ is(existing.length, 0, "All the files that should still exist exist");
+ ok(promptShown, "Prompt shown");
+ }
+ );
+});
diff --git a/toolkit/crashreporter/test/browser/browser_cpu_microcode.js b/toolkit/crashreporter/test/browser/browser_cpu_microcode.js
new file mode 100644
index 0000000000..e4efd1c86d
--- /dev/null
+++ b/toolkit/crashreporter/test/browser/browser_cpu_microcode.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* global BrowserTestUtils, ok, gBrowser, add_task */
+
+"use strict";
+
+/**
+ * Checks that we set the CPUMicrocodeVersion annotation.
+ */
+add_task(async function test_cpu_microcode_version_annotation() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ },
+ async function (browser) {
+ // Crash the tab
+ let annotations = await BrowserTestUtils.crashFrame(browser);
+
+ ok(
+ "CPUMicrocodeVersion" in annotations,
+ "contains CPU microcode version"
+ );
+ }
+ );
+});
diff --git a/toolkit/crashreporter/test/browser/browser_sandbox_crash.js b/toolkit/crashreporter/test/browser/browser_sandbox_crash.js
new file mode 100644
index 0000000000..4cd3d782e5
--- /dev/null
+++ b/toolkit/crashreporter/test/browser/browser_sandbox_crash.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* global BrowserTestUtils, ok, gBrowser, add_task */
+
+"use strict";
+
+/**
+ * Checks that we set the CPUMicrocodeVersion annotation.
+ * This is a Windows-specific crash annotation.
+ */
+if (AppConstants.platform == "win") {
+ add_task(async function test_cpu_microcode_version_annotation() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ },
+ async function (browser) {
+ // Crash the tab
+ let annotations = await BrowserTestUtils.crashFrame(browser);
+
+ ok(
+ "CPUMicrocodeVersion" in annotations,
+ "contains CPU microcode version"
+ );
+ }
+ );
+ });
+}
+
+/**
+ * Checks that Linux sandbox violations are reported as SIGSYS crashes with
+ * the syscall number provided in the address field of the minidump's exception
+ * stream.
+ */
+if (AppConstants.platform == "linux") {
+ add_task(async function test_sandbox_violation_is_sigsys() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ },
+ async function (browser) {
+ // Crash the tab
+ let annotations = await BrowserTestUtils.crashFrame(
+ browser,
+ true,
+ true,
+ /* Default browsing context */ null,
+ { crashType: "CRASH_SYSCALL" }
+ );
+
+ Assert.equal(
+ annotations.StackTraces.crash_info.type,
+ "SIGSYS",
+ "The crash type is SIGSYS"
+ );
+
+ function chroot_syscall_number() {
+ // We crash by calling chroot(), see BrowserTestUtilsChild.sys.mjs
+ switch (Services.sysinfo.get("arch")) {
+ case "x86-64":
+ return "0xa1";
+ case "aarch64":
+ return "0x33";
+ default:
+ return "0x3d";
+ }
+ }
+
+ Assert.equal(
+ annotations.StackTraces.crash_info.address,
+ chroot_syscall_number(),
+ "The address corresponds to the chroot() syscall number"
+ );
+ }
+ );
+ });
+}
diff --git a/toolkit/crashreporter/test/browser/crashreport.sjs b/toolkit/crashreporter/test/browser/crashreport.sjs
new file mode 100644
index 0000000000..908455c9e6
--- /dev/null
+++ b/toolkit/crashreporter/test/browser/crashreport.sjs
@@ -0,0 +1,176 @@
+const CC = Components.Constructor;
+
+const BinaryInputStream = CC(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+
+function parseHeaders(data, start) {
+ let headers = {};
+
+ while (true) {
+ let end = data.indexOf("\r\n", start);
+ if (end == -1) {
+ end = data.length;
+ }
+ let line = data.substring(start, end);
+ start = end + 2;
+ if (line == "") {
+ // empty line, we're done
+ break;
+ }
+
+ //XXX: this doesn't handle multi-line headers. do we care?
+ let [name, value] = line.split(":");
+ //XXX: not normalized, should probably use nsHttpHeaders or something
+ headers[name] = value.trimLeft();
+ }
+ return [headers, start];
+}
+
+function parseMultipartForm(request) {
+ let boundary = null;
+ // See if this is a multipart/form-data request, and if so, find the
+ // boundary string
+ if (request.hasHeader("Content-Type")) {
+ var contenttype = request.getHeader("Content-Type");
+ var bits = contenttype.split(";");
+ if (bits[0] == "multipart/form-data") {
+ for (var i = 1; i < bits.length; i++) {
+ var b = bits[i].trimLeft();
+ if (b.indexOf("boundary=") == 0) {
+ // grab everything after boundary=
+ boundary = "--" + b.substring(9);
+ break;
+ }
+ }
+ }
+ }
+ if (boundary == null) {
+ return null;
+ }
+
+ let body = new BinaryInputStream(request.bodyInputStream);
+ let avail;
+ let bytes = [];
+ while ((avail = body.available()) > 0) {
+ let readBytes = body.readByteArray(avail);
+ for (let b of readBytes) {
+ bytes.push(b);
+ }
+ }
+ let data = "";
+ for (let b of bytes) {
+ data += String.fromCharCode(b);
+ }
+ let formData = {};
+ let start = 0;
+ while (true) {
+ // read first line
+ let end = data.indexOf("\r\n", start);
+ if (end == -1) {
+ end = data.length;
+ }
+
+ let line = data.substring(start, end);
+ // look for closing boundary delimiter line
+ if (line == boundary + "--") {
+ break;
+ }
+
+ if (line != boundary) {
+ dump("expected boundary line but didn't find it!");
+ break;
+ }
+
+ // parse headers
+ start = end + 2;
+ let headers = null;
+ [headers, start] = parseHeaders(data, start);
+
+ // find next boundary string
+ end = data.indexOf("\r\n" + boundary, start);
+ if (end == -1) {
+ dump("couldn't find next boundary string\n");
+ break;
+ }
+
+ // read part data, stick in formData using Content-Disposition header
+ let part = data.substring(start, end);
+ start = end + 2;
+
+ if ("Content-Disposition" in headers) {
+ let bits = headers["Content-Disposition"].split(";");
+ if (bits[0] == "form-data") {
+ for (let i = 0; i < bits.length; i++) {
+ let b = bits[i].trimLeft();
+ if (b.indexOf("name=") == 0) {
+ //TODO: handle non-ascii here?
+ let name = b.substring(6, b.length - 1);
+ //TODO: handle multiple-value properties?
+ if (
+ "Content-Type" in headers &&
+ headers["Content-Type"] == "application/json"
+ ) {
+ formData = Object.assign(formData, JSON.parse(part));
+ } else {
+ formData[name] = part;
+ }
+ }
+ //TODO: handle filename= ?
+ //TODO: handle multipart/mixed for multi-file uploads?
+ }
+ }
+ }
+ }
+ return formData;
+}
+
+function handleRequest(request, response) {
+ if (request.method == "GET") {
+ let id = null;
+ for (let p of request.queryString.split("&")) {
+ let [key, value] = p.split("=");
+ if (key == "id") {
+ id = value;
+ }
+ }
+ if (id == null) {
+ response.setStatusLine(request.httpVersion, 400, "Bad Request");
+ response.write("Missing id parameter");
+ } else {
+ let data = getState(id);
+ if (data == "") {
+ response.setStatusLine(request.httpVersion, 404, "Not Found");
+ response.write("Not Found");
+ } else {
+ response.setHeader("Content-Type", "text/plain", false);
+ response.write(data);
+ }
+ }
+ } else if (request.method == "POST") {
+ let formData = parseMultipartForm(request);
+
+ if (formData && "upload_file_minidump" in formData) {
+ response.setHeader("Content-Type", "text/plain", false);
+
+ let uuid = Services.uuid.generateUUID().toString();
+ // ditch the {}, add bp- prefix
+ uuid = "bp-" + uuid.substring(1, uuid.length - 1);
+
+ let d = JSON.stringify(formData);
+ //dump('saving crash report ' + uuid + ': ' + d + '\n');
+ setState(uuid, d);
+
+ response.write("CrashID=" + uuid + "\n");
+ } else {
+ dump("*** crashreport.sjs: Malformed request?\n");
+ response.setStatusLine(request.httpVersion, 400, "Bad Request");
+ response.write("Missing minidump file");
+ }
+ } else {
+ response.setStatusLine(request.httpVersion, 405, "Method not allowed");
+ response.write("Can't handle HTTP method " + request.method);
+ }
+}
diff --git a/toolkit/crashreporter/test/browser/head.js b/toolkit/crashreporter/test/browser/head.js
new file mode 100644
index 0000000000..fea602a028
--- /dev/null
+++ b/toolkit/crashreporter/test/browser/head.js
@@ -0,0 +1,163 @@
+function create_subdir(dir, subdirname) {
+ let subdir = dir.clone();
+ subdir.append(subdirname);
+ if (subdir.exists()) {
+ subdir.remove(true);
+ }
+ subdir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ return subdir;
+}
+
+function generate_uuid() {
+ let uuidGenerator = Services.uuid;
+ let uuid = uuidGenerator.generateUUID().toString();
+ // ditch the {}
+ return uuid.substring(1, uuid.length - 1);
+}
+
+// need to hold on to this to unregister for cleanup
+var _provider = null;
+
+function make_fake_appdir() {
+ // Create a directory inside the profile and register it as UAppData, so
+ // we can stick fake crash reports inside there. We put it inside the profile
+ // just because we know that will get cleaned up after the mochitest run.
+ let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ // create a subdir just to keep our files out of the way
+ let appD = create_subdir(profD, "UAppData");
+
+ let crashesDir = create_subdir(appD, "Crash Reports");
+ create_subdir(crashesDir, "pending");
+ create_subdir(crashesDir, "submitted");
+
+ _provider = {
+ getFile(prop, persistent) {
+ persistent.value = true;
+ if (prop == "UAppData") {
+ return appD.clone();
+ }
+ // Depending on timing we can get requests for other files.
+ // When we threw an exception here, in the world before bug 997440, this got lost
+ // because of the arbitrary JSContext being used in nsXPCWrappedJS::CallMethod.
+ // After bug 997440 this gets reported to our window and causes the tests to fail.
+ // So, we'll just dump out a message to the logs.
+ dump(
+ "WARNING: make_fake_appdir - fake nsIDirectoryServiceProvider - Unexpected getFile for: '" +
+ prop +
+ "'\n"
+ );
+ return null;
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIDirectoryServiceProvider"]),
+ };
+ // register our new provider
+ Services.dirsvc
+ .QueryInterface(Ci.nsIDirectoryService)
+ .registerProvider(_provider);
+ // and undefine the old value
+ try {
+ Services.dirsvc.undefine("UAppData");
+ } catch (ex) {} // it's ok if this fails, the value might not be cached yet
+ return appD.clone();
+}
+
+function cleanup_fake_appdir() {
+ Services.dirsvc
+ .QueryInterface(Ci.nsIDirectoryService)
+ .unregisterProvider(_provider);
+ // undefine our value so future calls get the real value
+ try {
+ Services.dirsvc.undefine("UAppData");
+ } catch (ex) {
+ dump("cleanup_fake_appdir: dirSvc.undefine failed: " + ex.message + "\n");
+ }
+}
+
+function add_fake_crashes(crD, count) {
+ let results = [];
+ let submitdir = crD.clone();
+ submitdir.append("submitted");
+ // create them from oldest to newest, to ensure that about:crashes
+ // displays them in the correct order
+ let date = Date.now() - count * 60000;
+ for (let i = 0; i < count; i++) {
+ let uuid = "bp-" + generate_uuid();
+ let fn = uuid + ".txt";
+ let file = submitdir.clone();
+ file.append(fn);
+ file.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
+ file.lastModifiedTime = date;
+ results.push({ id: uuid, date, pending: false });
+
+ date += 60000;
+ }
+ // we want them sorted newest to oldest, since that's the order
+ // that about:crashes lists them in
+ results.sort((a, b) => b.date - a.date);
+ return results;
+}
+
+function clear_fake_crashes(crD, crashes) {
+ let submitdir = crD.clone();
+ submitdir.append("submitted");
+ for (let i of crashes) {
+ let fn = i.id + ".txt";
+ let file = submitdir.clone();
+ file.append(fn);
+ file.remove(false);
+ }
+}
+
+function writeDataToFile(file, data) {
+ var fstream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(
+ Ci.nsIFileOutputStream
+ );
+ // open, write, truncate
+ fstream.init(file, -1, -1, 0);
+ var os = Cc["@mozilla.org/intl/converter-output-stream;1"].createInstance(
+ Ci.nsIConverterOutputStream
+ );
+ os.init(fstream, "UTF-8");
+ os.writeString(data);
+ os.close();
+ fstream.close();
+}
+
+function writeCrashReportFile(dir, uuid, suffix, date, data) {
+ let file = dir.clone();
+ file.append(uuid + suffix);
+ writeDataToFile(file, data);
+ file.lastModifiedTime = date;
+}
+
+function writeMinidumpFile(dir, uuid, date) {
+ // that's the start of a valid minidump, anyway
+ writeCrashReportFile(dir, uuid, ".dmp", date, "MDMP");
+}
+
+function writeExtraFile(dir, uuid, date, data) {
+ writeCrashReportFile(dir, uuid, ".extra", date, JSON.stringify(data));
+}
+
+function writeMemoryReport(dir, uuid, date) {
+ let data = "Let's pretend this is a memory report";
+ writeCrashReportFile(dir, uuid, ".memory.json.gz", date, data);
+}
+
+function addPendingCrashreport(crD, date, extra) {
+ let pendingdir = crD.clone();
+ pendingdir.append("pending");
+ let uuid = generate_uuid();
+ writeMinidumpFile(pendingdir, uuid, date);
+ writeExtraFile(pendingdir, uuid, date, extra);
+ writeMemoryReport(pendingdir, uuid, date);
+ return { id: uuid, date, pending: true, extra };
+}
+
+function addIncompletePendingCrashreport(crD, date) {
+ let pendingdir = crD.clone();
+ pendingdir.append("pending");
+ let uuid = generate_uuid();
+ writeMinidumpFile(pendingdir, uuid, date);
+ return { id: uuid, date, pending: true };
+}
diff --git a/toolkit/crashreporter/test/dumputils.cpp b/toolkit/crashreporter/test/dumputils.cpp
new file mode 100644
index 0000000000..fedaa6532a
--- /dev/null
+++ b/toolkit/crashreporter/test/dumputils.cpp
@@ -0,0 +1,83 @@
+#include <stdio.h>
+
+#include "google_breakpad/processor/minidump.h"
+#include "nscore.h"
+
+using namespace google_breakpad;
+
+// Return true if the specified minidump contains a stream of |stream_type|.
+extern "C" NS_EXPORT bool DumpHasStream(const char* dump_file,
+ uint32_t stream_type) {
+ Minidump dump(dump_file);
+ if (!dump.Read()) return false;
+
+ uint32_t length;
+ if (!dump.SeekToStreamType(stream_type, &length) || length == 0) return false;
+
+ return true;
+}
+
+// Return true if the specified minidump contains a memory region
+// that contains the instruction pointer from the exception record.
+extern "C" NS_EXPORT bool DumpHasInstructionPointerMemory(
+ const char* dump_file) {
+ Minidump minidump(dump_file);
+ if (!minidump.Read()) return false;
+
+ MinidumpException* exception = minidump.GetException();
+ MinidumpMemoryList* memory_list = minidump.GetMemoryList();
+ if (!exception || !memory_list) {
+ return false;
+ }
+
+ MinidumpContext* context = exception->GetContext();
+ if (!context) return false;
+
+ uint64_t instruction_pointer;
+ if (!context->GetInstructionPointer(&instruction_pointer)) {
+ return false;
+ }
+
+ MinidumpMemoryRegion* region =
+ memory_list->GetMemoryRegionForAddress(instruction_pointer);
+ return region != nullptr;
+}
+
+// This function tests for a very specific condition. It finds
+// an address in a file, "crash-addr", in the CWD. It checks
+// that the minidump has a memory region starting at that
+// address. The region must be 32 bytes long and contain the
+// values 0 to 31 as bytes, in ascending order.
+extern "C" NS_EXPORT bool DumpCheckMemory(const char* dump_file) {
+ Minidump dump(dump_file);
+ if (!dump.Read()) return false;
+
+ MinidumpMemoryList* memory_list = dump.GetMemoryList();
+ if (!memory_list) {
+ return false;
+ }
+
+ void* addr;
+ FILE* fp = fopen("crash-addr", "r");
+ if (!fp) return false;
+ if (fscanf(fp, "%p", &addr) != 1) {
+ fclose(fp);
+ return false;
+ }
+ fclose(fp);
+
+ remove("crash-addr");
+
+ MinidumpMemoryRegion* region =
+ memory_list->GetMemoryRegionForAddress(uint64_t(addr));
+ if (!region) return false;
+
+ const uint8_t* chars = region->GetMemory();
+ if (region->GetSize() != 32) return false;
+
+ for (int i = 0; i < 32; i++) {
+ if (chars[i] != i) return false;
+ }
+
+ return true;
+}
diff --git a/toolkit/crashreporter/test/moz.build b/toolkit/crashreporter/test/moz.build
new file mode 100644
index 0000000000..70678af74a
--- /dev/null
+++ b/toolkit/crashreporter/test/moz.build
@@ -0,0 +1,67 @@
+# -*- 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/.
+FINAL_TARGET = "_tests/xpcshell/toolkit/crashreporter/test"
+
+XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell.ini", "unit_ipc/xpcshell.ini"]
+if CONFIG["MOZ_PHC"]:
+ XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell-phc.ini", "unit_ipc/xpcshell-phc.ini"]
+
+BROWSER_CHROME_MANIFESTS += ["browser/browser.ini"]
+
+UNIFIED_SOURCES += [
+ "../google-breakpad/src/processor/basic_code_modules.cc",
+ "../google-breakpad/src/processor/convert_old_arm64_context.cc",
+ "../google-breakpad/src/processor/dump_context.cc",
+ "../google-breakpad/src/processor/dump_object.cc",
+ "../google-breakpad/src/processor/logging.cc",
+ "../google-breakpad/src/processor/minidump.cc",
+ "../google-breakpad/src/processor/pathname_stripper.cc",
+ "../google-breakpad/src/processor/proc_maps_linux.cc",
+ "dumputils.cpp",
+ "nsTestCrasher.cpp",
+]
+
+SOURCES += [
+ "ExceptionThrower.cpp",
+]
+
+if CONFIG["OS_TARGET"] == "WINNT" and CONFIG["CPU_ARCH"] == "x86_64":
+ if CONFIG["CC_TYPE"] not in ("gcc", "clang"):
+ SOURCES += [
+ "win64UnwindInfoTests.asm",
+ ]
+
+if CONFIG["CC_TYPE"] == "clang-cl":
+ SOURCES["ExceptionThrower.cpp"].flags += [
+ "-Xclang",
+ "-fcxx-exceptions",
+ ]
+else:
+ SOURCES["ExceptionThrower.cpp"].flags += [
+ "-fexceptions",
+ ]
+
+if CONFIG["MOZ_PHC"]:
+ DEFINES["MOZ_PHC"] = True
+
+GeckoSharedLibrary("testcrasher")
+
+DEFINES["SHARED_LIBRARY"] = "%s%s%s" % (
+ CONFIG["DLL_PREFIX"],
+ LIBRARY_NAME,
+ CONFIG["DLL_SUFFIX"],
+)
+
+TEST_HARNESS_FILES.xpcshell.toolkit.crashreporter.test.unit += [
+ "CrashTestUtils.sys.mjs"
+]
+TEST_HARNESS_FILES.xpcshell.toolkit.crashreporter.test.unit_ipc += [
+ "CrashTestUtils.sys.mjs"
+]
+
+include("/toolkit/crashreporter/crashreporter.mozbuild")
+
+NO_PGO = True
diff --git a/toolkit/crashreporter/test/nsTestCrasher.cpp b/toolkit/crashreporter/test/nsTestCrasher.cpp
new file mode 100644
index 0000000000..d8de2ed63e
--- /dev/null
+++ b/toolkit/crashreporter/test/nsTestCrasher.cpp
@@ -0,0 +1,364 @@
+#include "mozilla/Assertions.h"
+
+#include <stdio.h>
+#include <map>
+
+#include "nscore.h"
+#include "mozilla/Unused.h"
+#include "ExceptionThrower.h"
+
+#ifdef XP_WIN
+# include <malloc.h>
+# include <windows.h>
+#elif defined(XP_MACOSX)
+# include <sys/types.h>
+# include <sys/fcntl.h>
+# include <unistd.h>
+# include <dlfcn.h> // For dlsym()
+// See https://github.com/apple/darwin-xnu/blob/main/bsd/sys/guarded.h
+# define GUARD_CLOSE (1u << 0)
+# define GUARD_DUP (1u << 1)
+# define GUARD_SOCKET_IPC (1u << 2)
+# define GUARD_FILEPORT (1u << 3)
+# define GUARD_WRITE (1u << 4)
+typedef uint64_t guardid_t;
+typedef int (*guarded_open_np_t)(const char*, const guardid_t*, u_int, int,
+ ...);
+#endif
+
+#ifndef XP_WIN
+# include <pthread.h>
+#endif
+
+#ifdef MOZ_PHC
+# include "replace_malloc_bridge.h"
+#endif
+
+/*
+ * This pure virtual call example is from MSDN
+ */
+class A;
+
+void fcn(A*);
+
+class A {
+ public:
+ virtual void f() = 0;
+ A() { fcn(this); }
+};
+
+class B : A {
+ void f() override {}
+
+ public:
+ void use() {}
+};
+
+void fcn(A* p) { p->f(); }
+
+void PureVirtualCall() {
+ // generates a pure virtual function call
+ B b;
+ b.use(); // make sure b's actually used
+}
+
+extern "C" {
+#if XP_WIN && HAVE_64BIT_BUILD && defined(_M_X64) && !defined(__MINGW32__)
+// Implementation in win64unwindInfoTests.asm
+uint64_t x64CrashCFITest_NO_MANS_LAND(uint64_t returnpfn, void*);
+uint64_t x64CrashCFITest_Launcher(uint64_t returnpfn, void* testProc);
+uint64_t x64CrashCFITest_UnknownOpcode(uint64_t returnpfn, void*);
+uint64_t x64CrashCFITest_PUSH_NONVOL(uint64_t returnpfn, void*);
+uint64_t x64CrashCFITest_ALLOC_SMALL(uint64_t returnpfn, void*);
+uint64_t x64CrashCFITest_ALLOC_LARGE(uint64_t returnpfn, void*);
+uint64_t x64CrashCFITest_SAVE_NONVOL(uint64_t returnpfn, void*);
+uint64_t x64CrashCFITest_SAVE_NONVOL_FAR(uint64_t returnpfn, void*);
+uint64_t x64CrashCFITest_SAVE_XMM128(uint64_t returnpfn, void*);
+uint64_t x64CrashCFITest_SAVE_XMM128_FAR(uint64_t returnpfn, void*);
+uint64_t x64CrashCFITest_EPILOG(uint64_t returnpfn, void*);
+uint64_t x64CrashCFITest_EOF(uint64_t returnpfn, void*);
+#endif // XP_WIN && HAVE_64BIT_BUILD && !defined(__MINGW32__)
+}
+
+// Keep these in sync with CrashTestUtils.jsm!
+const int16_t CRASH_INVALID_POINTER_DEREF = 0;
+const int16_t CRASH_PURE_VIRTUAL_CALL = 1;
+const int16_t CRASH_OOM = 3;
+const int16_t CRASH_MOZ_CRASH = 4;
+const int16_t CRASH_ABORT = 5;
+const int16_t CRASH_UNCAUGHT_EXCEPTION = 6;
+#if XP_WIN && HAVE_64BIT_BUILD && defined(_M_X64) && !defined(__MINGW32__)
+const int16_t CRASH_X64CFI_NO_MANS_LAND = 7;
+const int16_t CRASH_X64CFI_LAUNCHER = 8;
+const int16_t CRASH_X64CFI_UNKNOWN_OPCODE = 9;
+const int16_t CRASH_X64CFI_PUSH_NONVOL = 10;
+const int16_t CRASH_X64CFI_ALLOC_SMALL = 11;
+const int16_t CRASH_X64CFI_ALLOC_LARGE = 12;
+const int16_t CRASH_X64CFI_SAVE_NONVOL = 15;
+const int16_t CRASH_X64CFI_SAVE_NONVOL_FAR = 16;
+const int16_t CRASH_X64CFI_SAVE_XMM128 = 17;
+const int16_t CRASH_X64CFI_SAVE_XMM128_FAR = 18;
+const int16_t CRASH_X64CFI_EPILOG = 19;
+const int16_t CRASH_X64CFI_EOF = 20;
+#endif
+const int16_t CRASH_PHC_USE_AFTER_FREE = 21;
+const int16_t CRASH_PHC_DOUBLE_FREE = 22;
+const int16_t CRASH_PHC_BOUNDS_VIOLATION = 23;
+#if XP_WIN
+const int16_t CRASH_HEAP_CORRUPTION = 24;
+#endif
+#ifdef XP_MACOSX
+const int16_t CRASH_EXC_GUARD = 25;
+#endif
+#ifndef XP_WIN
+const int16_t CRASH_STACK_OVERFLOW = 26;
+#endif
+
+#if XP_WIN && HAVE_64BIT_BUILD && defined(_M_X64) && !defined(__MINGW32__)
+
+typedef decltype(&x64CrashCFITest_UnknownOpcode) win64CFITestFnPtr_t;
+
+static std::map<int16_t, win64CFITestFnPtr_t> GetWin64CFITestMap() {
+ std::map<int16_t, win64CFITestFnPtr_t> ret = {
+ {CRASH_X64CFI_NO_MANS_LAND, x64CrashCFITest_NO_MANS_LAND},
+ {CRASH_X64CFI_LAUNCHER, x64CrashCFITest_Launcher},
+ {CRASH_X64CFI_UNKNOWN_OPCODE, x64CrashCFITest_UnknownOpcode},
+ {CRASH_X64CFI_PUSH_NONVOL, x64CrashCFITest_PUSH_NONVOL},
+ {CRASH_X64CFI_ALLOC_SMALL, x64CrashCFITest_ALLOC_SMALL},
+ {CRASH_X64CFI_ALLOC_LARGE, x64CrashCFITest_ALLOC_LARGE},
+ {CRASH_X64CFI_SAVE_NONVOL, x64CrashCFITest_SAVE_NONVOL},
+ {CRASH_X64CFI_SAVE_NONVOL_FAR, x64CrashCFITest_SAVE_NONVOL_FAR},
+ {CRASH_X64CFI_SAVE_XMM128, x64CrashCFITest_SAVE_XMM128},
+ {CRASH_X64CFI_SAVE_XMM128_FAR, x64CrashCFITest_SAVE_XMM128_FAR},
+ {CRASH_X64CFI_EPILOG, x64CrashCFITest_EPILOG},
+ {CRASH_X64CFI_EOF, x64CrashCFITest_EOF}};
+ // ret values point to jump table entries, not the actual function bodies.
+ // Get the correct pointer by calling the function with returnpfn=1
+ for (auto it = ret.begin(); it != ret.end(); ++it) {
+ it->second = (win64CFITestFnPtr_t)it->second(1, nullptr);
+ }
+ return ret;
+}
+
+// This ensures tests have enough committed stack space.
+// Must not be inlined, or the stack space would not be freed for the caller
+// to use.
+void MOZ_NEVER_INLINE ReserveStack() {
+ // We must actually use the memory in some way that the compiler can't
+ // optimize away.
+ static const size_t elements = (1024000 / sizeof(FILETIME)) + 1;
+ FILETIME stackmem[elements];
+ ::GetSystemTimeAsFileTime(&stackmem[0]);
+ ::GetSystemTimeAsFileTime(&stackmem[elements - 1]);
+}
+
+#endif // XP_WIN && HAVE_64BIT_BUILD
+
+#ifdef MOZ_PHC
+uint8_t* GetPHCAllocation(size_t aSize) {
+ // A crude but effective way to get a PHC allocation.
+ for (int i = 0; i < 2000000; i++) {
+ uint8_t* p = (uint8_t*)malloc(aSize);
+ if (ReplaceMalloc::IsPHCAllocation(p, nullptr)) {
+ return p;
+ }
+ free(p);
+ }
+ // This failure doesn't seem to occur in practice...
+ MOZ_CRASH("failed to get a PHC allocation");
+}
+#endif
+
+#ifndef XP_WIN
+static int64_t recurse(int64_t aRandom) {
+ char buff[256] = {};
+ int64_t result = aRandom;
+
+ strncpy(buff, "This is gibberish", sizeof(buff));
+
+ for (auto& c : buff) {
+ result += c;
+ }
+
+ if (result == 0) {
+ return result;
+ }
+
+ return recurse(result) + 1;
+}
+
+static void* overflow_stack(void* aInput) {
+ int64_t result = recurse(*((int64_t*)(aInput)));
+
+ return (void*)result;
+}
+#endif // XP_WIN
+
+extern "C" NS_EXPORT void Crash(int16_t how) {
+ switch (how) {
+ case CRASH_INVALID_POINTER_DEREF: {
+ volatile int* foo = (int*)0x42;
+ *foo = 0;
+ // not reached
+ break;
+ }
+ case CRASH_PURE_VIRTUAL_CALL: {
+ PureVirtualCall();
+ // not reached
+ break;
+ }
+ case CRASH_OOM: {
+ mozilla::Unused << moz_xmalloc((size_t)-1);
+ mozilla::Unused << moz_xmalloc((size_t)-1);
+ mozilla::Unused << moz_xmalloc((size_t)-1);
+ break;
+ }
+ case CRASH_MOZ_CRASH: {
+ MOZ_CRASH();
+ break;
+ }
+ case CRASH_ABORT: {
+ abort();
+ break;
+ }
+ case CRASH_UNCAUGHT_EXCEPTION: {
+ ThrowException();
+ break;
+ }
+#if XP_WIN && HAVE_64BIT_BUILD && defined(_M_X64) && !defined(__MINGW32__)
+ case CRASH_X64CFI_UNKNOWN_OPCODE:
+ case CRASH_X64CFI_PUSH_NONVOL:
+ case CRASH_X64CFI_ALLOC_SMALL:
+ case CRASH_X64CFI_ALLOC_LARGE:
+ case CRASH_X64CFI_SAVE_NONVOL:
+ case CRASH_X64CFI_SAVE_NONVOL_FAR:
+ case CRASH_X64CFI_SAVE_XMM128:
+ case CRASH_X64CFI_SAVE_XMM128_FAR:
+ case CRASH_X64CFI_EPILOG: {
+ auto m = GetWin64CFITestMap();
+ if (m.find(how) == m.end()) {
+ break;
+ }
+ auto pfnTest = m[how];
+ auto pfnLauncher = m[CRASH_X64CFI_LAUNCHER];
+ ReserveStack();
+ pfnLauncher(0, reinterpret_cast<void*>(pfnTest));
+ break;
+ }
+#endif // XP_WIN && HAVE_64BIT_BUILD && !defined(__MINGW32__)
+#ifdef MOZ_PHC
+ case CRASH_PHC_USE_AFTER_FREE: {
+ // Do a UAF, triggering a crash.
+ uint8_t* p = GetPHCAllocation(32);
+ free(p);
+ p[0] = 0;
+ // not reached
+ }
+ case CRASH_PHC_DOUBLE_FREE: {
+ // Do a double free, triggering a crash.
+ uint8_t* p = GetPHCAllocation(64);
+ free(p);
+ free(p);
+ // not reached
+ }
+ case CRASH_PHC_BOUNDS_VIOLATION: {
+ // Do a bounds violation, triggering a crash.
+ uint8_t* p = GetPHCAllocation(96);
+ p[96] = 0;
+ // not reached
+ }
+#endif
+#if XP_WIN
+ case CRASH_HEAP_CORRUPTION: {
+ // We override the HeapFree() function in mozglue so that we can force
+ // the code calling it to use our allocator instead of the Windows one.
+ // Since we need to call the real HeapFree() we get its pointer directly.
+ HMODULE kernel32 = LoadLibraryW(L"Kernel32.dll");
+ if (kernel32) {
+ typedef BOOL (*HeapFreeT)(HANDLE, DWORD, LPVOID);
+ HeapFreeT heapFree = (HeapFreeT)GetProcAddress(kernel32, "HeapFree");
+ if (heapFree) {
+ HANDLE heap = GetProcessHeap();
+ LPVOID badPointer = (LPVOID)3;
+ heapFree(heap, 0, badPointer);
+ break; // This should be unreachable
+ }
+ }
+ }
+#endif // XP_WIN
+#ifdef XP_MACOSX
+ case CRASH_EXC_GUARD: {
+ guarded_open_np_t dl_guarded_open_np;
+ void* kernellib =
+ (void*)dlopen("/usr/lib/system/libsystem_kernel.dylib", RTLD_GLOBAL);
+ dl_guarded_open_np =
+ (guarded_open_np_t)dlsym(kernellib, "guarded_open_np");
+ const guardid_t guard = 0x123456789ABCDEFULL;
+ // Guard the file descriptor against regular close() calls
+ int fd = dl_guarded_open_np(
+ "/tmp/try.txt", &guard,
+ GUARD_CLOSE | GUARD_DUP | GUARD_SOCKET_IPC | GUARD_FILEPORT,
+ O_CREAT | O_CLOEXEC | O_RDWR, 0666);
+
+ if (fd != -1) {
+ close(fd);
+ // not reached
+ }
+ }
+#endif // XP_MACOSX
+#ifndef XP_WIN
+ case CRASH_STACK_OVERFLOW: {
+ pthread_t thread_id;
+ int64_t data = 1337;
+ int rv = pthread_create(&thread_id, nullptr, overflow_stack, &data);
+ if (!rv) {
+ pthread_join(thread_id, nullptr);
+ }
+
+ break; // This should be unreachable
+ }
+#endif // XP_WIN
+ default:
+ break;
+ }
+}
+
+char testData[32];
+
+extern "C" NS_EXPORT uint64_t SaveAppMemory() {
+ for (size_t i = 0; i < sizeof(testData); i++) testData[i] = i;
+
+ FILE* fp = fopen("crash-addr", "w");
+ if (!fp) return 0;
+ fprintf(fp, "%p\n", (void*)testData);
+ fclose(fp);
+
+ return (int64_t)testData;
+}
+
+#ifdef XP_WIN
+static LONG WINAPI HandleException(EXCEPTION_POINTERS* exinfo) {
+ TerminateProcess(GetCurrentProcess(), 0);
+ return 0;
+}
+
+extern "C" NS_EXPORT void TryOverrideExceptionHandler() {
+ SetUnhandledExceptionFilter(HandleException);
+}
+#endif
+
+extern "C" NS_EXPORT uint32_t GetWin64CFITestFnAddrOffset(int16_t fnid) {
+#if XP_WIN && HAVE_64BIT_BUILD && defined(_M_X64) && !defined(__MINGW32__)
+ // fnid uses the same constants as Crash().
+ // Returns the RVA of the requested function.
+ // Returns 0 on failure.
+ auto m = GetWin64CFITestMap();
+ if (m.find(fnid) == m.end()) {
+ return 0;
+ }
+ uint64_t moduleBase = (uint64_t)GetModuleHandleW(L"testcrasher.dll");
+ return ((uint64_t)m[fnid]) - moduleBase;
+#else
+ return 0;
+#endif // XP_WIN && HAVE_64BIT_BUILD && !defined(__MINGW32__)
+}
diff --git a/toolkit/crashreporter/test/unit/crasher_subprocess_head.js b/toolkit/crashreporter/test/unit/crasher_subprocess_head.js
new file mode 100644
index 0000000000..19525bb605
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/crasher_subprocess_head.js
@@ -0,0 +1,29 @@
+// enable crash reporting first
+var cwd = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
+
+// get the temp dir
+var _tmpd = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+_tmpd.initWithPath(Services.env.get("XPCSHELL_TEST_TEMP_DIR"));
+
+// Allow `crashReporter` to be used as an alias in the tests.
+var crashReporter = Services.appinfo;
+
+// We need to call this or crash events go in an undefined location.
+Services.appinfo.UpdateCrashEventsDir();
+
+// Setting the minidump path is not allowed in content processes
+var processType = Services.appinfo.processType;
+if (processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
+ Services.appinfo.minidumpPath = _tmpd;
+}
+
+var protocolHandler = Services.io
+ .getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+var curDirURI = Services.io.newFileURI(cwd);
+protocolHandler.setSubstitution("test", curDirURI);
+const { CrashTestUtils } = ChromeUtils.importESModule(
+ "resource://test/CrashTestUtils.sys.mjs"
+);
+var crashType = CrashTestUtils.CRASH_INVALID_POINTER_DEREF;
+var shouldDelay = false;
diff --git a/toolkit/crashreporter/test/unit/crasher_subprocess_tail.js b/toolkit/crashreporter/test/unit/crasher_subprocess_tail.js
new file mode 100644
index 0000000000..c9e415b567
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/crasher_subprocess_tail.js
@@ -0,0 +1,20 @@
+/* import-globals-from crasher_subprocess_head.js */
+
+// Let the event loop process a bit before crashing.
+if (shouldDelay) {
+ let shouldCrashNow = false;
+
+ Services.tm.dispatchToMainThread({
+ run: () => {
+ shouldCrashNow = true;
+ },
+ });
+
+ Services.tm.spinEventLoopUntil(
+ "Test(crasher_subprocess_tail.js:shouldDelay)",
+ () => shouldCrashNow
+ );
+}
+
+// now actually crash
+CrashTestUtils.crash(crashType);
diff --git a/toolkit/crashreporter/test/unit/head_crashreporter.js b/toolkit/crashreporter/test/unit/head_crashreporter.js
new file mode 100644
index 0000000000..c37c8acf8c
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/head_crashreporter.js
@@ -0,0 +1,347 @@
+const { makeFakeAppDir } = ChromeUtils.importESModule(
+ "resource://testing-common/AppData.sys.mjs"
+);
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+function getEventDir() {
+ return PathUtils.join(do_get_tempdir().path, "crash-events");
+}
+
+function sendCommandAsync(command) {
+ return new Promise(resolve => {
+ sendCommand(command, resolve);
+ });
+}
+
+/*
+ * Run an xpcshell subprocess and crash it.
+ *
+ * @param setup
+ * A string of JavaScript code to execute in the subprocess
+ * before crashing. If this is a function and not a string,
+ * it will have .toSource() called on it, and turned into
+ * a call to itself. (for programmer convenience)
+ * This code will be evaluted between crasher_subprocess_head.js
+ * and crasher_subprocess_tail.js, so it will have access
+ * to everything defined in crasher_subprocess_head.js,
+ * which includes "crashReporter", a variable holding
+ * the crash reporter service.
+ *
+ * @param callback
+ * A JavaScript function to be called after the subprocess
+ * crashes. It will be passed (minidump, extra, extrafile), where
+ * - minidump is an nsIFile of the minidump file produced,
+ * - extra is an object containing the key,value pairs from
+ * the .extra file.
+ * - extrafile is an nsIFile of the extra file
+ *
+ * @param canReturnZero
+ * If true, the subprocess may return with a zero exit code.
+ * Certain types of crashes may not cause the process to
+ * exit with an error.
+ *
+ */
+async function do_crash(setup, callback, canReturnZero) {
+ // get current process filename (xpcshell)
+ let bin = Services.dirsvc.get("XREExeF", Ci.nsIFile);
+ if (!bin.exists()) {
+ // weird, can't find xpcshell binary?
+ do_throw("Can't find xpcshell binary!");
+ }
+ // get Gre dir (GreD)
+ let greD = Services.dirsvc.get("GreD", Ci.nsIFile);
+ let headfile = do_get_file("crasher_subprocess_head.js");
+ let tailfile = do_get_file("crasher_subprocess_tail.js");
+ // run xpcshell -g GreD -f head -e "some setup code" -f tail
+ let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
+ process.init(bin);
+ let args = ["-g", greD.path, "-f", headfile.path];
+ if (setup) {
+ if (typeof setup == "function") {
+ // funky, but convenient
+ setup = "(" + setup.toSource() + ")();";
+ }
+ args.push("-e", setup);
+ }
+ args.push("-f", tailfile.path);
+
+ let crashD = do_get_tempdir();
+ crashD.append("crash-events");
+ if (!crashD.exists()) {
+ crashD.create(crashD.DIRECTORY_TYPE, 0o700);
+ }
+
+ Services.env.set("CRASHES_EVENTS_DIR", crashD.path);
+
+ try {
+ process.run(true, args, args.length);
+ } catch (ex) {
+ // on Windows we exit with a -1 status when crashing.
+ } finally {
+ Services.env.set("CRASHES_EVENTS_DIR", "");
+ }
+
+ if (!canReturnZero) {
+ // should exit with an error (should have crashed)
+ Assert.notEqual(process.exitValue, 0);
+ }
+
+ await handleMinidump(callback);
+}
+
+function getMinidump() {
+ let en = do_get_tempdir().directoryEntries;
+ while (en.hasMoreElements()) {
+ let f = en.nextFile;
+ if (f.leafName.substr(-4) == ".dmp") {
+ return f;
+ }
+ }
+
+ return null;
+}
+
+function getMinidumpAnalyzerPath() {
+ const binSuffix = AppConstants.platform === "win" ? ".exe" : "";
+ const exeName = "minidump-analyzer" + binSuffix;
+
+ let exe = Services.dirsvc.get("GreBinD", Ci.nsIFile);
+ exe.append(exeName);
+
+ return exe;
+}
+
+function runMinidumpAnalyzer(dumpFile, additionalArgs) {
+ let bin = getMinidumpAnalyzerPath();
+ let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
+ process.init(bin);
+ let args = [];
+ if (additionalArgs) {
+ args = args.concat(additionalArgs);
+ }
+ args.push(dumpFile.path);
+ process.run(true /* blocking */, args, args.length);
+}
+
+async function handleMinidump(callback) {
+ // find minidump
+ let minidump = getMinidump();
+
+ if (minidump == null) {
+ do_throw("No minidump found!");
+ }
+
+ let extrafile = minidump.clone();
+ extrafile.leafName = extrafile.leafName.slice(0, -4) + ".extra";
+
+ let memoryfile = minidump.clone();
+ memoryfile.leafName = memoryfile.leafName.slice(0, -4) + ".memory.json.gz";
+
+ let cleanup = async function () {
+ for (let file of [minidump, extrafile, memoryfile]) {
+ while (file.exists()) {
+ try {
+ file.remove(false);
+ } catch (e) {
+ // On Windows the file may be locked, wait briefly and try again
+ await new Promise(resolve => do_timeout(50, resolve));
+ }
+ }
+ }
+ };
+
+ // Just in case, don't let these files linger.
+ registerCleanupFunction(cleanup);
+
+ Assert.ok(extrafile.exists());
+ let extra = await IOUtils.readJSON(extrafile.path);
+
+ if (callback) {
+ await callback(minidump, extra, extrafile, memoryfile);
+ }
+
+ await cleanup();
+}
+
+function spinEventLoop() {
+ return new Promise(resolve => {
+ executeSoon(resolve);
+ });
+}
+
+/**
+ * Helper for testing a content process crash.
+ *
+ * This variant accepts a setup function which runs in the content process
+ * to set data as needed _before_ the crash. The tail file triggers a generic
+ * crash after setup.
+ */
+async function do_content_crash(setup, callback) {
+ do_load_child_test_harness();
+
+ // Setting the minidump path won't work in the child, so we need to do
+ // that here.
+ Services.appinfo.minidumpPath = do_get_tempdir();
+
+ /* import-globals-from ../unit/crasher_subprocess_head.js */
+ /* import-globals-from ../unit/crasher_subprocess_tail.js */
+
+ let headfile = do_get_file("../unit/crasher_subprocess_head.js");
+ let tailfile = do_get_file("../unit/crasher_subprocess_tail.js");
+ if (setup) {
+ if (typeof setup == "function") {
+ // funky, but convenient
+ setup = "(" + setup.toSource() + ")();";
+ }
+ }
+
+ do_get_profile();
+ await makeFakeAppDir();
+ await sendCommandAsync('load("' + headfile.path.replace(/\\/g, "/") + '");');
+ if (setup) {
+ await sendCommandAsync(setup);
+ }
+ await sendCommandAsync('load("' + tailfile.path.replace(/\\/g, "/") + '");');
+ await spinEventLoop();
+
+ let minidump = getMinidump();
+ let id = minidump.leafName.slice(0, -4);
+ await Services.crashmanager.ensureCrashIsPresent(id);
+ try {
+ await handleMinidump(callback);
+ } catch (x) {
+ do_report_unexpected_exception(x);
+ }
+}
+
+/**
+ * Helper for testing a content process crash.
+ *
+ * This variant accepts a trigger function which runs in the content process
+ * and does something to _trigger_ the crash.
+ */
+async function do_triggered_content_crash(trigger, callback) {
+ do_load_child_test_harness();
+
+ // Setting the minidump path won't work in the child, so we need to do
+ // that here.
+ Services.appinfo.minidumpPath = do_get_tempdir();
+
+ /* import-globals-from ../unit/crasher_subprocess_head.js */
+
+ let headfile = do_get_file("../unit/crasher_subprocess_head.js");
+ if (trigger) {
+ if (typeof trigger == "function") {
+ // funky, but convenient
+ trigger = "(" + trigger.toSource() + ")();";
+ }
+ }
+
+ do_get_profile();
+ await makeFakeAppDir();
+ await sendCommandAsync('load("' + headfile.path.replace(/\\/g, "/") + '");');
+ await sendCommandAsync(trigger);
+ await spinEventLoop();
+ let id = getMinidump().leafName.slice(0, -4);
+ await Services.crashmanager.ensureCrashIsPresent(id);
+ try {
+ await handleMinidump(callback);
+ } catch (x) {
+ do_report_unexpected_exception(x);
+ }
+}
+
+/*
+ * Run the `crash` backgroundtask subprocess, crashing it in the
+ * specified manner.
+ *
+ * @param crashType Integer `CrashTestUtils.CRASH_...` code.
+ * @param crashExtras Dictionary of key-value pairs to include in
+ * minidump extras.
+ *
+ * @param callback
+ * A JavaScript function to be called after the subprocess
+ * crashes. It will be passed (minidump, extra, extrafile), where
+ * - minidump is an nsIFile of the minidump file produced,
+ * - extra is an object containing the key,value pairs from
+ * the .extra file.
+ * - extrafile is an nsIFile of the extra file
+ *
+ * @param canReturnZero
+ * If true, the subprocess may return with a zero exit code.
+ * Certain types of crashes may not cause the process to
+ * exit with an error.
+ *
+ */
+async function do_backgroundtask_crash(
+ crashType,
+ crashExtras,
+ callback,
+ canReturnZero
+) {
+ Assert.ok(AppConstants.MOZ_BACKGROUNDTASKS);
+
+ // Get full path to application (not xpcshell)
+ let bin = Services.dirsvc.get("GreBinD", Ci.nsIFile);
+ if (AppConstants.platform === "win") {
+ bin.append(AppConstants.MOZ_APP_NAME + ".exe");
+ } else {
+ bin.append(AppConstants.MOZ_APP_NAME);
+ }
+
+ // run `application --backgroundtask crash ...`.
+ let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
+ process.init(bin);
+
+ let args = ["--backgroundtask", "crash"];
+ args.push(crashType.toString());
+
+ // Sorted to be deterministic.
+ let sorted = Object.entries(crashExtras).sort((a, b) => a[0] < b[0]);
+ for (let [key, value] of sorted) {
+ args.push(key);
+ args.push(value);
+ }
+
+ let crashD = do_get_tempdir();
+ crashD.append("crash-events");
+ if (!crashD.exists()) {
+ crashD.create(crashD.DIRECTORY_TYPE, 0o700);
+ }
+
+ Services.env.set("CRASHES_EVENTS_DIR", crashD.path);
+
+ // Ensure `resource://testing-common` gets mapped.
+ let protocolHandler = Services.io
+ .getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+
+ let uri = protocolHandler.getSubstitution("testing-common");
+ Assert.ok(uri, "resource://testing-common is not substituted");
+
+ // The equivalent of _TESTING_MODULES_DIR in xpcshell.
+ Services.env.set("XPCSHELL_TESTING_MODULES_URI", uri.spec);
+
+ try {
+ process.run(true, args, args.length);
+ } catch (ex) {
+ // on Windows we exit with a -1 status when crashing.
+ } finally {
+ Services.env.set("CRASHES_EVENTS_DIR", "");
+ Services.env.set("XPCSHELL_TESTING_MODULES_URI", "");
+ }
+
+ if (!canReturnZero) {
+ // should exit with an error (should have crashed)
+ Assert.notEqual(process.exitValue, 0);
+ }
+
+ await handleMinidump(callback);
+}
+
+// Import binary APIs via js-ctypes.
+var { CrashTestUtils } = ChromeUtils.importESModule(
+ "resource://test/CrashTestUtils.sys.mjs"
+);
diff --git a/toolkit/crashreporter/test/unit/head_win64cfi.js b/toolkit/crashreporter/test/unit/head_win64cfi.js
new file mode 100644
index 0000000000..4df99213db
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/head_win64cfi.js
@@ -0,0 +1,215 @@
+/* import-globals-from head_crashreporter.js */
+
+let gTestCrasherSyms = null;
+let gModules = null;
+
+// Returns the offset (int) of an IP with a given base address.
+// This is effectively (ip - base), except a bit more complication due to
+// Javascript's shaky handling of 64-bit integers.
+// base & ip are passed as hex strings.
+function getModuleOffset(base, ip) {
+ let i = 0;
+ // Find where the two addresses diverge, which enables us to perform a 32-bit
+ // subtraction.
+ // e.g. "0x1111111111112222"
+ // - "0x1111111111111111"
+ // becomes 2222 - 1111
+ for (; i < base.length; ++i) {
+ if (base[i] != ip[i]) {
+ break;
+ }
+ }
+ if (i == base.length) {
+ return 0;
+ }
+ let lhs2 = "0x" + base.substring(i);
+ let rhs2 = "0x" + ip.substring(i);
+ return parseInt(rhs2) - parseInt(lhs2);
+}
+
+// Uses gTestCrasherSyms to convert an address to a symbol.
+function findNearestTestCrasherSymbol(addr) {
+ addr += 1; // Breakpad sometimes offsets addresses; correct for this.
+ let closestDistance = null;
+ let closestSym = null;
+ for (let sym in gTestCrasherSyms) {
+ if (addr >= gTestCrasherSyms[sym]) {
+ let thisDistance = addr - gTestCrasherSyms[sym];
+ if (closestDistance === null || thisDistance < closestDistance) {
+ closestDistance = thisDistance;
+ closestSym = sym;
+ }
+ }
+ }
+ if (closestSym === null) {
+ return null;
+ }
+ return { symbol: closestSym, offset: closestDistance };
+}
+
+// Populate known symbols for testcrasher.dll.
+// Use the same prop names as from CrashTestUtils to avoid the need for mapping.
+function initTestCrasherSymbols() {
+ gTestCrasherSyms = {};
+ for (let k in CrashTestUtils) {
+ // Not all keys here are valid symbol names. getWin64CFITestFnAddrOffset
+ // will return 0 in those cases, no need to filter here.
+ if (Number.isInteger(CrashTestUtils[k])) {
+ let t = CrashTestUtils.getWin64CFITestFnAddrOffset(CrashTestUtils[k]);
+ if (t > 0) {
+ gTestCrasherSyms[k] = t;
+ }
+ }
+ }
+}
+
+function stackFrameToString(frameIndex, frame) {
+ // Calculate the module offset.
+ let ip = frame.ip;
+ let symbol = "";
+ let moduleOffset = "unknown_offset";
+ let filename = "unknown_module";
+
+ if (
+ typeof frame.module_index !== "undefined" &&
+ frame.module_index >= 0 &&
+ frame.module_index < gModules.length
+ ) {
+ let base = gModules[frame.module_index].base_addr;
+ moduleOffset = getModuleOffset(base, ip);
+ filename = gModules[frame.module_index].filename;
+
+ if (filename === "testcrasher.dll") {
+ let nearestSym = findNearestTestCrasherSymbol(moduleOffset);
+ if (nearestSym !== null) {
+ symbol = nearestSym.symbol + "+" + nearestSym.offset.toString(16);
+ }
+ }
+ }
+
+ let ret =
+ "frames[" +
+ frameIndex +
+ "] ip=" +
+ ip +
+ " " +
+ symbol +
+ ", module:" +
+ filename +
+ ", trust:" +
+ frame.trust +
+ ", moduleOffset:" +
+ moduleOffset.toString(16);
+ return ret;
+}
+
+function dumpStackFrames(frames, maxFrames) {
+ for (let i = 0; i < Math.min(maxFrames, frames.length); ++i) {
+ info(stackFrameToString(i, frames[i]));
+ }
+}
+
+// Test that the top of the given stack (from extra data) matches the given
+// expected frames.
+//
+// expected is { symbol: "", trust: "" }
+function assertStack(stack, expected) {
+ for (let i = 0; i < stack.length; ++i) {
+ if (i >= expected.length) {
+ ok("Top stack frames were expected");
+ return;
+ }
+ let frame = stack[i];
+ let expectedFrame = expected[i];
+ let dumpThisFrame = function () {
+ info(" Actual frame: " + stackFrameToString(i, frame));
+ info(
+ "Expected { symbol: " +
+ expectedFrame.symbol +
+ ", trust: " +
+ expectedFrame.trust +
+ "}"
+ );
+ };
+
+ if (expectedFrame.trust) {
+ if (expectedFrame.trust.startsWith("!")) {
+ // A "!" prefix on the frame trust matching is a logical "not".
+ if (frame.trust === expectedFrame.trust.substring(1)) {
+ dumpThisFrame();
+ info("Expected frame trust matched when it should not have.");
+ ok(false);
+ }
+ } else if (frame.trust !== expectedFrame.trust) {
+ dumpThisFrame();
+ info("Expected frame trust did not match.");
+ ok(false);
+ }
+ }
+
+ if (expectedFrame.symbol) {
+ if (typeof frame.module_index === "undefined") {
+ // Without a module_index, it happened in an unknown module. Currently
+ // you can't specify an expected "unknown" module.
+ info("Unknown symbol in unknown module.");
+ ok(false);
+ }
+ if (frame.module_index < 0 || frame.module_index >= gModules.length) {
+ dumpThisFrame();
+ info("Unknown module.");
+ ok(false);
+ return;
+ }
+ let base = gModules[frame.module_index].base_addr;
+ let moduleOffset = getModuleOffset(base, frame.ip);
+ let filename = gModules[frame.module_index].filename;
+ if (filename == "testcrasher.dll") {
+ let nearestSym = findNearestTestCrasherSymbol(moduleOffset);
+ if (nearestSym === null) {
+ dumpThisFrame();
+ info("Unknown symbol.");
+ ok(false);
+ return;
+ }
+
+ if (nearestSym.symbol !== expectedFrame.symbol) {
+ dumpThisFrame();
+ info("Mismatching symbol.");
+ ok(false);
+ }
+ }
+ }
+ }
+}
+
+// Performs a crash, runs minidump-analyzer, and checks expected stack analysis.
+//
+// how: The crash to perform. Constants defined in both CrashTestUtils.jsm
+// and nsTestCrasher.cpp (i.e. CRASH_X64CFI_PUSH_NONVOL)
+// expectedStack: An array of {"symbol", "trust"} where trust is "cfi",
+// "context", "scan", et al. May be null if you don't need to check the stack.
+// minidumpAnalyzerArgs: An array of additional arguments to pass to
+// minidump-analyzer.exe.
+async function do_x64CFITest(how, expectedStack, minidumpAnalyzerArgs) {
+ // Setup is run in the subprocess so we cannot use any closures.
+ let setupFn = "crashType = CrashTestUtils." + how + ";";
+
+ let callbackFn = async function (minidumpFile, extra, extraFile) {
+ runMinidumpAnalyzer(minidumpFile, minidumpAnalyzerArgs);
+
+ // Refresh updated extra data
+ extra = await IOUtils.readJSON(extraFile.path);
+
+ initTestCrasherSymbols();
+ let stackTraces = extra.StackTraces;
+ let crashingThreadIndex = stackTraces.crash_info.crashing_thread;
+ gModules = stackTraces.modules;
+ let crashingFrames = stackTraces.threads[crashingThreadIndex].frames;
+
+ dumpStackFrames(crashingFrames, 10);
+
+ assertStack(crashingFrames, expectedStack);
+ };
+
+ do_crash(setupFn, callbackFn, true, true);
+}
diff --git a/toolkit/crashreporter/test/unit/test_crash_AsyncShutdown.js b/toolkit/crashreporter/test/unit/test_crash_AsyncShutdown.js
new file mode 100644
index 0000000000..66cc937f28
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_AsyncShutdown.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that AsyncShutdown report errors correctly
+
+// Note: these functions are evaluated in their own process, hence the need
+// to import modules into each function.
+
+function setup_crash() {
+ const { AsyncShutdown } = ChromeUtils.importESModule(
+ "resource://gre/modules/AsyncShutdown.sys.mjs"
+ );
+ const { PromiseUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PromiseUtils.sys.mjs"
+ );
+
+ Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true);
+ Services.prefs.setIntPref("toolkit.asyncshutdown.crash_timeout", 10);
+
+ let TOPIC = "testing-async-shutdown-crash";
+ let phase = AsyncShutdown._getPhase(TOPIC);
+ phase.addBlocker("A blocker that is never satisfied", function () {
+ dump("Installing blocker\n");
+ let deferred = PromiseUtils.defer();
+ return deferred.promise;
+ });
+
+ Services.obs.notifyObservers(null, TOPIC);
+ dump(new Error().stack + "\n");
+ dump("Waiting for crash\n");
+}
+
+function after_crash(mdump, extra) {
+ info("after crash: " + extra.AsyncShutdownTimeout);
+ let data = JSON.parse(extra.AsyncShutdownTimeout);
+ Assert.equal(data.phase, "testing-async-shutdown-crash");
+ Assert.equal(data.conditions[0].name, "A blocker that is never satisfied");
+ // This test spawns subprocesses by using argument "-e" of xpcshell, so
+ // this is the filename known to xpcshell.
+ Assert.equal(data.conditions[0].filename, "-e");
+}
+
+// Test that AsyncShutdown + IOUtils reports errors correctly.,
+
+function setup_ioutils_crash() {
+ const { PromiseUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PromiseUtils.sys.mjs"
+ );
+
+ Services.prefs.setIntPref("toolkit.asyncshutdown.crash_timeout", 1);
+
+ IOUtils.profileBeforeChange.addBlocker(
+ "Adding a blocker that will never be resolved",
+ () => PromiseUtils.defer().promise
+ );
+
+ Services.startup.advanceShutdownPhase(
+ Services.startup.SHUTDOWN_PHASE_APPSHUTDOWN
+ );
+ dump("Waiting for crash\n");
+}
+
+function after_ioutils_crash(mdump, extra) {
+ info("after IOUtils crash: " + extra.AsyncShutdownTimeout);
+ let data = JSON.parse(extra.AsyncShutdownTimeout);
+ Assert.equal(
+ data.phase,
+ "IOUtils: waiting for profileBeforeChange IO to complete"
+ );
+}
+
+add_task(async function run_test() {
+ await do_crash(setup_crash, after_crash);
+ await do_crash(setup_ioutils_crash, after_ioutils_crash);
+});
diff --git a/toolkit/crashreporter/test/unit/test_crash_abort.js b/toolkit/crashreporter/test/unit/test_crash_abort.js
new file mode 100644
index 0000000000..aa602dacb4
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_abort.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function run_test() {
+ // Try crashing with an abort().
+ await do_crash(
+ function () {
+ crashType = CrashTestUtils.CRASH_ABORT;
+ crashReporter.annotateCrashReport("TestKey", "TestValue");
+ },
+ function (mdump, extra) {
+ Assert.equal(extra.TestKey, "TestValue");
+ },
+ // process will exit with a zero exit status
+ true
+ );
+});
diff --git a/toolkit/crashreporter/test/unit/test_crash_after_js_large_allocation_failure.js b/toolkit/crashreporter/test/unit/test_crash_after_js_large_allocation_failure.js
new file mode 100644
index 0000000000..611ba9b6ac
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_after_js_large_allocation_failure.js
@@ -0,0 +1,23 @@
+add_task(async function run_test() {
+ if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) {
+ dump(
+ "INFO | test_crash_after_js_large_allocation_failure.js | Can't test crashreporter in a non-libxul build.\n"
+ );
+ return;
+ }
+
+ await do_crash(
+ function () {
+ crashType = CrashTestUtils.CRASH_MOZ_CRASH;
+ crashReporter.annotateCrashReport("TestKey", "Yes");
+ Cu.getJSTestingFunctions().reportLargeAllocationFailure();
+ Cu.forceGC();
+ },
+ function (mdump, extra) {
+ Assert.equal(extra.TestKey, "Yes");
+ Assert.equal(false, "JSOutOfMemory" in extra);
+ Assert.equal(extra.JSLargeAllocationFailure, "Recovered");
+ },
+ true
+ );
+});
diff --git a/toolkit/crashreporter/test/unit/test_crash_after_js_large_allocation_failure_reporting.js b/toolkit/crashreporter/test/unit/test_crash_after_js_large_allocation_failure_reporting.js
new file mode 100644
index 0000000000..cdae0f2d8e
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_after_js_large_allocation_failure_reporting.js
@@ -0,0 +1,27 @@
+add_task(async function run_test() {
+ if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) {
+ dump(
+ "INFO | test_crash_after_js_oom_reporting.js | Can't test crashreporter in a non-libxul build.\n"
+ );
+ return;
+ }
+
+ await do_crash(
+ function () {
+ crashType = CrashTestUtils.CRASH_MOZ_CRASH;
+ crashReporter.annotateCrashReport("TestKey", "Yes");
+
+ function crashWhileReporting() {
+ CrashTestUtils.crash(crashType);
+ }
+
+ Services.obs.addObserver(crashWhileReporting, "memory-pressure");
+ Cu.getJSTestingFunctions().reportLargeAllocationFailure();
+ },
+ function (mdump, extra) {
+ Assert.equal(extra.TestKey, "Yes");
+ Assert.equal(extra.JSLargeAllocationFailure, "Reporting");
+ },
+ true
+ );
+});
diff --git a/toolkit/crashreporter/test/unit/test_crash_after_js_oom_recovered.js b/toolkit/crashreporter/test/unit/test_crash_after_js_oom_recovered.js
new file mode 100644
index 0000000000..7b2491bc0b
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_after_js_oom_recovered.js
@@ -0,0 +1,22 @@
+add_task(async function run_test() {
+ if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) {
+ dump(
+ "INFO | test_crash_after_js_oom_recovered.js | Can't test crashreporter in a non-libxul build.\n"
+ );
+ return;
+ }
+
+ await do_crash(
+ function () {
+ crashType = CrashTestUtils.CRASH_MOZ_CRASH;
+ crashReporter.annotateCrashReport("TestKey", "Yes");
+ Cu.getJSTestingFunctions().reportOutOfMemory();
+ Cu.forceGC();
+ },
+ function (mdump, extra) {
+ Assert.equal(extra.TestKey, "Yes");
+ Assert.equal(extra.JSOutOfMemory, "Recovered");
+ },
+ true
+ );
+});
diff --git a/toolkit/crashreporter/test/unit/test_crash_after_js_oom_reported.js b/toolkit/crashreporter/test/unit/test_crash_after_js_oom_reported.js
new file mode 100644
index 0000000000..8796aee3c0
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_after_js_oom_reported.js
@@ -0,0 +1,36 @@
+add_task(async function run_test() {
+ if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) {
+ dump(
+ "INFO | test_crash_after_js_oom_reported.js | Can't test crashreporter in a non-libxul build.\n"
+ );
+ return;
+ }
+
+ await do_crash(
+ function () {
+ crashType = CrashTestUtils.CRASH_MOZ_CRASH;
+ crashReporter.annotateCrashReport("TestKey", "Yes");
+
+ // GC now to avoid having it happen randomly later, which would make the
+ // test bogusly fail. See comment below.
+ Cu.forceGC();
+
+ Cu.getJSTestingFunctions().reportOutOfMemory();
+ },
+ function (mdump, extra) {
+ Assert.equal(extra.TestKey, "Yes");
+
+ // The JSOutOfMemory field is absent if the JS engine never reported OOM,
+ // "Reported" if it did, and "Recovered" if it reported OOM but
+ // subsequently completed a full GC cycle. Since this test calls
+ // reportOutOfMemory() and then crashes, we expect "Reported".
+ //
+ // Theoretically, GC can happen any time, so it is just possible that
+ // this property could be "Recovered" even if the implementation is
+ // correct. More likely, though, that indicates a bug, so only accept
+ // "Reported".
+ Assert.equal(extra.JSOutOfMemory, "Reported");
+ },
+ true
+ );
+});
diff --git a/toolkit/crashreporter/test/unit/test_crash_after_js_oom_reported_2.js b/toolkit/crashreporter/test/unit/test_crash_after_js_oom_reported_2.js
new file mode 100644
index 0000000000..ba67981444
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_after_js_oom_reported_2.js
@@ -0,0 +1,28 @@
+add_task(async function run_test() {
+ if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) {
+ dump(
+ "INFO | test_crash_after_js_oom_reported_2.js | Can't test crashreporter in a non-libxul build.\n"
+ );
+ return;
+ }
+
+ await do_crash(
+ function () {
+ crashType = CrashTestUtils.CRASH_MOZ_CRASH;
+ crashReporter.annotateCrashReport("TestKey", "Yes");
+ Cu.getJSTestingFunctions().reportOutOfMemory();
+ Cu.forceGC(); // recover from first OOM
+ Cu.getJSTestingFunctions().reportOutOfMemory();
+ },
+ function (mdump, extra) {
+ Assert.equal(extra.TestKey, "Yes");
+
+ // Technically, GC can happen at any time, but it would be really
+ // peculiar for it to happen again heuristically right after a GC was
+ // forced. If extra.JSOutOfMemory is "Recovered" here, that's most
+ // likely a bug in the error reporting machinery.
+ Assert.equal(extra.JSOutOfMemory, "Reported");
+ },
+ true
+ );
+});
diff --git a/toolkit/crashreporter/test/unit/test_crash_backgroundtask_moz_crash.js b/toolkit/crashreporter/test/unit/test_crash_backgroundtask_moz_crash.js
new file mode 100644
index 0000000000..847e260925
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_backgroundtask_moz_crash.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function run_test() {
+ if (!AppConstants.MOZ_BACKGROUNDTASKS) {
+ return;
+ }
+
+ // Try crashing background task with a runtime abort
+ await do_backgroundtask_crash(
+ CrashTestUtils.CRASH_MOZ_CRASH,
+ { TestKey: "TestValue" },
+ function (mdump, extra) {
+ Assert.equal(extra.TestKey, "TestValue");
+ Assert.equal(extra.BackgroundTaskMode, "1");
+ Assert.equal(extra.BackgroundTaskName, "crash");
+ Assert.equal(extra.HeadlessMode, "1");
+ Assert.equal(false, "OOMAllocationSize" in extra);
+ Assert.equal(false, "JSOutOfMemory" in extra);
+ Assert.equal(false, "JSLargeAllocationFailure" in extra);
+ },
+ // process will exit with a zero exit status
+ true
+ );
+});
diff --git a/toolkit/crashreporter/test/unit/test_crash_exc_guard.js b/toolkit/crashreporter/test/unit/test_crash_exc_guard.js
new file mode 100644
index 0000000000..f7daffe68c
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_exc_guard.js
@@ -0,0 +1,30 @@
+add_task(async function run_test() {
+ if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) {
+ dump(
+ "INFO | test_crash_exc_guard.js | Can't test crashreporter in a non-libxul build.\n"
+ );
+ return;
+ }
+
+ // Try crashing by closing a guarded file descriptor
+ await do_crash(
+ function () {
+ crashType = CrashTestUtils.CRASH_EXC_GUARD;
+ crashReporter.annotateCrashReport("TestKey", "TestValue");
+ },
+ async function (mdump, extra, extraFile) {
+ runMinidumpAnalyzer(mdump);
+
+ // Refresh updated extra data
+ extra = await IOUtils.readJSON(extraFile.path);
+
+ Assert.equal(
+ extra.StackTraces.crash_info.type,
+ "EXC_GUARD / GUARD_TYPE_FD / GUARD_EXC_CLOSE"
+ );
+ Assert.equal(extra.TestKey, "TestValue");
+ },
+ // process will exit with a zero exit status
+ true
+ );
+});
diff --git a/toolkit/crashreporter/test/unit/test_crash_heap_corruption.js b/toolkit/crashreporter/test/unit/test_crash_heap_corruption.js
new file mode 100644
index 0000000000..0530b8b074
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_heap_corruption.js
@@ -0,0 +1,27 @@
+add_task(async function run_test() {
+ if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) {
+ dump(
+ "INFO | test_crash_heap_corruption.js | Can't test crashreporter in a non-libxul build.\n"
+ );
+ return;
+ }
+
+ // Try crashing with a STATUS_HEAP_CORRUPTION exception
+ await do_crash(
+ function () {
+ crashType = CrashTestUtils.CRASH_HEAP_CORRUPTION;
+ crashReporter.annotateCrashReport("TestKey", "TestValue");
+ },
+ async function (mdump, extra, extraFile) {
+ runMinidumpAnalyzer(mdump);
+
+ // Refresh updated extra data
+ extra = await IOUtils.readJSON(extraFile.path);
+
+ Assert.equal(extra.StackTraces.crash_info.type, "STATUS_HEAP_CORRUPTION");
+ Assert.equal(extra.TestKey, "TestValue");
+ },
+ // process will exit with a zero exit status
+ true
+ );
+});
diff --git a/toolkit/crashreporter/test/unit/test_crash_modules.js b/toolkit/crashreporter/test/unit/test_crash_modules.js
new file mode 100644
index 0000000000..dd14e77dab
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_modules.js
@@ -0,0 +1,44 @@
+add_task(async function run_test() {
+ if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) {
+ dump(
+ "INFO | test_crash_modules.js | Can't test crashreporter in a non-libxul build.\n"
+ );
+ return;
+ }
+
+ await do_crash(
+ function () {
+ const { ctypes } = ChromeUtils.importESModule(
+ "resource://gre/modules/ctypes.sys.mjs"
+ );
+ // Load and unload a DLL so that it will show up as unloaded in the minidump
+ let lib = ctypes.open("wininet");
+ lib.close();
+ },
+ async function (mdump, extra, extraFile) {
+ runMinidumpAnalyzer(mdump);
+
+ // Refresh updated extra data
+ extra = await IOUtils.readJSON(extraFile.path);
+
+ // Check unloaded modules
+ const unloadedModules = extra.StackTraces.unloaded_modules;
+ Assert.ok(!!unloadedModules, "The unloaded_modules field exists");
+ Assert.notEqual(unloadedModules.find(e => e.filename == "wininet.DLL"));
+
+ // Check the module signature information
+ const sigInfo = JSON.parse(extra.ModuleSignatureInfo);
+ Assert.ok(
+ !!sigInfo["Microsoft Windows"],
+ "The module signature info contains valid data"
+ );
+ Assert.greater(
+ sigInfo["Microsoft Windows"].length,
+ 0,
+ "Multiple signed binaries were found"
+ );
+ },
+ // process will exit with a zero exit status
+ true
+ );
+});
diff --git a/toolkit/crashreporter/test/unit/test_crash_moz_crash.js b/toolkit/crashreporter/test/unit/test_crash_moz_crash.js
new file mode 100644
index 0000000000..6016957d44
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_moz_crash.js
@@ -0,0 +1,17 @@
+add_task(async function run_test() {
+ // Try crashing with a runtime abort
+ await do_crash(
+ function () {
+ crashType = CrashTestUtils.CRASH_MOZ_CRASH;
+ crashReporter.annotateCrashReport("TestKey", "TestValue");
+ },
+ function (mdump, extra) {
+ Assert.equal(extra.TestKey, "TestValue");
+ Assert.equal(false, "OOMAllocationSize" in extra);
+ Assert.equal(false, "JSOutOfMemory" in extra);
+ Assert.equal(false, "JSLargeAllocationFailure" in extra);
+ },
+ // process will exit with a zero exit status
+ true
+ );
+});
diff --git a/toolkit/crashreporter/test/unit/test_crash_oom.js b/toolkit/crashreporter/test/unit/test_crash_oom.js
new file mode 100644
index 0000000000..3b32da4ab8
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_oom.js
@@ -0,0 +1,21 @@
+add_task(async function run_test() {
+ if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) {
+ dump(
+ "INFO | test_crash_oom.js | Can't test crashreporter in a non-libxul build.\n"
+ );
+ return;
+ }
+
+ await do_crash(
+ function () {
+ crashType = CrashTestUtils.CRASH_OOM;
+ },
+ function (mdump, extra) {
+ Assert.ok("OOMAllocationSize" in extra);
+ Assert.ok(Number(extra.OOMAllocationSize) > 0);
+ Assert.ok("TotalPhysicalMemory" in extra);
+ Assert.ok(Number(extra.TotalPhysicalMemory) >= 0);
+ },
+ true
+ );
+});
diff --git a/toolkit/crashreporter/test/unit/test_crash_phc.js b/toolkit/crashreporter/test/unit/test_crash_phc.js
new file mode 100644
index 0000000000..54bc8ec34e
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_phc.js
@@ -0,0 +1,59 @@
+function check(extra, kind, size, hasFreeStack) {
+ Assert.equal(extra.PHCKind, kind);
+
+ // This is a string holding a decimal address.
+ Assert.ok(/^\d+$/.test(extra.PHCBaseAddress));
+
+ Assert.equal(extra.PHCUsableSize, size);
+
+ // These are strings holding comma-separated lists of decimal addresses.
+ // Sometimes on Mac they have a single entry.
+ Assert.ok(/^(\d+,)*\d+$/.test(extra.PHCAllocStack));
+ if (hasFreeStack) {
+ Assert.ok(/^(\d+,)*\d+$/.test(extra.PHCFreeStack));
+ } else {
+ Assert.ok(!extra.hasOwnProperty("PHCFreeStack"));
+ }
+}
+
+add_task(async function run_test() {
+ if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) {
+ dump(
+ "INFO | test_crash_phc.js | Can't test crashreporter in a non-libxul build.\n"
+ );
+ return;
+ }
+
+ await do_crash(
+ function () {
+ crashType = CrashTestUtils.CRASH_PHC_USE_AFTER_FREE;
+ },
+ function (mdump, extra) {
+ // CRASH_PHC_USE_AFTER_FREE uses 32 for the size.
+ check(extra, "FreedPage", 32, /* hasFreeStack */ true);
+ },
+ true
+ );
+
+ await do_crash(
+ function () {
+ crashType = CrashTestUtils.CRASH_PHC_DOUBLE_FREE;
+ },
+ function (mdump, extra) {
+ // CRASH_PHC_DOUBLE_FREE uses 64 for the size.
+ check(extra, "FreedPage", 64, /* hasFreeStack */ true);
+ },
+ true
+ );
+
+ do_crash(
+ function () {
+ crashType = CrashTestUtils.CRASH_PHC_BOUNDS_VIOLATION;
+ },
+ function (mdump, extra) {
+ // CRASH_PHC_BOUNDS_VIOLATION uses 96 for the size.
+ check(extra, "GuardPage", 96, /* hasFreeStack */ false);
+ },
+ true
+ );
+});
diff --git a/toolkit/crashreporter/test/unit/test_crash_purevirtual.js b/toolkit/crashreporter/test/unit/test_crash_purevirtual.js
new file mode 100644
index 0000000000..e482eaa415
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_purevirtual.js
@@ -0,0 +1,29 @@
+add_task(async function run_test() {
+ if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) {
+ dump(
+ "INFO | test_crash_purevirtual.js | Can't test crashreporter in a non-libxul build.\n"
+ );
+ return;
+ }
+
+ var isOSX = "nsILocalFileMac" in Ci;
+ if (isOSX) {
+ dump(
+ "INFO | test_crash_purevirtual.js | TODO: purecalls not caught on OS X\n"
+ );
+ return;
+ }
+
+ // Try crashing with a pure virtual call
+ await do_crash(
+ function () {
+ crashType = CrashTestUtils.CRASH_PURE_VIRTUAL_CALL;
+ crashReporter.annotateCrashReport("TestKey", "TestValue");
+ },
+ function (mdump, extra) {
+ Assert.equal(extra.TestKey, "TestValue");
+ },
+ // process will exit with a zero exit status
+ true
+ );
+});
diff --git a/toolkit/crashreporter/test/unit/test_crash_rust_panic.js b/toolkit/crashreporter/test/unit/test_crash_rust_panic.js
new file mode 100644
index 0000000000..dff1d6281a
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_rust_panic.js
@@ -0,0 +1,15 @@
+add_task(async function run_test() {
+ // Try crashing with a Rust panic
+ await do_crash(
+ function () {
+ Cc["@mozilla.org/xpcom/debug;1"]
+ .getService(Ci.nsIDebug2)
+ .rustPanic("OH NO");
+ },
+ function (mdump, extra) {
+ Assert.equal(extra.MozCrashReason, "OH NO");
+ },
+ // process will exit with a zero exit status
+ true
+ );
+});
diff --git a/toolkit/crashreporter/test/unit/test_crash_rust_panic_multiline.js b/toolkit/crashreporter/test/unit/test_crash_rust_panic_multiline.js
new file mode 100644
index 0000000000..3908c76a2e
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_rust_panic_multiline.js
@@ -0,0 +1,15 @@
+add_task(async function run_test() {
+ // Try crashing with a Rust panic
+ await do_crash(
+ function () {
+ Cc["@mozilla.org/xpcom/debug;1"]
+ .getService(Ci.nsIDebug2)
+ .rustPanic("OH NO\nOH NOES!");
+ },
+ function (mdump, extra) {
+ Assert.equal(extra.MozCrashReason, "OH NO\nOH NOES!");
+ },
+ // process will exit with a zero exit status
+ true
+ );
+});
diff --git a/toolkit/crashreporter/test/unit/test_crash_stack_overflow.js b/toolkit/crashreporter/test/unit/test_crash_stack_overflow.js
new file mode 100644
index 0000000000..a47c217238
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_stack_overflow.js
@@ -0,0 +1,21 @@
+add_task(async function run_test() {
+ if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) {
+ dump(
+ "INFO | test_crash_stack_overflow.js | Can't test crashreporter in a non-libxul build.\n"
+ );
+ return;
+ }
+
+ // Try crashing by overflowing a thread's stack
+ await do_crash(
+ function () {
+ crashType = CrashTestUtils.CRASH_STACK_OVERFLOW;
+ crashReporter.annotateCrashReport("TestKey", "TestValue");
+ },
+ async function (mdump, extra, extraFile) {
+ Assert.equal(extra.TestKey, "TestValue");
+ },
+ // process will exit with a zero exit status
+ true
+ );
+});
diff --git a/toolkit/crashreporter/test/unit/test_crash_terminator.js b/toolkit/crashreporter/test/unit/test_crash_terminator.js
new file mode 100644
index 0000000000..c051a7e277
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_terminator.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the Shutdown Terminator report errors correctly
+
+function setup_crash() {
+ Services.prefs.setBoolPref("toolkit.terminator.testing", true);
+ Services.prefs.setIntPref("toolkit.asyncshutdown.crash_timeout", 150);
+
+ // Initialize the terminator
+ // (normally, this is done through the manifest file, but xpcshell
+ // doesn't take them into account).
+ let terminator = Cc[
+ "@mozilla.org/toolkit/shutdown-terminator;1"
+ ].createInstance(Ci.nsIObserver);
+ terminator.observe(null, "terminator-test-profile-after-change", null);
+
+ // Inform the terminator that shutdown has started
+ // Pick an arbitrary notification
+ terminator.observe(null, "terminator-test-profile-before-change", null);
+ terminator.observe(null, "terminator-test-xpcom-will-shutdown", null);
+
+ dump("Waiting (actively) for the crash\n");
+ Services.tm.spinEventLoopUntil(
+ "Test(test_crash_terminator.js:setup_crash())",
+ () => false
+ );
+}
+
+function after_crash(mdump, extra) {
+ info("Crash signature: " + JSON.stringify(extra, null, "\t"));
+ Assert.equal(extra.ShutdownProgress, "xpcom-will-shutdown");
+}
+
+add_task(async function run_test() {
+ await do_crash(setup_crash, after_crash);
+});
diff --git a/toolkit/crashreporter/test/unit/test_crash_uncaught_exception.js b/toolkit/crashreporter/test/unit/test_crash_uncaught_exception.js
new file mode 100644
index 0000000000..bfcd2b0ef2
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_uncaught_exception.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function run_test() {
+ // Try crashing with an uncaught exception.
+ await do_crash(
+ function () {
+ crashType = CrashTestUtils.CRASH_UNCAUGHT_EXCEPTION;
+ crashReporter.annotateCrashReport("TestKey", "TestValue");
+ },
+ function (mdump, extra) {
+ Assert.equal(extra.TestKey, "TestValue");
+ },
+ // process will exit with a zero exit status
+ true
+ );
+});
diff --git a/toolkit/crashreporter/test/unit/test_crash_win64cfi_alloc_large.js b/toolkit/crashreporter/test/unit/test_crash_win64cfi_alloc_large.js
new file mode 100644
index 0000000000..a16fe20b4c
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_win64cfi_alloc_large.js
@@ -0,0 +1,6 @@
+add_task(async function run_test() {
+ await do_x64CFITest("CRASH_X64CFI_ALLOC_LARGE", [
+ { symbol: "CRASH_X64CFI_ALLOC_LARGE", trust: "context" },
+ { symbol: "CRASH_X64CFI_LAUNCHER", trust: "cfi" },
+ ]);
+});
diff --git a/toolkit/crashreporter/test/unit/test_crash_win64cfi_alloc_small.js b/toolkit/crashreporter/test/unit/test_crash_win64cfi_alloc_small.js
new file mode 100644
index 0000000000..0b321bac6f
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_win64cfi_alloc_small.js
@@ -0,0 +1,6 @@
+add_task(async function run_test() {
+ await do_x64CFITest("CRASH_X64CFI_ALLOC_SMALL", [
+ { symbol: "CRASH_X64CFI_ALLOC_SMALL", trust: "context" },
+ { symbol: "CRASH_X64CFI_LAUNCHER", trust: "cfi" },
+ ]);
+});
diff --git a/toolkit/crashreporter/test/unit/test_crash_win64cfi_epilog.js b/toolkit/crashreporter/test/unit/test_crash_win64cfi_epilog.js
new file mode 100644
index 0000000000..423b67f6f9
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_win64cfi_epilog.js
@@ -0,0 +1,6 @@
+add_task(async function run_test() {
+ await do_x64CFITest("CRASH_X64CFI_EPILOG", [
+ { symbol: "CRASH_X64CFI_EPILOG", trust: "context" },
+ { symbol: "CRASH_X64CFI_LAUNCHER", trust: "cfi" },
+ ]);
+});
diff --git a/toolkit/crashreporter/test/unit/test_crash_win64cfi_infinite_code_chain.exe b/toolkit/crashreporter/test/unit/test_crash_win64cfi_infinite_code_chain.exe
new file mode 100644
index 0000000000..5283cc5df7
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_win64cfi_infinite_code_chain.exe
Binary files differ
diff --git a/toolkit/crashreporter/test/unit/test_crash_win64cfi_infinite_code_chain.js b/toolkit/crashreporter/test/unit/test_crash_win64cfi_infinite_code_chain.js
new file mode 100644
index 0000000000..a8a900c995
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_win64cfi_infinite_code_chain.js
@@ -0,0 +1,22 @@
+add_task(async function run_test() {
+ // Test that minidump-analyzer gracefully handles chained
+ // unwind code entries that form a circular reference
+ // (infinite loop).
+ let exe = do_get_file("test_crash_win64cfi_infinite_code_chain.exe");
+ ok(exe);
+
+ // Perform a crash. The PE used for unwind info should fail, resulting in
+ // fallback behavior, calculating the first frame from thread context.
+ // Further frames would be calculated with either frame_pointer or scan trust,
+ // but should not be calculated via CFI. If we see CFI here that would be an
+ // indication that either our alternative EXE was not used, or we failed to
+ // abandon unwind info parsing.
+ await do_x64CFITest(
+ "CRASH_X64CFI_ALLOC_SMALL",
+ [
+ { symbol: "CRASH_X64CFI_ALLOC_SMALL", trust: "context" },
+ { symbol: null, trust: "!cfi" },
+ ],
+ ["--force-use-module", exe.path]
+ );
+});
diff --git a/toolkit/crashreporter/test/unit/test_crash_win64cfi_infinite_entry_chain.exe b/toolkit/crashreporter/test/unit/test_crash_win64cfi_infinite_entry_chain.exe
new file mode 100644
index 0000000000..de48576f65
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_win64cfi_infinite_entry_chain.exe
Binary files differ
diff --git a/toolkit/crashreporter/test/unit/test_crash_win64cfi_infinite_entry_chain.js b/toolkit/crashreporter/test/unit/test_crash_win64cfi_infinite_entry_chain.js
new file mode 100644
index 0000000000..de3d31f0ef
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_win64cfi_infinite_entry_chain.js
@@ -0,0 +1,22 @@
+add_task(async function run_test() {
+ // Test that minidump-analyzer gracefully handles chained
+ // IMAGE_RUNTIME_FUNCTION_ENTRY items that form a circular reference
+ // (infinite loop).
+ let exe = do_get_file("test_crash_win64cfi_infinite_entry_chain.exe");
+ ok(exe);
+
+ // Perform a crash. The PE used for unwind info should fail, resulting in
+ // fallback behavior, calculating the first frame from thread context.
+ // Further frames would be calculated with either frame_pointer or scan trust,
+ // but should not be calculated via CFI. If we see CFI here that would be an
+ // indication that either our alternative EXE was not used, or we failed to
+ // abandon unwind info parsing.
+ await do_x64CFITest(
+ "CRASH_X64CFI_ALLOC_SMALL",
+ [
+ { symbol: "CRASH_X64CFI_ALLOC_SMALL", trust: "context" },
+ { symbol: null, trust: "!cfi" },
+ ],
+ ["--force-use-module", exe.path]
+ );
+});
diff --git a/toolkit/crashreporter/test/unit/test_crash_win64cfi_invalid_exception_rva.exe b/toolkit/crashreporter/test/unit/test_crash_win64cfi_invalid_exception_rva.exe
new file mode 100644
index 0000000000..ab4ce326bd
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_win64cfi_invalid_exception_rva.exe
Binary files differ
diff --git a/toolkit/crashreporter/test/unit/test_crash_win64cfi_invalid_exception_rva.js b/toolkit/crashreporter/test/unit/test_crash_win64cfi_invalid_exception_rva.js
new file mode 100644
index 0000000000..3d1eb02011
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_win64cfi_invalid_exception_rva.js
@@ -0,0 +1,21 @@
+add_task(async function run_test() {
+ // Test that minidump-analyzer gracefully handles an invalid pointer to the
+ // exception unwind information.
+ let exe = do_get_file("test_crash_win64cfi_invalid_exception_rva.exe");
+ ok(exe);
+
+ // Perform a crash. The PE used for unwind info should fail, resulting in
+ // fallback behavior, calculating the first frame from thread context.
+ // Further frames would be calculated with either frame_pointer or scan trust,
+ // but should not be calculated via CFI. If we see CFI here that would be an
+ // indication that either our alternative EXE was not used, or we failed to
+ // abandon unwind info parsing.
+ await do_x64CFITest(
+ "CRASH_X64CFI_ALLOC_SMALL",
+ [
+ { symbol: "CRASH_X64CFI_ALLOC_SMALL", trust: "context" },
+ { symbol: null, trust: "!cfi" },
+ ],
+ ["--force-use-module", exe.path]
+ );
+});
diff --git a/toolkit/crashreporter/test/unit/test_crash_win64cfi_not_a_pe.exe b/toolkit/crashreporter/test/unit/test_crash_win64cfi_not_a_pe.exe
new file mode 100644
index 0000000000..5fa1d76ffc
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_win64cfi_not_a_pe.exe
@@ -0,0 +1 @@
+this is not a valid PE file. \ No newline at end of file
diff --git a/toolkit/crashreporter/test/unit/test_crash_win64cfi_not_a_pe.js b/toolkit/crashreporter/test/unit/test_crash_win64cfi_not_a_pe.js
new file mode 100644
index 0000000000..5fcbee9cbe
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_win64cfi_not_a_pe.js
@@ -0,0 +1,20 @@
+add_task(async function run_test() {
+ // Test that minidump-analyzer gracefully handles corrupt PE files.
+ let exe = do_get_file("test_crash_win64cfi_not_a_pe.exe");
+ ok(exe);
+
+ // Perform a crash. The PE used for unwind info should fail, resulting in
+ // fallback behavior, calculating the first frame from thread context.
+ // Further frames would be calculated with either frame_pointer or scan trust,
+ // but should not be calculated via CFI. If we see CFI here that would be an
+ // indication that either our alternative EXE was not used, or we failed to
+ // abandon unwind info parsing.
+ await do_x64CFITest(
+ "CRASH_X64CFI_ALLOC_SMALL",
+ [
+ { symbol: "CRASH_X64CFI_ALLOC_SMALL", trust: "context" },
+ { symbol: null, trust: "!cfi" },
+ ],
+ ["--force-use-module", exe.path]
+ );
+});
diff --git a/toolkit/crashreporter/test/unit/test_crash_win64cfi_push_nonvol.js b/toolkit/crashreporter/test/unit/test_crash_win64cfi_push_nonvol.js
new file mode 100644
index 0000000000..e875ab3434
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_win64cfi_push_nonvol.js
@@ -0,0 +1,6 @@
+add_task(async function run_test() {
+ await do_x64CFITest("CRASH_X64CFI_PUSH_NONVOL", [
+ { symbol: "CRASH_X64CFI_PUSH_NONVOL", trust: "context" },
+ { symbol: "CRASH_X64CFI_LAUNCHER", trust: "cfi" },
+ ]);
+});
diff --git a/toolkit/crashreporter/test/unit/test_crash_win64cfi_save_nonvol.js b/toolkit/crashreporter/test/unit/test_crash_win64cfi_save_nonvol.js
new file mode 100644
index 0000000000..9bc309834a
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_win64cfi_save_nonvol.js
@@ -0,0 +1,6 @@
+add_task(async function run_test() {
+ await do_x64CFITest("CRASH_X64CFI_SAVE_NONVOL", [
+ { symbol: "CRASH_X64CFI_SAVE_NONVOL", trust: "context" },
+ { symbol: "CRASH_X64CFI_LAUNCHER", trust: "cfi" },
+ ]);
+});
diff --git a/toolkit/crashreporter/test/unit/test_crash_win64cfi_save_nonvol_far.js b/toolkit/crashreporter/test/unit/test_crash_win64cfi_save_nonvol_far.js
new file mode 100644
index 0000000000..b87b4b35b4
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_win64cfi_save_nonvol_far.js
@@ -0,0 +1,6 @@
+add_task(async function run_test() {
+ await do_x64CFITest("CRASH_X64CFI_SAVE_NONVOL_FAR", [
+ { symbol: "CRASH_X64CFI_SAVE_NONVOL_FAR", trust: "context" },
+ { symbol: "CRASH_X64CFI_LAUNCHER", trust: "cfi" },
+ ]);
+});
diff --git a/toolkit/crashreporter/test/unit/test_crash_win64cfi_save_xmm128.js b/toolkit/crashreporter/test/unit/test_crash_win64cfi_save_xmm128.js
new file mode 100644
index 0000000000..22d09f157a
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_win64cfi_save_xmm128.js
@@ -0,0 +1,6 @@
+add_task(async function run_test() {
+ await do_x64CFITest("CRASH_X64CFI_SAVE_XMM128", [
+ { symbol: "CRASH_X64CFI_SAVE_XMM128", trust: "context" },
+ { symbol: "CRASH_X64CFI_LAUNCHER", trust: "cfi" },
+ ]);
+});
diff --git a/toolkit/crashreporter/test/unit/test_crash_win64cfi_save_xmm128_far.js b/toolkit/crashreporter/test/unit/test_crash_win64cfi_save_xmm128_far.js
new file mode 100644
index 0000000000..bd5f16baa8
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_win64cfi_save_xmm128_far.js
@@ -0,0 +1,6 @@
+add_task(async function run_test() {
+ await do_x64CFITest("CRASH_X64CFI_SAVE_XMM128_FAR", [
+ { symbol: "CRASH_X64CFI_SAVE_XMM128_FAR", trust: "context" },
+ { symbol: "CRASH_X64CFI_LAUNCHER", trust: "cfi" },
+ ]);
+});
diff --git a/toolkit/crashreporter/test/unit/test_crash_win64cfi_unknown_op.js b/toolkit/crashreporter/test/unit/test_crash_win64cfi_unknown_op.js
new file mode 100644
index 0000000000..cdcd8c2aad
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_win64cfi_unknown_op.js
@@ -0,0 +1,12 @@
+add_task(async function run_test() {
+ // In the case of an unknown unwind code or missing CFI,
+ // make certain we can still walk the stack via stack scan. The crashing
+ // function places NO_MANS_LAND on the stack so it will get picked up via
+ // stack scan.
+ await do_x64CFITest("CRASH_X64CFI_UNKNOWN_OPCODE", [
+ { symbol: "CRASH_X64CFI_UNKNOWN_OPCODE", trust: "context" },
+ // Trust may either be scan or frame_pointer; we don't really care as
+ // long as the address is expected.
+ { symbol: "CRASH_X64CFI_NO_MANS_LAND", trust: null },
+ ]);
+});
diff --git a/toolkit/crashreporter/test/unit/test_crash_with_memory_report.js b/toolkit/crashreporter/test/unit/test_crash_with_memory_report.js
new file mode 100644
index 0000000000..e10e6697df
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crash_with_memory_report.js
@@ -0,0 +1,49 @@
+add_task(async function run_test() {
+ if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) {
+ dump(
+ "INFO | test_crash_oom.js | Can't test crashreporter in a non-libxul build.\n"
+ );
+ return;
+ }
+
+ // This was shamelessly copied and stripped down from do_get_profile() in
+ // head.js so that nsICrashReporter::saveMemoryReport can use a profile
+ // within the crasher subprocess.
+
+ await do_crash(
+ function () {
+ // Delay crashing so that the memory report has time to complete.
+ shouldDelay = true;
+
+ let profd = Services.env.get("XPCSHELL_TEST_PROFILE_DIR");
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.initWithPath(profd);
+
+ let provider = {
+ getFile(prop, persistent) {
+ persistent.value = true;
+ if (
+ prop == "ProfD" ||
+ prop == "ProfLD" ||
+ prop == "ProfDS" ||
+ prop == "ProfLDS" ||
+ prop == "TmpD"
+ ) {
+ return file.clone();
+ }
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIDirectoryServiceProvider"]),
+ };
+ Services.dirsvc
+ .QueryInterface(Ci.nsIDirectoryService)
+ .registerProvider(provider);
+
+ crashReporter.saveMemoryReport();
+ },
+ function (mdump, extra, extrafile, memoryfile) {
+ Assert.ok(memoryfile.exists());
+ },
+ true
+ );
+});
diff --git a/toolkit/crashreporter/test/unit/test_crashreporter.js b/toolkit/crashreporter/test/unit/test_crashreporter.js
new file mode 100644
index 0000000000..d1a8b10316
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crashreporter.js
@@ -0,0 +1,90 @@
+function run_test() {
+ dump("INFO | test_crashreporter.js | Get crashreporter service.\n");
+ var cr = Services.appinfo;
+ Assert.ok(Services.appinfo.crashReporterEnabled);
+
+ try {
+ cr.serverURL;
+ do_throw("Getting serverURL when not set should have thrown!");
+ } catch (ex) {
+ Assert.equal(ex.result, Cr.NS_ERROR_FAILURE);
+ }
+
+ // check setting/getting serverURL
+
+ // try it with two different URLs, just for kicks
+ var testspecs = [
+ "http://example.com/submit",
+ "https://example.org/anothersubmit",
+ ];
+ for (var i = 0; i < testspecs.length; ++i) {
+ cr.serverURL = Services.io.newURI(testspecs[i]);
+ Assert.equal(cr.serverURL.spec, testspecs[i]);
+ }
+
+ // should not allow setting non-http/https URLs
+ try {
+ cr.serverURL = Services.io.newURI("ftp://example.com/submit");
+ do_throw("Setting serverURL to a non-http(s) URL should have thrown!");
+ } catch (ex) {
+ Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ // check getting/setting minidumpPath
+ // it should be $TEMP by default, but I'm not sure if we can exactly test that
+ // this will at least test that it doesn't throw
+ Assert.notEqual(cr.minidumpPath.path, "");
+ var cwd = do_get_cwd();
+ cr.minidumpPath = cwd;
+ Assert.equal(cr.minidumpPath.path, cwd.path);
+
+ // Test annotateCrashReport()
+ try {
+ cr.annotateCrashReport(undefined, "");
+ do_throw(
+ "Calling annotateCrashReport() with an undefined key should have thrown!"
+ );
+ } catch (ex) {
+ Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+ try {
+ cr.annotateCrashReport("foobar", "");
+ do_throw(
+ "Calling annotateCrashReport() with a bogus key should have thrown!"
+ );
+ } catch (ex) {
+ Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+ cr.annotateCrashReport("TestKey", "testData1");
+ // Replace previous data.
+ cr.annotateCrashReport("TestKey", "testData2");
+ // Allow nul chars in annotations.
+ cr.annotateCrashReport("TestKey", "da\0ta");
+
+ cr.appendAppNotesToCrashReport("additional testData3");
+ // Add more data.
+ cr.appendAppNotesToCrashReport("additional testData4");
+
+ // Test removeCrashReportAnnotation()
+ try {
+ cr.removeCrashReportAnnotation(undefined);
+ do_throw(
+ "Calling removeCrashReportAnnotation() with an undefined key should have thrown!"
+ );
+ } catch (ex) {
+ Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+ try {
+ cr.removeCrashReportAnnotation("foobar");
+ do_throw(
+ "Calling removeCrashReportAnnotation() with a bogus key should have thrown!"
+ );
+ } catch (ex) {
+ Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+ cr.removeCrashReportAnnotation("TestKey");
+
+ // Testing setting the minidumpPath field
+ cr.minidumpPath = cwd;
+ Assert.equal(cr.minidumpPath.path, cwd.path);
+}
diff --git a/toolkit/crashreporter/test/unit/test_crashreporter_appmem.js b/toolkit/crashreporter/test/unit/test_crashreporter_appmem.js
new file mode 100644
index 0000000000..f42fe10f0c
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crashreporter_appmem.js
@@ -0,0 +1,13 @@
+add_task(async function run_test() {
+ await do_crash(
+ function () {
+ let appAddr = CrashTestUtils.saveAppMemory();
+ crashReporter.registerAppMemory(appAddr, 32);
+ },
+ function (mdump, extra) {
+ Assert.ok(mdump.exists());
+ Assert.ok(mdump.fileSize > 0);
+ Assert.ok(CrashTestUtils.dumpCheckMemory(mdump.path));
+ }
+ );
+});
diff --git a/toolkit/crashreporter/test/unit/test_crashreporter_crash.js b/toolkit/crashreporter/test/unit/test_crashreporter_crash.js
new file mode 100644
index 0000000000..4d0abbc2ef
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_crashreporter_crash.js
@@ -0,0 +1,223 @@
+add_task(async function run_test() {
+ var is_win7_or_newer = false;
+ var is_windows = false;
+ var ph = Cc["@mozilla.org/network/protocol;1?name=http"].getService(
+ Ci.nsIHttpProtocolHandler
+ );
+ var match = ph.userAgent.match(/Windows NT (\d+).(\d+)/);
+ if (match) {
+ is_windows = true;
+ }
+ if (
+ match &&
+ (parseInt(match[1]) > 6 ||
+ (parseInt(match[1]) == 6 && parseInt(match[2]) >= 1))
+ ) {
+ is_win7_or_newer = true;
+ }
+
+ // try a basic crash
+ await do_content_crash(null, function (mdump, extra) {
+ Assert.ok(mdump.exists());
+ Assert.ok(mdump.fileSize > 0);
+ Assert.ok("StartupTime" in extra);
+ Assert.ok("CrashTime" in extra);
+ Assert.ok(
+ CrashTestUtils.dumpHasStream(
+ mdump.path,
+ CrashTestUtils.MD_THREAD_LIST_STREAM
+ )
+ );
+ Assert.ok(CrashTestUtils.dumpHasInstructionPointerMemory(mdump.path));
+ if (is_windows) {
+ [
+ "SystemMemoryUsePercentage",
+ "TotalVirtualMemory",
+ "AvailableVirtualMemory",
+ "AvailablePageFile",
+ "AvailablePhysicalMemory",
+ ].forEach(function (prop) {
+ Assert.ok(/^\d+$/.test(extra[prop].toString()));
+ });
+ }
+ if (is_win7_or_newer) {
+ Assert.ok(
+ CrashTestUtils.dumpHasStream(
+ mdump.path,
+ CrashTestUtils.MD_MEMORY_INFO_LIST_STREAM
+ )
+ );
+ }
+ });
+
+ // check setting some basic data
+ await do_crash(
+ function () {
+ // Add various annotations
+ crashReporter.annotateCrashReport("TestKey", "TestValue");
+ crashReporter.annotateCrashReport(
+ "TestUnicode",
+ "\u{1F4A9}\n\u{0000}Escape"
+ );
+ crashReporter.annotateCrashReport("Add-ons", "test%40mozilla.org:0.1");
+ crashReporter.appendAppNotesToCrashReport("Junk");
+ crashReporter.appendAppNotesToCrashReport("MoreJunk");
+
+ // TelemetrySession setup will trigger the session annotation
+ let { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+ );
+ TelemetryController.testSetup();
+ },
+ function (mdump, extra) {
+ Assert.equal(extra.TestKey, "TestValue");
+ Assert.equal(extra.TestUnicode, "\u{1F4A9}\n\u{0000}Escape");
+ Assert.ok(
+ extra.Notes.endsWith("JunkMoreJunk"),
+ "Should include our notes"
+ );
+ Assert.equal(extra["Add-ons"], "test%40mozilla.org:0.1");
+ const UUID_REGEX =
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
+ Assert.ok(
+ "TelemetrySessionId" in extra,
+ "The TelemetrySessionId field is present in the extra file"
+ );
+ Assert.ok(
+ UUID_REGEX.test(extra.TelemetrySessionId),
+ "The TelemetrySessionId is a UUID"
+ );
+ Assert.ok(
+ !("TelemetryClientId" in extra),
+ "The TelemetryClientId field is omitted by default"
+ );
+ Assert.ok(
+ !("TelemetryServerURL" in extra),
+ "The TelemetryServerURL field is omitted by default"
+ );
+ }
+ );
+
+ await do_crash(
+ function () {
+ // Enable the FHR, official policy bypass (since we're in a test) and
+ // specify a telemetry server & client ID.
+ Services.prefs.setBoolPref(
+ "datareporting.policy.dataSubmissionPolicyBypassNotification",
+ true
+ );
+ Services.prefs.setBoolPref(
+ "datareporting.healthreport.uploadEnabled",
+ true
+ );
+ Services.prefs.setCharPref(
+ "toolkit.telemetry.server",
+ "http://a.telemetry.server"
+ );
+ Services.prefs.setIntPref("telemetry.fog.test.localhost_port", -1);
+ Services.prefs.setCharPref(
+ "toolkit.telemetry.cachedClientID",
+ "f3582dee-22b9-4d73-96d1-79ef5bf2fc24"
+ );
+
+ // TelemetrySession setup will trigger the session annotation
+ let { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+ );
+ let { TelemetrySend } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySend.sys.mjs"
+ );
+ TelemetrySend.setTestModeEnabled(true);
+ TelemetryController.testSetup();
+ },
+ function (mdump, extra) {
+ Assert.ok(
+ "TelemetryClientId" in extra,
+ "The TelemetryClientId field is present when the FHR is on"
+ );
+ Assert.equal(
+ extra.TelemetryClientId,
+ "f3582dee-22b9-4d73-96d1-79ef5bf2fc24",
+ "The TelemetryClientId matches the expected value"
+ );
+ Assert.ok(
+ "TelemetryServerURL" in extra,
+ "The TelemetryServerURL field is present when the FHR is on"
+ );
+ Assert.equal(
+ extra.TelemetryServerURL,
+ "http://a.telemetry.server",
+ "The TelemetryServerURL matches the expected value"
+ );
+ }
+ );
+
+ await do_crash(
+ function () {
+ // Disable the FHR upload, no telemetry annotations should be present.
+ Services.prefs.setBoolPref(
+ "datareporting.policy.dataSubmissionPolicyBypassNotification",
+ true
+ );
+ Services.prefs.setBoolPref(
+ "datareporting.healthreport.uploadEnabled",
+ false
+ );
+
+ // TelemetrySession setup will trigger the session annotation
+ let { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+ );
+ let { TelemetrySend } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySend.sys.mjs"
+ );
+ TelemetrySend.setTestModeEnabled(true);
+ TelemetryController.testSetup();
+ },
+ function (mdump, extra) {
+ Assert.ok(
+ !("TelemetryClientId" in extra),
+ "The TelemetryClientId field is omitted when FHR upload is disabled"
+ );
+ Assert.ok(
+ !("TelemetryServerURL" in extra),
+ "The TelemetryServerURL field is omitted when FHR upload is disabled"
+ );
+ }
+ );
+
+ await do_crash(
+ function () {
+ // No telemetry annotations should be present if the user has not been
+ // notified yet
+ Services.prefs.setBoolPref(
+ "datareporting.policy.dataSubmissionPolicyBypassNotification",
+ false
+ );
+ Services.prefs.setBoolPref(
+ "datareporting.healthreport.uploadEnabled",
+ true
+ );
+
+ // TelemetrySession setup will trigger the session annotation
+ let { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+ );
+ let { TelemetrySend } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySend.sys.mjs"
+ );
+ TelemetrySend.setTestModeEnabled(true);
+ TelemetryController.testSetup();
+ },
+ function (mdump, extra) {
+ Assert.ok(
+ !("TelemetryClientId" in extra),
+ "The TelemetryClientId field is omitted when FHR upload is disabled"
+ );
+ Assert.ok(
+ !("TelemetryServerURL" in extra),
+ "The TelemetryServerURL field is omitted when FHR upload is disabled"
+ );
+ }
+ );
+});
diff --git a/toolkit/crashreporter/test/unit/test_event_files.js b/toolkit/crashreporter/test/unit/test_event_files.js
new file mode 100644
index 0000000000..844d7700ff
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_event_files.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_setup() {
+ do_get_profile();
+ await makeFakeAppDir();
+});
+
+add_task(async function test_main_process_crash() {
+ let cm = Services.crashmanager;
+ Assert.ok(cm, "CrashManager available.");
+
+ let basename;
+ let count = await new Promise((resolve, reject) => {
+ do_crash(
+ function () {
+ // TelemetrySession setup will trigger the session annotation
+ let { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+ );
+ TelemetryController.testSetup();
+ crashType = CrashTestUtils.CRASH_MOZ_CRASH;
+ crashReporter.annotateCrashReport("ShutdownProgress", "event-test");
+ },
+ (minidump, extra) => {
+ basename = minidump.leafName;
+ Object.defineProperty(cm, "_eventsDirs", { value: [getEventDir()] });
+ cm.aggregateEventsFiles().then(resolve, reject);
+ },
+ true
+ );
+ });
+ Assert.equal(count, 1, "A single crash event file was seen.");
+ let crashes = await cm.getCrashes();
+ Assert.equal(crashes.length, 1);
+ let crash = crashes[0];
+ Assert.ok(
+ crash.isOfType(
+ cm.processTypes[Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT],
+ cm.CRASH_TYPE_CRASH
+ )
+ );
+ Assert.equal(crash.id + ".dmp", basename, "ID recorded properly");
+ Assert.equal(crash.metadata.ShutdownProgress, "event-test");
+ Assert.ok("TelemetrySessionId" in crash.metadata);
+ Assert.ok("UptimeTS" in crash.metadata);
+ Assert.ok(
+ /^[0-9a-f]{8}\-([0-9a-f]{4}\-){3}[0-9a-f]{12}$/.test(
+ crash.metadata.TelemetrySessionId
+ )
+ );
+ Assert.ok("CrashTime" in crash.metadata);
+ Assert.ok(/^\d+$/.test(crash.metadata.CrashTime));
+});
diff --git a/toolkit/crashreporter/test/unit/test_kill.js b/toolkit/crashreporter/test/unit/test_kill.js
new file mode 100644
index 0000000000..3d8830bd22
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_kill.js
@@ -0,0 +1,43 @@
+// Test that calling Services.processtools.kill doesn't create a crash report.
+add_task(async function run_test() {
+ if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) {
+ dump(
+ "INFO | test_kill.js | Can't test crashreporter in a non-libxul build.\n"
+ );
+ return;
+ }
+
+ // Let's launch a child process and kill it (from within, it's simpler).
+
+ do_load_child_test_harness();
+
+ // Setting the minidump path won't work in the child, so we need to do
+ // that here.
+ Services.appinfo.minidumpPath = do_get_tempdir();
+ let headfile = do_get_file("../unit/crasher_subprocess_head.js");
+ const CRASH_THEN_WAIT =
+ "const ProcessTools = Cc['@mozilla.org/processtools-service;1'].getService(Ci.nsIProcessToolsService);\
+ console.log('Child process commiting ritual self-sacrifice');\
+ ProcessTools.kill(ProcessTools.pid);\
+ console.error('Oops, I should be dead');\
+ while (true) {} ;";
+ do_get_profile();
+ await makeFakeAppDir();
+ await sendCommandAsync('load("' + headfile.path.replace(/\\/g, "/") + '");');
+ await sendCommandAsync(CRASH_THEN_WAIT);
+
+ // Let's wait a little to give the child process a chance to create a minidump.
+ let { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+ );
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 100));
+
+ // Now make sure that we have no minidump.
+ let minidump = getMinidump();
+ Assert.equal(
+ minidump,
+ null,
+ `There should be no minidump ${minidump == null ? "null" : minidump.path}`
+ );
+});
diff --git a/toolkit/crashreporter/test/unit/test_oom_annotation.js b/toolkit/crashreporter/test/unit/test_oom_annotation.js
new file mode 100644
index 0000000000..029497429a
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_oom_annotation.js
@@ -0,0 +1,71 @@
+add_task(async function run_test() {
+ if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) {
+ dump(
+ "INFO | test_crash_oom.js | Can't test crashreporter in a non-libxul build.\n"
+ );
+ return;
+ }
+
+ await do_content_crash(
+ function () {
+ crashType = CrashTestUtils.CRASH_OOM;
+ crashReporter.annotateCrashReport("TestKey", "Yes");
+ },
+ function (mdump, extra) {
+ const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+ );
+ Assert.equal(extra.TestKey, "Yes");
+
+ // A list of pairs [annotation name, must be > 0]
+ let annotations;
+ switch (AppConstants.platform) {
+ case "win":
+ annotations = [
+ ["OOMAllocationSize", true],
+ ["SystemMemoryUsePercentage", false],
+ ["TotalVirtualMemory", true],
+ ["AvailableVirtualMemory", false],
+ ["TotalPageFile", false],
+ ["AvailablePageFile", false],
+ ["TotalPhysicalMemory", true],
+ ["AvailablePhysicalMemory", false],
+ ];
+ break;
+ case "linux":
+ annotations = [
+ ["OOMAllocationSize", true],
+ ["AvailablePageFile", false],
+ ["AvailablePhysicalMemory", false],
+ ["AvailableSwapMemory", false],
+ ["AvailableVirtualMemory", false],
+ ["TotalPageFile", false],
+ ["TotalPhysicalMemory", true],
+ ];
+ break;
+ case "macosx":
+ annotations = [
+ ["OOMAllocationSize", true],
+ ["AvailablePhysicalMemory", false],
+ ["AvailableSwapMemory", false],
+ ["PurgeablePhysicalMemory", false],
+ ["TotalPhysicalMemory", true],
+ ];
+ break;
+ default:
+ annotations = [];
+ }
+ for (let [label, shouldBeGreaterThanZero] of annotations) {
+ Assert.ok(label in extra, `Annotation ${label} is present`);
+
+ // All these annotations should represent non-negative numbers.
+ // A few of them (e.g. physical memory) are guaranteed to be positive.
+ if (shouldBeGreaterThanZero) {
+ Assert.ok(Number(extra[label]) > 0);
+ } else {
+ Assert.ok(Number(extra[label]) >= 0);
+ }
+ }
+ }
+ );
+});
diff --git a/toolkit/crashreporter/test/unit/test_override_exception_handler.js b/toolkit/crashreporter/test/unit/test_override_exception_handler.js
new file mode 100644
index 0000000000..1b4dec8a61
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/test_override_exception_handler.js
@@ -0,0 +1,11 @@
+add_task(async function run_test() {
+ // Ensure that attempting to override the exception handler doesn't cause
+ // us to lose our exception handler.
+ await do_crash(
+ function () {
+ CrashTestUtils.TryOverrideExceptionHandler();
+ },
+ function (mdump, extra) {},
+ true
+ );
+});
diff --git a/toolkit/crashreporter/test/unit/xpcshell-phc.ini b/toolkit/crashreporter/test/unit/xpcshell-phc.ini
new file mode 100644
index 0000000000..d1d0ada544
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/xpcshell-phc.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+head = head_crashreporter.js
+skip-if =
+ toolkit == "android" # 1536217
+ os == "win" && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1807922
+support-files =
+ crasher_subprocess_head.js
+ crasher_subprocess_tail.js
+
+[test_crash_phc.js]
+
diff --git a/toolkit/crashreporter/test/unit/xpcshell.ini b/toolkit/crashreporter/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..8ba9e894ba
--- /dev/null
+++ b/toolkit/crashreporter/test/unit/xpcshell.ini
@@ -0,0 +1,131 @@
+[DEFAULT]
+head = head_crashreporter.js
+skip-if =
+ toolkit == 'android'
+ os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1807922
+support-files =
+ crasher_subprocess_head.js
+ crasher_subprocess_tail.js
+
+[test_crash_moz_crash.js]
+[test_crash_purevirtual.js]
+[test_crash_rust_panic.js]
+[test_crash_rust_panic_multiline.js]
+[test_crash_after_js_oom_reported.js]
+[test_crash_after_js_oom_recovered.js]
+[test_crash_after_js_oom_reported_2.js]
+[test_crash_after_js_large_allocation_failure.js]
+[test_crash_after_js_large_allocation_failure_reporting.js]
+[test_crash_oom.js]
+[test_oom_annotation.js]
+run-sequentially = very high failure rate in parallel
+[test_kill.js]
+
+[test_crash_abort.js]
+skip-if = os == 'win'
+
+[test_crash_uncaught_exception.js]
+
+[test_crash_with_memory_report.js]
+[test_crashreporter.js]
+[test_crashreporter_crash.js]
+run-sequentially = very high failure rate in parallel
+[test_override_exception_handler.js]
+skip-if = os != 'win'
+
+[test_crashreporter_appmem.js]
+# we need to skip this due to bug 838613
+skip-if = (os != 'win' && os != 'linux') || (os=='linux' && bits==32)
+
+[test_crash_AsyncShutdown.js]
+[test_event_files.js]
+[test_crash_terminator.js]
+
+[test_crash_backgroundtask_moz_crash.js]
+
+[test_crash_heap_corruption.js]
+skip-if = os != 'win'
+reason = Test covering Windows-specific crash type
+run-sequentially = very high failure rate in parallel
+
+[test_crash_exc_guard.js]
+skip-if = os != 'mac'
+reason = Test covering macOS-specific crash type
+
+[test_crash_modules.js]
+skip-if = os != 'win'
+reason = Test covering Windows-specific module handling
+run-sequentially = very high failure rate in parallel
+
+[test_crash_win64cfi_unknown_op.js]
+head = head_crashreporter.js head_win64cfi.js
+skip-if = !(os == 'win' && bits == 64 && processor == 'x86_64')
+reason = Windows test specific to the x86-64 architecture
+
+[test_crash_win64cfi_push_nonvol.js]
+head = head_crashreporter.js head_win64cfi.js
+skip-if = !(os == 'win' && bits == 64 && processor == 'x86_64')
+reason = Windows test specific to the x86-64 architecture
+
+[test_crash_win64cfi_alloc_small.js]
+head = head_crashreporter.js head_win64cfi.js
+skip-if = !(os == 'win' && bits == 64 && processor == 'x86_64')
+reason = Windows test specific to the x86-64 architecture
+
+[test_crash_win64cfi_alloc_large.js]
+head = head_crashreporter.js head_win64cfi.js
+skip-if = !(os == 'win' && bits == 64 && processor == 'x86_64')
+reason = Windows test specific to the x86-64 architecture
+
+[test_crash_win64cfi_save_nonvol.js]
+head = head_crashreporter.js head_win64cfi.js
+skip-if = !(os == 'win' && bits == 64 && processor == 'x86_64')
+reason = Windows test specific to the x86-64 architecture
+
+[test_crash_win64cfi_save_nonvol_far.js]
+head = head_crashreporter.js head_win64cfi.js
+skip-if = !(os == 'win' && bits == 64 && processor == 'x86_64')
+reason = Windows test specific to the x86-64 architecture
+
+[test_crash_win64cfi_save_xmm128.js]
+head = head_crashreporter.js head_win64cfi.js
+skip-if = !(os == 'win' && bits == 64 && processor == 'x86_64')
+reason = Windows test specific to the x86-64 architecture
+
+[test_crash_win64cfi_save_xmm128_far.js]
+head = head_crashreporter.js head_win64cfi.js
+skip-if = !(os == 'win' && bits == 64 && processor == 'x86_64')
+reason = Windows test specific to the x86-64 architecture
+
+[test_crash_win64cfi_epilog.js]
+head = head_crashreporter.js head_win64cfi.js
+skip-if = !(os == 'win' && bits == 64 && processor == 'x86_64')
+reason = Windows test specific to the x86-64 architecture
+
+[test_crash_win64cfi_infinite_entry_chain.js]
+head = head_crashreporter.js head_win64cfi.js
+skip-if = !(os == 'win' && bits == 64 && processor == 'x86_64')
+reason = Windows test specific to the x86-64 architecture
+support-files = test_crash_win64cfi_infinite_entry_chain.exe
+
+[test_crash_win64cfi_infinite_code_chain.js]
+head = head_crashreporter.js head_win64cfi.js
+skip-if = !(os == 'win' && bits == 64 && processor == 'x86_64')
+reason = Windows test specific to the x86-64 architecture
+support-files = test_crash_win64cfi_infinite_code_chain.exe
+
+[test_crash_win64cfi_invalid_exception_rva.js]
+head = head_crashreporter.js head_win64cfi.js
+skip-if = !(os == 'win' && bits == 64 && processor == 'x86_64')
+reason = Windows test specific to the x86-64 architecture
+support-files = test_crash_win64cfi_invalid_exception_rva.exe
+
+[test_crash_win64cfi_not_a_pe.js]
+head = head_crashreporter.js head_win64cfi.js
+skip-if = !(os == 'win' && bits == 64 && processor == 'x86_64')
+reason = Windows test specific to the x86-64 architecture
+support-files = test_crash_win64cfi_not_a_pe.exe
+
+[test_crash_stack_overflow.js]
+skip-if = os != 'linux'
+reason = Still broken on macOS and not yet supported on Windows
diff --git a/toolkit/crashreporter/test/unit_ipc/test_content_annotation.js b/toolkit/crashreporter/test/unit_ipc/test_content_annotation.js
new file mode 100644
index 0000000000..badc15c27d
--- /dev/null
+++ b/toolkit/crashreporter/test/unit_ipc/test_content_annotation.js
@@ -0,0 +1,33 @@
+/* import-globals-from ../unit/head_crashreporter.js */
+load("../unit/head_crashreporter.js");
+
+add_task(async function run_test() {
+ if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) {
+ dump(
+ "INFO | test_content_annotation.js | Can't test crashreporter in a non-libxul build.\n"
+ );
+ return;
+ }
+
+ // TelemetrySession setup will trigger the session annotation
+ let { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+ );
+ TelemetryController.testSetup();
+
+ // Try crashing with a runtime abort
+ await do_content_crash(
+ function () {
+ crashType = CrashTestUtils.CRASH_MOZ_CRASH;
+ crashReporter.annotateCrashReport("TestKey", "TestValue");
+ crashReporter.appendAppNotesToCrashReport("!!!foo!!!");
+ },
+ function (mdump, extra) {
+ Assert.equal(extra.TestKey, "TestValue");
+ Assert.ok("ProcessType" in extra);
+ Assert.ok("StartupTime" in extra);
+ Assert.ok("TelemetrySessionId" in extra);
+ Assert.notEqual(extra.Notes.indexOf("!!!foo!!!"), -1);
+ }
+ );
+});
diff --git a/toolkit/crashreporter/test/unit_ipc/test_content_exception_time_annotation.js b/toolkit/crashreporter/test/unit_ipc/test_content_exception_time_annotation.js
new file mode 100644
index 0000000000..95205a6017
--- /dev/null
+++ b/toolkit/crashreporter/test/unit_ipc/test_content_exception_time_annotation.js
@@ -0,0 +1,21 @@
+/* import-globals-from ../unit/head_crashreporter.js */
+load("../unit/head_crashreporter.js");
+
+add_task(async function run_test() {
+ if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) {
+ dump(
+ "INFO | test_content_annotation.js | Can't test crashreporter in a non-libxul build.\n"
+ );
+ return;
+ }
+
+ // Try crashing with an OOM
+ await do_content_crash(
+ function () {
+ crashType = CrashTestUtils.CRASH_OOM;
+ },
+ function (mdump, extra) {
+ Assert.ok("OOMAllocationSize" in extra);
+ }
+ );
+});
diff --git a/toolkit/crashreporter/test/unit_ipc/test_content_large_annotation.js b/toolkit/crashreporter/test/unit_ipc/test_content_large_annotation.js
new file mode 100644
index 0000000000..92875ccd99
--- /dev/null
+++ b/toolkit/crashreporter/test/unit_ipc/test_content_large_annotation.js
@@ -0,0 +1,25 @@
+/* import-globals-from ../unit/head_crashreporter.js */
+load("../unit/head_crashreporter.js");
+
+add_task(async function run_test() {
+ if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) {
+ dump(
+ "INFO | test_content_large_annotation.js | Can't test crashreporter in a non-libxul build.\n"
+ );
+ return;
+ }
+
+ // Try crashing with a runtime abort
+ await do_content_crash(
+ function () {
+ crashType = CrashTestUtils.CRASH_MOZ_CRASH;
+ crashReporter.annotateCrashReport("TestKey", "a".repeat(65536));
+ },
+ function (mdump, extra) {
+ Assert.ok(
+ extra.TestKey == "a".repeat(65536),
+ "The TestKey annotation matches the expected value"
+ );
+ }
+ );
+});
diff --git a/toolkit/crashreporter/test/unit_ipc/test_content_memory_list.js b/toolkit/crashreporter/test/unit_ipc/test_content_memory_list.js
new file mode 100644
index 0000000000..733d224160
--- /dev/null
+++ b/toolkit/crashreporter/test/unit_ipc/test_content_memory_list.js
@@ -0,0 +1,33 @@
+// Any copyright is dedicated to the Public Domain.
+// http://creativecommons.org/publicdomain/zero/1.0/
+
+/* import-globals-from ../unit/head_crashreporter.js */
+load("../unit/head_crashreporter.js");
+
+add_task(async function run_test() {
+ var is_win7_or_newer = false;
+ var ph = Cc["@mozilla.org/network/protocol;1?name=http"].getService(
+ Ci.nsIHttpProtocolHandler
+ );
+ var match = ph.userAgent.match(/Windows NT (\d+).(\d+)/);
+ if (
+ match &&
+ (parseInt(match[1]) > 6 ||
+ (parseInt(match[1]) == 6 && parseInt(match[2]) >= 1))
+ ) {
+ is_win7_or_newer = true;
+ }
+
+ await do_content_crash(null, function (mdump, extra) {
+ Assert.ok(mdump.exists());
+ Assert.ok(mdump.fileSize > 0);
+ if (is_win7_or_newer) {
+ Assert.ok(
+ CrashTestUtils.dumpHasStream(
+ mdump.path,
+ CrashTestUtils.MD_MEMORY_INFO_LIST_STREAM
+ )
+ );
+ }
+ });
+});
diff --git a/toolkit/crashreporter/test/unit_ipc/test_content_oom_annotation.js b/toolkit/crashreporter/test/unit_ipc/test_content_oom_annotation.js
new file mode 100644
index 0000000000..d17e01f5f5
--- /dev/null
+++ b/toolkit/crashreporter/test/unit_ipc/test_content_oom_annotation.js
@@ -0,0 +1,33 @@
+/* import-globals-from ../unit/head_crashreporter.js */
+load("../unit/head_crashreporter.js");
+
+add_task(async function run_test() {
+ if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) {
+ dump(
+ "INFO | test_content_annotation.js | Can't test crashreporter in a non-libxul build.\n"
+ );
+ return;
+ }
+
+ // Try crashing with an OOM
+ await do_content_crash(
+ function () {
+ crashType = CrashTestUtils.CRASH_OOM;
+ },
+ function (mdump, extra) {
+ Assert.ok("TotalPhysicalMemory" in extra);
+ Assert.ok("AvailablePhysicalMemory" in extra);
+
+ if (mozinfo.os == "win") {
+ Assert.ok("SystemMemoryUsePercentage" in extra);
+ Assert.ok("TotalVirtualMemory" in extra);
+ Assert.ok("AvailableVirtualMemory" in extra);
+ Assert.ok("TotalPageFile" in extra);
+ Assert.ok("AvailablePageFile" in extra);
+ } else if (mozinfo.os == "linux") {
+ Assert.ok("TotalPageFile" in extra);
+ Assert.ok("AvailablePageFile" in extra);
+ }
+ }
+ );
+});
diff --git a/toolkit/crashreporter/test/unit_ipc/test_content_phc.js b/toolkit/crashreporter/test/unit_ipc/test_content_phc.js
new file mode 100644
index 0000000000..1b54448135
--- /dev/null
+++ b/toolkit/crashreporter/test/unit_ipc/test_content_phc.js
@@ -0,0 +1,31 @@
+/* import-globals-from ../unit/head_crashreporter.js */
+load("../unit/head_crashreporter.js");
+
+add_task(async function run_test() {
+ if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) {
+ dump(
+ "INFO | test_content_phc.js | Can't test crashreporter in a non-libxul build.\n"
+ );
+ return;
+ }
+
+ await do_content_crash(
+ function () {
+ crashType = CrashTestUtils.CRASH_PHC_USE_AFTER_FREE;
+ },
+ function (mdump, extra) {
+ Assert.equal(extra.PHCKind, "FreedPage");
+
+ // This is a string holding a decimal address.
+ Assert.ok(/^\d+$/.test(extra.PHCBaseAddress));
+
+ // CRASH_PHC_USE_AFTER_FREE uses 32 for the size.
+ Assert.equal(extra.PHCUsableSize, 32);
+
+ // These are strings holding comma-separated lists of decimal addresses.
+ // Sometimes on Mac they have a single entry.
+ Assert.ok(/^(\d+,)*\d+$/.test(extra.PHCAllocStack));
+ Assert.ok(/^(\d+,)*\d+$/.test(extra.PHCFreeStack));
+ }
+ );
+});
diff --git a/toolkit/crashreporter/test/unit_ipc/test_content_phc2.js b/toolkit/crashreporter/test/unit_ipc/test_content_phc2.js
new file mode 100644
index 0000000000..0db9165b14
--- /dev/null
+++ b/toolkit/crashreporter/test/unit_ipc/test_content_phc2.js
@@ -0,0 +1,34 @@
+/* import-globals-from ../unit/head_crashreporter.js */
+load("../unit/head_crashreporter.js");
+
+add_task(async function run_test() {
+ if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) {
+ dump(
+ "INFO | test_content_phc.js | Can't test crashreporter in a non-libxul build.\n"
+ );
+ return;
+ }
+
+ // For some unknown reason, having multiple do_content_crash() calls in a
+ // single test doesn't work. That explains why this test exists separately
+ // from test_content_phc.js.
+ await do_content_crash(
+ function () {
+ crashType = CrashTestUtils.CRASH_PHC_DOUBLE_FREE;
+ },
+ function (mdump, extra) {
+ Assert.equal(extra.PHCKind, "FreedPage");
+
+ // This is a string holding a decimal address.
+ Assert.ok(/^\d+$/.test(extra.PHCBaseAddress));
+
+ // CRASH_PHC_DOUBLE_FREE uses 64 for the size.
+ Assert.equal(extra.PHCUsableSize, 64);
+
+ // These are strings holding comma-separated lists of decimal addresses.
+ // Sometimes on Mac they have a single entry.
+ Assert.ok(/^(\d+,)*\d+$/.test(extra.PHCAllocStack));
+ Assert.ok(/^(\d+,)*\d+$/.test(extra.PHCFreeStack));
+ }
+ );
+});
diff --git a/toolkit/crashreporter/test/unit_ipc/test_content_phc3.js b/toolkit/crashreporter/test/unit_ipc/test_content_phc3.js
new file mode 100644
index 0000000000..ab6b81f834
--- /dev/null
+++ b/toolkit/crashreporter/test/unit_ipc/test_content_phc3.js
@@ -0,0 +1,35 @@
+/* import-globals-from ../unit/head_crashreporter.js */
+load("../unit/head_crashreporter.js");
+
+add_task(async function run_test() {
+ if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) {
+ dump(
+ "INFO | test_content_phc.js | Can't test crashreporter in a non-libxul build.\n"
+ );
+ return;
+ }
+
+ // For some unknown reason, having multiple do_content_crash() calls in a
+ // single test doesn't work. That explains why this test exists separately
+ // from test_content_phc.js.
+ await do_content_crash(
+ function () {
+ crashType = CrashTestUtils.CRASH_PHC_BOUNDS_VIOLATION;
+ },
+ function (mdump, extra) {
+ Assert.equal(extra.PHCKind, "GuardPage");
+
+ // This is a string holding a decimal address.
+ Assert.ok(/^\d+$/.test(extra.PHCBaseAddress));
+
+ // CRASH_PHC_BOUNDS_VIOLATION uses 96 for the size.
+ Assert.equal(extra.PHCUsableSize, 96);
+
+ // This is a string holding a comma-separated list of decimal addresses.
+ // Sometimes on Mac it has a single entry.
+ Assert.ok(/^(\d+,)*\d+$/.test(extra.PHCAllocStack));
+
+ Assert.ok(!extra.hasOwnProperty("PHCFreeStack"));
+ }
+ );
+});
diff --git a/toolkit/crashreporter/test/unit_ipc/test_content_rust_panic.js b/toolkit/crashreporter/test/unit_ipc/test_content_rust_panic.js
new file mode 100644
index 0000000000..91b9b86bb6
--- /dev/null
+++ b/toolkit/crashreporter/test/unit_ipc/test_content_rust_panic.js
@@ -0,0 +1,23 @@
+/* import-globals-from ../unit/head_crashreporter.js */
+load("../unit/head_crashreporter.js");
+
+add_task(async function run_test() {
+ if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) {
+ dump(
+ "INFO | test_content_rust_panic.js | Can't test crashreporter in a non-libxul build.\n"
+ );
+ return;
+ }
+
+ // Try crashing with a Rust panic
+ await do_triggered_content_crash(
+ function () {
+ Cc["@mozilla.org/xpcom/debug;1"]
+ .getService(Ci.nsIDebug2)
+ .rustPanic("OH NO");
+ },
+ function (mdump, extra) {
+ Assert.equal(extra.MozCrashReason, "OH NO");
+ }
+ );
+});
diff --git a/toolkit/crashreporter/test/unit_ipc/test_content_rust_panic_multiline.js b/toolkit/crashreporter/test/unit_ipc/test_content_rust_panic_multiline.js
new file mode 100644
index 0000000000..393afa42ab
--- /dev/null
+++ b/toolkit/crashreporter/test/unit_ipc/test_content_rust_panic_multiline.js
@@ -0,0 +1,23 @@
+/* import-globals-from ../unit/head_crashreporter.js */
+load("../unit/head_crashreporter.js");
+
+add_task(async function run_test() {
+ if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) {
+ dump(
+ "INFO | test_content_rust_panic.js | Can't test crashreporter in a non-libxul build.\n"
+ );
+ return;
+ }
+
+ // Try crashing with a Rust panic
+ await do_triggered_content_crash(
+ function () {
+ Cc["@mozilla.org/xpcom/debug;1"]
+ .getService(Ci.nsIDebug2)
+ .rustPanic("OH NO\nOH NOES!");
+ },
+ function (mdump, extra) {
+ Assert.equal(extra.MozCrashReason, "OH NO\nOH NOES!");
+ }
+ );
+});
diff --git a/toolkit/crashreporter/test/unit_ipc/xpcshell-phc.ini b/toolkit/crashreporter/test/unit_ipc/xpcshell-phc.ini
new file mode 100644
index 0000000000..45a5b1108d
--- /dev/null
+++ b/toolkit/crashreporter/test/unit_ipc/xpcshell-phc.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+head =
+skip-if = toolkit == "android" # 1536217
+support-files =
+ !/toolkit/crashreporter/test/unit/crasher_subprocess_head.js
+ !/toolkit/crashreporter/test/unit/crasher_subprocess_tail.js
+ !/toolkit/crashreporter/test/unit/head_crashreporter.js
+
+[test_content_phc.js]
+[test_content_phc2.js]
+[test_content_phc3.js]
diff --git a/toolkit/crashreporter/test/unit_ipc/xpcshell.ini b/toolkit/crashreporter/test/unit_ipc/xpcshell.ini
new file mode 100644
index 0000000000..af8d1c21c4
--- /dev/null
+++ b/toolkit/crashreporter/test/unit_ipc/xpcshell.ini
@@ -0,0 +1,17 @@
+[DEFAULT]
+run-sequentially = very high failure rate in parallel
+head =
+skip-if = toolkit == 'android'
+support-files =
+ !/toolkit/crashreporter/test/unit/crasher_subprocess_head.js
+ !/toolkit/crashreporter/test/unit/crasher_subprocess_tail.js
+ !/toolkit/crashreporter/test/unit/head_crashreporter.js
+
+[test_content_annotation.js]
+[test_content_large_annotation.js]
+[test_content_exception_time_annotation.js]
+[test_content_oom_annotation.js]
+[test_content_memory_list.js]
+skip-if = os != 'win'
+[test_content_rust_panic.js]
+[test_content_rust_panic_multiline.js]
diff --git a/toolkit/crashreporter/test/win64UnwindInfoTests.asm b/toolkit/crashreporter/test/win64UnwindInfoTests.asm
new file mode 100644
index 0000000000..4dd5ce7646
--- /dev/null
+++ b/toolkit/crashreporter/test/win64UnwindInfoTests.asm
@@ -0,0 +1,378 @@
+; 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/.
+
+; Comments indicate stack memory layout during execution.
+; For example at the top of a function, where RIP just points to the return
+; address, the stack looks like
+; rip = [ra]
+; And after pushing rax to the stack,
+; rip = [rax][ra]
+; And then, after allocating 20h bytes on the stack,
+; rip = [..20..][rax][ra]
+; And then, after pushing a function pointer,
+; rip = [pfn][..20..][rax][ra]
+
+include ksamd64.inc
+
+.code
+
+; It helps to add padding between functions so they're not right up against
+; each other. Adds clarity to debugging, and gives a bit of leeway when
+; searching for symbols (e.g. a function whose last instruction is CALL
+; would push a return address that's in the next function.)
+PaddingBetweenFunctions macro
+ repeat 10h
+ int 3
+ endm
+endm
+
+DoCrash macro
+ mov rax, 7
+ mov byte ptr [rax], 9
+endm
+
+PaddingBetweenFunctions
+
+; There is no rip addressing mode in x64. The only way to get the value
+; of rip is to call a function, and pop it from the stack.
+WhoCalledMe proc
+ pop rax ; rax is now ra
+ push rax ; Restore ra so this function can return.
+ sub rax, 5 ; Correct for the size of the call instruction
+ ret
+WhoCalledMe endp
+
+PaddingBetweenFunctions
+
+; Any function that we expect to test against on the stack, we'll need its
+; real address. If we use function pointers in C, we'll get the address to jump
+; table entries. This bit of code at the beginning of each function will
+; return the real address we'd expect to see in stack traces.
+;
+; rcx (1st arg) = mode
+; rax (return) = address of either NO_MANS_LAND or this function.
+;
+; When mode is 0, we place the address of NO_MANS_LAND in RAX, for the function
+; to use as it wants. This is just for convenience because almost all functions
+; here need this address at some point.
+;
+; When mode is 1, the address of this function is returned.
+TestHeader macro
+ call WhoCalledMe
+ test rcx, rcx
+ je continue_test
+ ret
+continue_test:
+ inc rcx
+ call x64CrashCFITest_NO_MANS_LAND
+ xor rcx, rcx
+endm
+
+; The point of this is to add a stack frame to test against.
+; void* x64CrashCFITest_Launcher(int getAddress, void* pTestFn)
+x64CrashCFITest_Launcher proc frame
+ TestHeader
+
+ .endprolog
+ call rdx
+ ret
+x64CrashCFITest_Launcher endp
+
+PaddingBetweenFunctions
+
+; void* x64CrashCFITest_NO_MANS_LAND(uint64_t mode);
+; Not meant to be called. Only when mode = 1 in order to return its address.
+; Place this function's address on the stack so the stack scanning algorithm
+; thinks this is a return address, and places it on the stack trace.
+x64CrashCFITest_NO_MANS_LAND proc frame
+ TestHeader
+ .endprolog
+ ret
+x64CrashCFITest_NO_MANS_LAND endp
+
+PaddingBetweenFunctions
+
+; Test that we:
+; - handle unknown opcodes gracefully
+; - fall back to other stack unwind strategies if CFI doesn't work
+;
+; In order to properly unwind this frame, we'd need to fully support
+; SET_FPREG with offsets, plus restoring registers via PUSH_NONVOL.
+; To do this, sprinkle the stack with bad return addresses
+; and stack pointers.
+x64CrashCFITest_UnknownOpcode proc frame
+ TestHeader
+
+ push rax
+ .allocstack 8
+
+ push rbp
+ .pushreg rbp
+
+ push rax
+ push rsp
+ push rax
+ push rsp
+ .allocstack 20h
+ ; rsp = [rsp][pfn][rsp][pfn][rbp][pfn][ra]
+
+ lea rbp, [rsp+10h]
+ .setframe rbp, 10h
+ ; rsp = [rsp][pfn] [rsp][pfn][rbp][pfn][ra]
+ ; rbp = ^
+
+ .endprolog
+
+ ; Now modify RSP so measuring stack size from unwind ops will not help
+ ; finding the return address.
+ push rax
+ push rsp
+ ; rsp = [rsp][pfn][rsp][pfn] [rsp][pfn][rbp][pfn][ra]
+
+ DoCrash
+
+x64CrashCFITest_UnknownOpcode endp
+
+PaddingBetweenFunctions
+
+; void* x64CrashCFITest_PUSH_NONVOL(uint64_t mode);
+;
+; Test correct handling of PUSH_NONVOL unwind code.
+;
+x64CrashCFITest_PUSH_NONVOL proc frame
+ TestHeader
+
+ push r10
+ .pushreg r10
+ push r15
+ .pushreg r15
+ push rbx
+ .pushreg rbx
+ push rsi
+ .pushreg rsi
+ push rbp
+ .pushreg rbp
+ ; rsp = [rbp][rsi][rbx][r15][r10][ra]
+
+ push rax
+ .allocstack 8
+ ; rsp = [pfn][rbp][rsi][rbx][r15][r10][ra]
+
+ .endprolog
+
+ DoCrash
+
+x64CrashCFITest_PUSH_NONVOL endp
+
+PaddingBetweenFunctions
+
+; void* x64CrashCFITest_ALLOC_SMALL(uint64_t mode);
+;
+; Small allocations are between 8bytes and 512kb-8bytes
+;
+x64CrashCFITest_ALLOC_SMALL proc frame
+ TestHeader
+
+ push rax
+ push rax
+ push rax
+ push rax
+ .allocstack 20h
+ ; rsp = [pfn][pfn][pfn][pfn][ra]
+
+ .endprolog
+
+ DoCrash
+
+x64CrashCFITest_ALLOC_SMALL endp
+
+PaddingBetweenFunctions
+
+; void* x64CrashCFITest_ALLOC_LARGE(uint64_t mode);
+;
+; Allocations between 512kb and 4gb
+; Note: ReserveStackSpace() in nsTestCrasher.cpp pre-allocates stack
+; space for this.
+x64CrashCFITest_ALLOC_LARGE proc frame
+ TestHeader
+
+ sub rsp, 0a000h
+ .allocstack 0a000h
+ ; rsp = [..640kb..][ra]
+
+ mov qword ptr [rsp], rax
+ ; rsp = [pfn][..640kb-8..][ra]
+
+ .endprolog
+
+ DoCrash
+
+x64CrashCFITest_ALLOC_LARGE endp
+
+PaddingBetweenFunctions
+
+; void* x64CrashCFITest_SAVE_NONVOL(uint64_t mode);
+;
+; Test correct handling of SAVE_NONVOL unwind code.
+;
+x64CrashCFITest_SAVE_NONVOL proc frame
+ TestHeader
+
+ sub rsp, 30h
+ .allocstack 30h
+ ; rsp = [..30..][ra]
+
+ mov qword ptr [rsp+28h], r10
+ .savereg r10, 28h
+ mov qword ptr [rsp+20h], rbp
+ .savereg rbp, 20h
+ mov qword ptr [rsp+18h], rsi
+ .savereg rsi, 18h
+ mov qword ptr [rsp+10h], rbx
+ .savereg rbx, 10h
+ mov qword ptr [rsp+8], r15
+ .savereg r15, 8
+ ; rsp = [r15][rbx][rsi][rbp][r10][ra]
+
+ mov qword ptr [rsp], rax
+
+ ; rsp = [pfn][r15][rbx][rsi][rbp][r10][ra]
+
+ .endprolog
+
+ DoCrash
+
+x64CrashCFITest_SAVE_NONVOL endp
+
+PaddingBetweenFunctions
+
+; void* x64CrashCFITest_SAVE_NONVOL_FAR(uint64_t mode);
+;
+; Similar to the test above but adding 640kb to most offsets.
+; Note: ReserveStackSpace() in nsTestCrasher.cpp pre-allocates stack
+; space for this.
+x64CrashCFITest_SAVE_NONVOL_FAR proc frame
+ TestHeader
+
+ sub rsp, 0a0030h
+ .allocstack 0a0030h
+ ; rsp = [..640k..][..30..][ra]
+
+ mov qword ptr [rsp+28h+0a0000h], r10
+ .savereg r10, 28h+0a0000h
+ mov qword ptr [rsp+20h+0a0000h], rbp
+ .savereg rbp, 20h+0a0000h
+ mov qword ptr [rsp+18h+0a0000h], rsi
+ .savereg rsi, 18h+0a0000h
+ mov qword ptr [rsp+10h+0a0000h], rbx
+ .savereg rbx, 10h+0a0000h
+ mov qword ptr [rsp+8+0a0000h], r15
+ .savereg r15, 8+0a0000h
+ ; rsp = [..640k..][..8..][r15][rbx][rsi][rbp][r10][ra]
+
+ mov qword ptr [rsp], rax
+
+ ; rsp = [pfn][..640k..][r15][rbx][rsi][rbp][r10][ra]
+
+ .endprolog
+
+ DoCrash
+
+x64CrashCFITest_SAVE_NONVOL_FAR endp
+
+PaddingBetweenFunctions
+
+; void* x64CrashCFITest_SAVE_XMM128(uint64_t mode);
+;
+; Test correct handling of SAVE_XMM128 unwind code.
+x64CrashCFITest_SAVE_XMM128 proc frame
+ TestHeader
+
+ sub rsp, 30h
+ .allocstack 30h
+ ; rsp = [..30..][ra]
+
+ movdqu [rsp+20h], xmm6
+ .savexmm128 xmm6, 20h
+ ; rsp = [..20..][xmm6][ra]
+
+ movdqu [rsp+10h], xmm15
+ .savexmm128 xmm15, 10h
+ ; rsp = [..10..][xmm15][xmm6][ra]
+
+ mov qword ptr [rsp], rax
+ ; rsp = [pfn][..8..][xmm15][xmm6][ra]
+
+ .endprolog
+
+ DoCrash
+
+x64CrashCFITest_SAVE_XMM128 endp
+
+PaddingBetweenFunctions
+
+; void* x64CrashCFITest_SAVE_XMM128(uint64_t mode);
+;
+; Similar to the test above but adding 640kb to most offsets.
+; Note: ReserveStackSpace() in nsTestCrasher.cpp pre-allocates stack
+; space for this.
+x64CrashCFITest_SAVE_XMM128_FAR proc frame
+ TestHeader
+
+ sub rsp, 0a0030h
+ .allocstack 0a0030h
+ ; rsp = [..640kb..][..30..][ra]
+
+ movdqu [rsp+20h+0a0000h], xmm6
+ .savexmm128 xmm6, 20h+0a0000h
+ ; rsp = [..640kb..][..20..][xmm6][ra]
+
+ movdqu [rsp+10h+0a0000h], xmm6
+ .savexmm128 xmm15, 10h+0a0000h
+ ; rsp = [..640kb..][..10..][xmm15][xmm6][ra]
+
+ mov qword ptr [rsp], rax
+ ; rsp = [pfn][..640kb..][..8..][xmm15][xmm6][ra]
+
+ .endprolog
+
+ DoCrash
+
+x64CrashCFITest_SAVE_XMM128_FAR endp
+
+PaddingBetweenFunctions
+
+; void* x64CrashCFITest_EPILOG(uint64_t mode);
+;
+; The epilog unwind op will also set the unwind version to 2.
+; Test that we don't choke on UWOP_EPILOG or version 2 unwind info.
+x64CrashCFITest_EPILOG proc frame
+ TestHeader
+
+ push rax
+ .allocstack 8
+ ; rsp = [pfn][ra]
+
+ .endprolog
+
+ DoCrash
+
+ .beginepilog
+
+ ret
+
+x64CrashCFITest_EPILOG endp
+
+PaddingBetweenFunctions
+
+; Having an EOF symbol at the end of this file contains symbolication to this
+; file. So addresses beyond this file don't get mistakenly symbolicated as a
+; meaningful function name.
+x64CrashCFITest_EOF proc frame
+ TestHeader
+ .endprolog
+ ret
+x64CrashCFITest_EOF endp
+
+end