diff options
Diffstat (limited to 'toolkit/crashreporter/test')
79 files changed, 4329 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..13cf1fec49 --- /dev/null +++ b/toolkit/crashreporter/test/CrashTestUtils.sys.mjs @@ -0,0 +1,106 @@ +/* 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, + enablePHC: 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 +); +CrashTestUtils.enablePHC = lib.declare( + "EnablePHC", + ctypes.default_abi, + ctypes.void_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.toml b/toolkit/crashreporter/test/browser/browser.toml new file mode 100644 index 0000000000..017cf7c60c --- /dev/null +++ b/toolkit/crashreporter/test/browser/browser.toml @@ -0,0 +1,21 @@ +[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'", # Linux-specific crash type + "release_or_beta", # 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..4f16a3b3b6 --- /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.startLoadingURIString(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..1ebd9b9029 --- /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.toml", "unit_ipc/xpcshell.toml"] +if CONFIG["MOZ_PHC"]: + XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell-phc.toml", "unit_ipc/xpcshell-phc.toml"] + +BROWSER_CHROME_MANIFESTS += ["browser/browser.toml"] + +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["TARGET_CPU"] == "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..2148a60088 --- /dev/null +++ b/toolkit/crashreporter/test/nsTestCrasher.cpp @@ -0,0 +1,370 @@ +#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 "PHC.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 (mozilla::phc::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; + } +} + +extern "C" NS_EXPORT void EnablePHC() { +#ifdef MOZ_PHC + mozilla::phc::SetPHCState(mozilla::phc::PHCState::Enabled); +#endif +}; + +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..6aa5c8192e --- /dev/null +++ b/toolkit/crashreporter/test/unit/crasher_subprocess_head.js @@ -0,0 +1,32 @@ +// 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; + +// Turn PHC on so that the PHC tests work. +CrashTestUtils.enablePHC(); 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..c4011c7474 --- /dev/null +++ b/toolkit/crashreporter/test/unit/test_crash_AsyncShutdown.js @@ -0,0 +1,68 @@ +/* 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" + ); + + 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 = Promise.withResolvers(); + 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() { + Services.prefs.setIntPref("toolkit.asyncshutdown.crash_timeout", 1); + + IOUtils.profileBeforeChange.addBlocker( + "Adding a blocker that will never be resolved", + () => Promise.withResolvers().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..cc81c008df --- /dev/null +++ b/toolkit/crashreporter/test/unit/test_crash_purevirtual.js @@ -0,0 +1,21 @@ +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; + } + + // 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 Binary files differnew file mode 100644 index 0000000000..5283cc5df7 --- /dev/null +++ b/toolkit/crashreporter/test/unit/test_crash_win64cfi_infinite_code_chain.exe 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 Binary files differnew file mode 100644 index 0000000000..de48576f65 --- /dev/null +++ b/toolkit/crashreporter/test/unit/test_crash_win64cfi_infinite_entry_chain.exe 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 Binary files differnew file mode 100644 index 0000000000..ab4ce326bd --- /dev/null +++ b/toolkit/crashreporter/test/unit/test_crash_win64cfi_invalid_exception_rva.exe 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.toml b/toolkit/crashreporter/test/unit/xpcshell-phc.toml new file mode 100644 index 0000000000..278cf28193 --- /dev/null +++ b/toolkit/crashreporter/test/unit/xpcshell-phc.toml @@ -0,0 +1,12 @@ +[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.toml b/toolkit/crashreporter/test/unit/xpcshell.toml new file mode 100644 index 0000000000..ffa631d0a1 --- /dev/null +++ b/toolkit/crashreporter/test/unit/xpcshell.toml @@ -0,0 +1,152 @@ +[DEFAULT] +head = "head_crashreporter.js" +skip-if = [ + "os == '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_AsyncShutdown.js"] + +["test_crash_abort.js"] +skip-if = ["os == 'win'"] + +["test_crash_after_js_large_allocation_failure.js"] + +["test_crash_after_js_large_allocation_failure_reporting.js"] + +["test_crash_after_js_oom_recovered.js"] + +["test_crash_after_js_oom_reported.js"] + +["test_crash_after_js_oom_reported_2.js"] + +["test_crash_backgroundtask_moz_crash.js"] + +["test_crash_exc_guard.js"] +run-if = ["os == 'mac'"] +reason = "Test covering macOS-specific crash type" + +["test_crash_heap_corruption.js"] +run-if = ["os == 'win'"] +reason = "Test covering Windows-specific crash type" +run-sequentially = "very high failure rate in parallel" + +["test_crash_modules.js"] +run-if = ["os == 'win'"] +reason = "Test covering Windows-specific module handling" +run-sequentially = "very high failure rate in parallel" + +["test_crash_moz_crash.js"] + +["test_crash_oom.js"] + +["test_crash_purevirtual.js"] + +["test_crash_rust_panic.js"] + +["test_crash_rust_panic_multiline.js"] + +["test_crash_stack_overflow.js"] +skip-if = ["os != 'linux'"] +reason = "Still broken on macOS and not yet supported on Windows" + +["test_crash_terminator.js"] + +["test_crash_uncaught_exception.js"] + +["test_crash_win64cfi_alloc_large.js"] +head = "head_crashreporter.js head_win64cfi.js" +run-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" +run-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" +run-if = ["os == 'win' && bits == 64 && processor == 'x86_64'"] +reason = "Windows test specific to the x86-64 architecture" + +["test_crash_win64cfi_infinite_code_chain.js"] +head = "head_crashreporter.js head_win64cfi.js" +run-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_infinite_entry_chain.js"] +head = "head_crashreporter.js head_win64cfi.js" +run-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_invalid_exception_rva.js"] +head = "head_crashreporter.js head_win64cfi.js" +run-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" +run-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_win64cfi_push_nonvol.js"] +head = "head_crashreporter.js head_win64cfi.js" +run-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" +run-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" +run-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" +run-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" +run-if = ["os == 'win' && bits == 64 && processor == 'x86_64'"] +reason = "Windows test specific to the x86-64 architecture" + +["test_crash_win64cfi_unknown_op.js"] +head = "head_crashreporter.js head_win64cfi.js" +run-if = ["os == 'win' && bits == 64 && processor == 'x86_64'"] +reason = "Windows test specific to the x86-64 architecture" + +["test_crash_with_memory_report.js"] + +["test_crashreporter.js"] + +["test_crashreporter_appmem.js"] +# we need to skip this due to bug 838613 +skip-if = [ + "os != 'win' && os != 'linux'", + "os=='linux' && bits==32", +] + +["test_crashreporter_crash.js"] +run-sequentially = "very high failure rate in parallel" + +["test_event_files.js"] + +["test_kill.js"] + +["test_oom_annotation.js"] +run-sequentially = "very high failure rate in parallel" + +["test_override_exception_handler.js"] +skip-if = ["os != 'win'"] 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.toml b/toolkit/crashreporter/test/unit_ipc/xpcshell-phc.toml new file mode 100644 index 0000000000..af88f5d80c --- /dev/null +++ b/toolkit/crashreporter/test/unit_ipc/xpcshell-phc.toml @@ -0,0 +1,13 @@ +[DEFAULT] +skip-if = ["os == '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.toml b/toolkit/crashreporter/test/unit_ipc/xpcshell.toml new file mode 100644 index 0000000000..b7cdb7bc08 --- /dev/null +++ b/toolkit/crashreporter/test/unit_ipc/xpcshell.toml @@ -0,0 +1,24 @@ +[DEFAULT] +run-sequentially = "very high failure rate in parallel" +head = "" +skip-if = ["os == '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_exception_time_annotation.js"] + +["test_content_large_annotation.js"] + +["test_content_memory_list.js"] +skip-if = ["os != 'win'"] + +["test_content_oom_annotation.js"] + +["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 |