diff options
Diffstat (limited to 'toolkit/crashreporter/test/unit')
51 files changed, 2042 insertions, 0 deletions
diff --git a/toolkit/crashreporter/test/unit/crasher_subprocess_head.js b/toolkit/crashreporter/test/unit/crasher_subprocess_head.js new file mode 100644 index 0000000000..19525bb605 --- /dev/null +++ b/toolkit/crashreporter/test/unit/crasher_subprocess_head.js @@ -0,0 +1,29 @@ +// enable crash reporting first +var cwd = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + +// get the temp dir +var _tmpd = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); +_tmpd.initWithPath(Services.env.get("XPCSHELL_TEST_TEMP_DIR")); + +// Allow `crashReporter` to be used as an alias in the tests. +var crashReporter = Services.appinfo; + +// We need to call this or crash events go in an undefined location. +Services.appinfo.UpdateCrashEventsDir(); + +// Setting the minidump path is not allowed in content processes +var processType = Services.appinfo.processType; +if (processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) { + Services.appinfo.minidumpPath = _tmpd; +} + +var protocolHandler = Services.io + .getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); +var curDirURI = Services.io.newFileURI(cwd); +protocolHandler.setSubstitution("test", curDirURI); +const { CrashTestUtils } = ChromeUtils.importESModule( + "resource://test/CrashTestUtils.sys.mjs" +); +var crashType = CrashTestUtils.CRASH_INVALID_POINTER_DEREF; +var shouldDelay = false; diff --git a/toolkit/crashreporter/test/unit/crasher_subprocess_tail.js b/toolkit/crashreporter/test/unit/crasher_subprocess_tail.js new file mode 100644 index 0000000000..c9e415b567 --- /dev/null +++ b/toolkit/crashreporter/test/unit/crasher_subprocess_tail.js @@ -0,0 +1,20 @@ +/* import-globals-from crasher_subprocess_head.js */ + +// Let the event loop process a bit before crashing. +if (shouldDelay) { + let shouldCrashNow = false; + + Services.tm.dispatchToMainThread({ + run: () => { + shouldCrashNow = true; + }, + }); + + Services.tm.spinEventLoopUntil( + "Test(crasher_subprocess_tail.js:shouldDelay)", + () => shouldCrashNow + ); +} + +// now actually crash +CrashTestUtils.crash(crashType); diff --git a/toolkit/crashreporter/test/unit/head_crashreporter.js b/toolkit/crashreporter/test/unit/head_crashreporter.js new file mode 100644 index 0000000000..c37c8acf8c --- /dev/null +++ b/toolkit/crashreporter/test/unit/head_crashreporter.js @@ -0,0 +1,347 @@ +const { makeFakeAppDir } = ChromeUtils.importESModule( + "resource://testing-common/AppData.sys.mjs" +); +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +function getEventDir() { + return PathUtils.join(do_get_tempdir().path, "crash-events"); +} + +function sendCommandAsync(command) { + return new Promise(resolve => { + sendCommand(command, resolve); + }); +} + +/* + * Run an xpcshell subprocess and crash it. + * + * @param setup + * A string of JavaScript code to execute in the subprocess + * before crashing. If this is a function and not a string, + * it will have .toSource() called on it, and turned into + * a call to itself. (for programmer convenience) + * This code will be evaluted between crasher_subprocess_head.js + * and crasher_subprocess_tail.js, so it will have access + * to everything defined in crasher_subprocess_head.js, + * which includes "crashReporter", a variable holding + * the crash reporter service. + * + * @param callback + * A JavaScript function to be called after the subprocess + * crashes. It will be passed (minidump, extra, extrafile), where + * - minidump is an nsIFile of the minidump file produced, + * - extra is an object containing the key,value pairs from + * the .extra file. + * - extrafile is an nsIFile of the extra file + * + * @param canReturnZero + * If true, the subprocess may return with a zero exit code. + * Certain types of crashes may not cause the process to + * exit with an error. + * + */ +async function do_crash(setup, callback, canReturnZero) { + // get current process filename (xpcshell) + let bin = Services.dirsvc.get("XREExeF", Ci.nsIFile); + if (!bin.exists()) { + // weird, can't find xpcshell binary? + do_throw("Can't find xpcshell binary!"); + } + // get Gre dir (GreD) + let greD = Services.dirsvc.get("GreD", Ci.nsIFile); + let headfile = do_get_file("crasher_subprocess_head.js"); + let tailfile = do_get_file("crasher_subprocess_tail.js"); + // run xpcshell -g GreD -f head -e "some setup code" -f tail + let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess); + process.init(bin); + let args = ["-g", greD.path, "-f", headfile.path]; + if (setup) { + if (typeof setup == "function") { + // funky, but convenient + setup = "(" + setup.toSource() + ")();"; + } + args.push("-e", setup); + } + args.push("-f", tailfile.path); + + let crashD = do_get_tempdir(); + crashD.append("crash-events"); + if (!crashD.exists()) { + crashD.create(crashD.DIRECTORY_TYPE, 0o700); + } + + Services.env.set("CRASHES_EVENTS_DIR", crashD.path); + + try { + process.run(true, args, args.length); + } catch (ex) { + // on Windows we exit with a -1 status when crashing. + } finally { + Services.env.set("CRASHES_EVENTS_DIR", ""); + } + + if (!canReturnZero) { + // should exit with an error (should have crashed) + Assert.notEqual(process.exitValue, 0); + } + + await handleMinidump(callback); +} + +function getMinidump() { + let en = do_get_tempdir().directoryEntries; + while (en.hasMoreElements()) { + let f = en.nextFile; + if (f.leafName.substr(-4) == ".dmp") { + return f; + } + } + + return null; +} + +function getMinidumpAnalyzerPath() { + const binSuffix = AppConstants.platform === "win" ? ".exe" : ""; + const exeName = "minidump-analyzer" + binSuffix; + + let exe = Services.dirsvc.get("GreBinD", Ci.nsIFile); + exe.append(exeName); + + return exe; +} + +function runMinidumpAnalyzer(dumpFile, additionalArgs) { + let bin = getMinidumpAnalyzerPath(); + let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess); + process.init(bin); + let args = []; + if (additionalArgs) { + args = args.concat(additionalArgs); + } + args.push(dumpFile.path); + process.run(true /* blocking */, args, args.length); +} + +async function handleMinidump(callback) { + // find minidump + let minidump = getMinidump(); + + if (minidump == null) { + do_throw("No minidump found!"); + } + + let extrafile = minidump.clone(); + extrafile.leafName = extrafile.leafName.slice(0, -4) + ".extra"; + + let memoryfile = minidump.clone(); + memoryfile.leafName = memoryfile.leafName.slice(0, -4) + ".memory.json.gz"; + + let cleanup = async function () { + for (let file of [minidump, extrafile, memoryfile]) { + while (file.exists()) { + try { + file.remove(false); + } catch (e) { + // On Windows the file may be locked, wait briefly and try again + await new Promise(resolve => do_timeout(50, resolve)); + } + } + } + }; + + // Just in case, don't let these files linger. + registerCleanupFunction(cleanup); + + Assert.ok(extrafile.exists()); + let extra = await IOUtils.readJSON(extrafile.path); + + if (callback) { + await callback(minidump, extra, extrafile, memoryfile); + } + + await cleanup(); +} + +function spinEventLoop() { + return new Promise(resolve => { + executeSoon(resolve); + }); +} + +/** + * Helper for testing a content process crash. + * + * This variant accepts a setup function which runs in the content process + * to set data as needed _before_ the crash. The tail file triggers a generic + * crash after setup. + */ +async function do_content_crash(setup, callback) { + do_load_child_test_harness(); + + // Setting the minidump path won't work in the child, so we need to do + // that here. + Services.appinfo.minidumpPath = do_get_tempdir(); + + /* import-globals-from ../unit/crasher_subprocess_head.js */ + /* import-globals-from ../unit/crasher_subprocess_tail.js */ + + let headfile = do_get_file("../unit/crasher_subprocess_head.js"); + let tailfile = do_get_file("../unit/crasher_subprocess_tail.js"); + if (setup) { + if (typeof setup == "function") { + // funky, but convenient + setup = "(" + setup.toSource() + ")();"; + } + } + + do_get_profile(); + await makeFakeAppDir(); + await sendCommandAsync('load("' + headfile.path.replace(/\\/g, "/") + '");'); + if (setup) { + await sendCommandAsync(setup); + } + await sendCommandAsync('load("' + tailfile.path.replace(/\\/g, "/") + '");'); + await spinEventLoop(); + + let minidump = getMinidump(); + let id = minidump.leafName.slice(0, -4); + await Services.crashmanager.ensureCrashIsPresent(id); + try { + await handleMinidump(callback); + } catch (x) { + do_report_unexpected_exception(x); + } +} + +/** + * Helper for testing a content process crash. + * + * This variant accepts a trigger function which runs in the content process + * and does something to _trigger_ the crash. + */ +async function do_triggered_content_crash(trigger, callback) { + do_load_child_test_harness(); + + // Setting the minidump path won't work in the child, so we need to do + // that here. + Services.appinfo.minidumpPath = do_get_tempdir(); + + /* import-globals-from ../unit/crasher_subprocess_head.js */ + + let headfile = do_get_file("../unit/crasher_subprocess_head.js"); + if (trigger) { + if (typeof trigger == "function") { + // funky, but convenient + trigger = "(" + trigger.toSource() + ")();"; + } + } + + do_get_profile(); + await makeFakeAppDir(); + await sendCommandAsync('load("' + headfile.path.replace(/\\/g, "/") + '");'); + await sendCommandAsync(trigger); + await spinEventLoop(); + let id = getMinidump().leafName.slice(0, -4); + await Services.crashmanager.ensureCrashIsPresent(id); + try { + await handleMinidump(callback); + } catch (x) { + do_report_unexpected_exception(x); + } +} + +/* + * Run the `crash` backgroundtask subprocess, crashing it in the + * specified manner. + * + * @param crashType Integer `CrashTestUtils.CRASH_...` code. + * @param crashExtras Dictionary of key-value pairs to include in + * minidump extras. + * + * @param callback + * A JavaScript function to be called after the subprocess + * crashes. It will be passed (minidump, extra, extrafile), where + * - minidump is an nsIFile of the minidump file produced, + * - extra is an object containing the key,value pairs from + * the .extra file. + * - extrafile is an nsIFile of the extra file + * + * @param canReturnZero + * If true, the subprocess may return with a zero exit code. + * Certain types of crashes may not cause the process to + * exit with an error. + * + */ +async function do_backgroundtask_crash( + crashType, + crashExtras, + callback, + canReturnZero +) { + Assert.ok(AppConstants.MOZ_BACKGROUNDTASKS); + + // Get full path to application (not xpcshell) + let bin = Services.dirsvc.get("GreBinD", Ci.nsIFile); + if (AppConstants.platform === "win") { + bin.append(AppConstants.MOZ_APP_NAME + ".exe"); + } else { + bin.append(AppConstants.MOZ_APP_NAME); + } + + // run `application --backgroundtask crash ...`. + let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess); + process.init(bin); + + let args = ["--backgroundtask", "crash"]; + args.push(crashType.toString()); + + // Sorted to be deterministic. + let sorted = Object.entries(crashExtras).sort((a, b) => a[0] < b[0]); + for (let [key, value] of sorted) { + args.push(key); + args.push(value); + } + + let crashD = do_get_tempdir(); + crashD.append("crash-events"); + if (!crashD.exists()) { + crashD.create(crashD.DIRECTORY_TYPE, 0o700); + } + + Services.env.set("CRASHES_EVENTS_DIR", crashD.path); + + // Ensure `resource://testing-common` gets mapped. + let protocolHandler = Services.io + .getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + + let uri = protocolHandler.getSubstitution("testing-common"); + Assert.ok(uri, "resource://testing-common is not substituted"); + + // The equivalent of _TESTING_MODULES_DIR in xpcshell. + Services.env.set("XPCSHELL_TESTING_MODULES_URI", uri.spec); + + try { + process.run(true, args, args.length); + } catch (ex) { + // on Windows we exit with a -1 status when crashing. + } finally { + Services.env.set("CRASHES_EVENTS_DIR", ""); + Services.env.set("XPCSHELL_TESTING_MODULES_URI", ""); + } + + if (!canReturnZero) { + // should exit with an error (should have crashed) + Assert.notEqual(process.exitValue, 0); + } + + await handleMinidump(callback); +} + +// Import binary APIs via js-ctypes. +var { CrashTestUtils } = ChromeUtils.importESModule( + "resource://test/CrashTestUtils.sys.mjs" +); diff --git a/toolkit/crashreporter/test/unit/head_win64cfi.js b/toolkit/crashreporter/test/unit/head_win64cfi.js new file mode 100644 index 0000000000..4df99213db --- /dev/null +++ b/toolkit/crashreporter/test/unit/head_win64cfi.js @@ -0,0 +1,215 @@ +/* import-globals-from head_crashreporter.js */ + +let gTestCrasherSyms = null; +let gModules = null; + +// Returns the offset (int) of an IP with a given base address. +// This is effectively (ip - base), except a bit more complication due to +// Javascript's shaky handling of 64-bit integers. +// base & ip are passed as hex strings. +function getModuleOffset(base, ip) { + let i = 0; + // Find where the two addresses diverge, which enables us to perform a 32-bit + // subtraction. + // e.g. "0x1111111111112222" + // - "0x1111111111111111" + // becomes 2222 - 1111 + for (; i < base.length; ++i) { + if (base[i] != ip[i]) { + break; + } + } + if (i == base.length) { + return 0; + } + let lhs2 = "0x" + base.substring(i); + let rhs2 = "0x" + ip.substring(i); + return parseInt(rhs2) - parseInt(lhs2); +} + +// Uses gTestCrasherSyms to convert an address to a symbol. +function findNearestTestCrasherSymbol(addr) { + addr += 1; // Breakpad sometimes offsets addresses; correct for this. + let closestDistance = null; + let closestSym = null; + for (let sym in gTestCrasherSyms) { + if (addr >= gTestCrasherSyms[sym]) { + let thisDistance = addr - gTestCrasherSyms[sym]; + if (closestDistance === null || thisDistance < closestDistance) { + closestDistance = thisDistance; + closestSym = sym; + } + } + } + if (closestSym === null) { + return null; + } + return { symbol: closestSym, offset: closestDistance }; +} + +// Populate known symbols for testcrasher.dll. +// Use the same prop names as from CrashTestUtils to avoid the need for mapping. +function initTestCrasherSymbols() { + gTestCrasherSyms = {}; + for (let k in CrashTestUtils) { + // Not all keys here are valid symbol names. getWin64CFITestFnAddrOffset + // will return 0 in those cases, no need to filter here. + if (Number.isInteger(CrashTestUtils[k])) { + let t = CrashTestUtils.getWin64CFITestFnAddrOffset(CrashTestUtils[k]); + if (t > 0) { + gTestCrasherSyms[k] = t; + } + } + } +} + +function stackFrameToString(frameIndex, frame) { + // Calculate the module offset. + let ip = frame.ip; + let symbol = ""; + let moduleOffset = "unknown_offset"; + let filename = "unknown_module"; + + if ( + typeof frame.module_index !== "undefined" && + frame.module_index >= 0 && + frame.module_index < gModules.length + ) { + let base = gModules[frame.module_index].base_addr; + moduleOffset = getModuleOffset(base, ip); + filename = gModules[frame.module_index].filename; + + if (filename === "testcrasher.dll") { + let nearestSym = findNearestTestCrasherSymbol(moduleOffset); + if (nearestSym !== null) { + symbol = nearestSym.symbol + "+" + nearestSym.offset.toString(16); + } + } + } + + let ret = + "frames[" + + frameIndex + + "] ip=" + + ip + + " " + + symbol + + ", module:" + + filename + + ", trust:" + + frame.trust + + ", moduleOffset:" + + moduleOffset.toString(16); + return ret; +} + +function dumpStackFrames(frames, maxFrames) { + for (let i = 0; i < Math.min(maxFrames, frames.length); ++i) { + info(stackFrameToString(i, frames[i])); + } +} + +// Test that the top of the given stack (from extra data) matches the given +// expected frames. +// +// expected is { symbol: "", trust: "" } +function assertStack(stack, expected) { + for (let i = 0; i < stack.length; ++i) { + if (i >= expected.length) { + ok("Top stack frames were expected"); + return; + } + let frame = stack[i]; + let expectedFrame = expected[i]; + let dumpThisFrame = function () { + info(" Actual frame: " + stackFrameToString(i, frame)); + info( + "Expected { symbol: " + + expectedFrame.symbol + + ", trust: " + + expectedFrame.trust + + "}" + ); + }; + + if (expectedFrame.trust) { + if (expectedFrame.trust.startsWith("!")) { + // A "!" prefix on the frame trust matching is a logical "not". + if (frame.trust === expectedFrame.trust.substring(1)) { + dumpThisFrame(); + info("Expected frame trust matched when it should not have."); + ok(false); + } + } else if (frame.trust !== expectedFrame.trust) { + dumpThisFrame(); + info("Expected frame trust did not match."); + ok(false); + } + } + + if (expectedFrame.symbol) { + if (typeof frame.module_index === "undefined") { + // Without a module_index, it happened in an unknown module. Currently + // you can't specify an expected "unknown" module. + info("Unknown symbol in unknown module."); + ok(false); + } + if (frame.module_index < 0 || frame.module_index >= gModules.length) { + dumpThisFrame(); + info("Unknown module."); + ok(false); + return; + } + let base = gModules[frame.module_index].base_addr; + let moduleOffset = getModuleOffset(base, frame.ip); + let filename = gModules[frame.module_index].filename; + if (filename == "testcrasher.dll") { + let nearestSym = findNearestTestCrasherSymbol(moduleOffset); + if (nearestSym === null) { + dumpThisFrame(); + info("Unknown symbol."); + ok(false); + return; + } + + if (nearestSym.symbol !== expectedFrame.symbol) { + dumpThisFrame(); + info("Mismatching symbol."); + ok(false); + } + } + } + } +} + +// Performs a crash, runs minidump-analyzer, and checks expected stack analysis. +// +// how: The crash to perform. Constants defined in both CrashTestUtils.jsm +// and nsTestCrasher.cpp (i.e. CRASH_X64CFI_PUSH_NONVOL) +// expectedStack: An array of {"symbol", "trust"} where trust is "cfi", +// "context", "scan", et al. May be null if you don't need to check the stack. +// minidumpAnalyzerArgs: An array of additional arguments to pass to +// minidump-analyzer.exe. +async function do_x64CFITest(how, expectedStack, minidumpAnalyzerArgs) { + // Setup is run in the subprocess so we cannot use any closures. + let setupFn = "crashType = CrashTestUtils." + how + ";"; + + let callbackFn = async function (minidumpFile, extra, extraFile) { + runMinidumpAnalyzer(minidumpFile, minidumpAnalyzerArgs); + + // Refresh updated extra data + extra = await IOUtils.readJSON(extraFile.path); + + initTestCrasherSymbols(); + let stackTraces = extra.StackTraces; + let crashingThreadIndex = stackTraces.crash_info.crashing_thread; + gModules = stackTraces.modules; + let crashingFrames = stackTraces.threads[crashingThreadIndex].frames; + + dumpStackFrames(crashingFrames, 10); + + assertStack(crashingFrames, expectedStack); + }; + + do_crash(setupFn, callbackFn, true, true); +} diff --git a/toolkit/crashreporter/test/unit/test_crash_AsyncShutdown.js b/toolkit/crashreporter/test/unit/test_crash_AsyncShutdown.js new file mode 100644 index 0000000000..66cc937f28 --- /dev/null +++ b/toolkit/crashreporter/test/unit/test_crash_AsyncShutdown.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that AsyncShutdown report errors correctly + +// Note: these functions are evaluated in their own process, hence the need +// to import modules into each function. + +function setup_crash() { + const { AsyncShutdown } = ChromeUtils.importESModule( + "resource://gre/modules/AsyncShutdown.sys.mjs" + ); + const { PromiseUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PromiseUtils.sys.mjs" + ); + + Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true); + Services.prefs.setIntPref("toolkit.asyncshutdown.crash_timeout", 10); + + let TOPIC = "testing-async-shutdown-crash"; + let phase = AsyncShutdown._getPhase(TOPIC); + phase.addBlocker("A blocker that is never satisfied", function () { + dump("Installing blocker\n"); + let deferred = PromiseUtils.defer(); + return deferred.promise; + }); + + Services.obs.notifyObservers(null, TOPIC); + dump(new Error().stack + "\n"); + dump("Waiting for crash\n"); +} + +function after_crash(mdump, extra) { + info("after crash: " + extra.AsyncShutdownTimeout); + let data = JSON.parse(extra.AsyncShutdownTimeout); + Assert.equal(data.phase, "testing-async-shutdown-crash"); + Assert.equal(data.conditions[0].name, "A blocker that is never satisfied"); + // This test spawns subprocesses by using argument "-e" of xpcshell, so + // this is the filename known to xpcshell. + Assert.equal(data.conditions[0].filename, "-e"); +} + +// Test that AsyncShutdown + IOUtils reports errors correctly., + +function setup_ioutils_crash() { + const { PromiseUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PromiseUtils.sys.mjs" + ); + + Services.prefs.setIntPref("toolkit.asyncshutdown.crash_timeout", 1); + + IOUtils.profileBeforeChange.addBlocker( + "Adding a blocker that will never be resolved", + () => PromiseUtils.defer().promise + ); + + Services.startup.advanceShutdownPhase( + Services.startup.SHUTDOWN_PHASE_APPSHUTDOWN + ); + dump("Waiting for crash\n"); +} + +function after_ioutils_crash(mdump, extra) { + info("after IOUtils crash: " + extra.AsyncShutdownTimeout); + let data = JSON.parse(extra.AsyncShutdownTimeout); + Assert.equal( + data.phase, + "IOUtils: waiting for profileBeforeChange IO to complete" + ); +} + +add_task(async function run_test() { + await do_crash(setup_crash, after_crash); + await do_crash(setup_ioutils_crash, after_ioutils_crash); +}); diff --git a/toolkit/crashreporter/test/unit/test_crash_abort.js b/toolkit/crashreporter/test/unit/test_crash_abort.js new file mode 100644 index 0000000000..aa602dacb4 --- /dev/null +++ b/toolkit/crashreporter/test/unit/test_crash_abort.js @@ -0,0 +1,17 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function run_test() { + // Try crashing with an abort(). + await do_crash( + function () { + crashType = CrashTestUtils.CRASH_ABORT; + crashReporter.annotateCrashReport("TestKey", "TestValue"); + }, + function (mdump, extra) { + Assert.equal(extra.TestKey, "TestValue"); + }, + // process will exit with a zero exit status + true + ); +}); diff --git a/toolkit/crashreporter/test/unit/test_crash_after_js_large_allocation_failure.js b/toolkit/crashreporter/test/unit/test_crash_after_js_large_allocation_failure.js new file mode 100644 index 0000000000..611ba9b6ac --- /dev/null +++ b/toolkit/crashreporter/test/unit/test_crash_after_js_large_allocation_failure.js @@ -0,0 +1,23 @@ +add_task(async function run_test() { + if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) { + dump( + "INFO | test_crash_after_js_large_allocation_failure.js | Can't test crashreporter in a non-libxul build.\n" + ); + return; + } + + await do_crash( + function () { + crashType = CrashTestUtils.CRASH_MOZ_CRASH; + crashReporter.annotateCrashReport("TestKey", "Yes"); + Cu.getJSTestingFunctions().reportLargeAllocationFailure(); + Cu.forceGC(); + }, + function (mdump, extra) { + Assert.equal(extra.TestKey, "Yes"); + Assert.equal(false, "JSOutOfMemory" in extra); + Assert.equal(extra.JSLargeAllocationFailure, "Recovered"); + }, + true + ); +}); diff --git a/toolkit/crashreporter/test/unit/test_crash_after_js_large_allocation_failure_reporting.js b/toolkit/crashreporter/test/unit/test_crash_after_js_large_allocation_failure_reporting.js new file mode 100644 index 0000000000..cdae0f2d8e --- /dev/null +++ b/toolkit/crashreporter/test/unit/test_crash_after_js_large_allocation_failure_reporting.js @@ -0,0 +1,27 @@ +add_task(async function run_test() { + if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) { + dump( + "INFO | test_crash_after_js_oom_reporting.js | Can't test crashreporter in a non-libxul build.\n" + ); + return; + } + + await do_crash( + function () { + crashType = CrashTestUtils.CRASH_MOZ_CRASH; + crashReporter.annotateCrashReport("TestKey", "Yes"); + + function crashWhileReporting() { + CrashTestUtils.crash(crashType); + } + + Services.obs.addObserver(crashWhileReporting, "memory-pressure"); + Cu.getJSTestingFunctions().reportLargeAllocationFailure(); + }, + function (mdump, extra) { + Assert.equal(extra.TestKey, "Yes"); + Assert.equal(extra.JSLargeAllocationFailure, "Reporting"); + }, + true + ); +}); diff --git a/toolkit/crashreporter/test/unit/test_crash_after_js_oom_recovered.js b/toolkit/crashreporter/test/unit/test_crash_after_js_oom_recovered.js new file mode 100644 index 0000000000..7b2491bc0b --- /dev/null +++ b/toolkit/crashreporter/test/unit/test_crash_after_js_oom_recovered.js @@ -0,0 +1,22 @@ +add_task(async function run_test() { + if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) { + dump( + "INFO | test_crash_after_js_oom_recovered.js | Can't test crashreporter in a non-libxul build.\n" + ); + return; + } + + await do_crash( + function () { + crashType = CrashTestUtils.CRASH_MOZ_CRASH; + crashReporter.annotateCrashReport("TestKey", "Yes"); + Cu.getJSTestingFunctions().reportOutOfMemory(); + Cu.forceGC(); + }, + function (mdump, extra) { + Assert.equal(extra.TestKey, "Yes"); + Assert.equal(extra.JSOutOfMemory, "Recovered"); + }, + true + ); +}); diff --git a/toolkit/crashreporter/test/unit/test_crash_after_js_oom_reported.js b/toolkit/crashreporter/test/unit/test_crash_after_js_oom_reported.js new file mode 100644 index 0000000000..8796aee3c0 --- /dev/null +++ b/toolkit/crashreporter/test/unit/test_crash_after_js_oom_reported.js @@ -0,0 +1,36 @@ +add_task(async function run_test() { + if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) { + dump( + "INFO | test_crash_after_js_oom_reported.js | Can't test crashreporter in a non-libxul build.\n" + ); + return; + } + + await do_crash( + function () { + crashType = CrashTestUtils.CRASH_MOZ_CRASH; + crashReporter.annotateCrashReport("TestKey", "Yes"); + + // GC now to avoid having it happen randomly later, which would make the + // test bogusly fail. See comment below. + Cu.forceGC(); + + Cu.getJSTestingFunctions().reportOutOfMemory(); + }, + function (mdump, extra) { + Assert.equal(extra.TestKey, "Yes"); + + // The JSOutOfMemory field is absent if the JS engine never reported OOM, + // "Reported" if it did, and "Recovered" if it reported OOM but + // subsequently completed a full GC cycle. Since this test calls + // reportOutOfMemory() and then crashes, we expect "Reported". + // + // Theoretically, GC can happen any time, so it is just possible that + // this property could be "Recovered" even if the implementation is + // correct. More likely, though, that indicates a bug, so only accept + // "Reported". + Assert.equal(extra.JSOutOfMemory, "Reported"); + }, + true + ); +}); diff --git a/toolkit/crashreporter/test/unit/test_crash_after_js_oom_reported_2.js b/toolkit/crashreporter/test/unit/test_crash_after_js_oom_reported_2.js new file mode 100644 index 0000000000..ba67981444 --- /dev/null +++ b/toolkit/crashreporter/test/unit/test_crash_after_js_oom_reported_2.js @@ -0,0 +1,28 @@ +add_task(async function run_test() { + if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) { + dump( + "INFO | test_crash_after_js_oom_reported_2.js | Can't test crashreporter in a non-libxul build.\n" + ); + return; + } + + await do_crash( + function () { + crashType = CrashTestUtils.CRASH_MOZ_CRASH; + crashReporter.annotateCrashReport("TestKey", "Yes"); + Cu.getJSTestingFunctions().reportOutOfMemory(); + Cu.forceGC(); // recover from first OOM + Cu.getJSTestingFunctions().reportOutOfMemory(); + }, + function (mdump, extra) { + Assert.equal(extra.TestKey, "Yes"); + + // Technically, GC can happen at any time, but it would be really + // peculiar for it to happen again heuristically right after a GC was + // forced. If extra.JSOutOfMemory is "Recovered" here, that's most + // likely a bug in the error reporting machinery. + Assert.equal(extra.JSOutOfMemory, "Reported"); + }, + true + ); +}); diff --git a/toolkit/crashreporter/test/unit/test_crash_backgroundtask_moz_crash.js b/toolkit/crashreporter/test/unit/test_crash_backgroundtask_moz_crash.js new file mode 100644 index 0000000000..847e260925 --- /dev/null +++ b/toolkit/crashreporter/test/unit/test_crash_backgroundtask_moz_crash.js @@ -0,0 +1,25 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function run_test() { + if (!AppConstants.MOZ_BACKGROUNDTASKS) { + return; + } + + // Try crashing background task with a runtime abort + await do_backgroundtask_crash( + CrashTestUtils.CRASH_MOZ_CRASH, + { TestKey: "TestValue" }, + function (mdump, extra) { + Assert.equal(extra.TestKey, "TestValue"); + Assert.equal(extra.BackgroundTaskMode, "1"); + Assert.equal(extra.BackgroundTaskName, "crash"); + Assert.equal(extra.HeadlessMode, "1"); + Assert.equal(false, "OOMAllocationSize" in extra); + Assert.equal(false, "JSOutOfMemory" in extra); + Assert.equal(false, "JSLargeAllocationFailure" in extra); + }, + // process will exit with a zero exit status + true + ); +}); diff --git a/toolkit/crashreporter/test/unit/test_crash_exc_guard.js b/toolkit/crashreporter/test/unit/test_crash_exc_guard.js new file mode 100644 index 0000000000..f7daffe68c --- /dev/null +++ b/toolkit/crashreporter/test/unit/test_crash_exc_guard.js @@ -0,0 +1,30 @@ +add_task(async function run_test() { + if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) { + dump( + "INFO | test_crash_exc_guard.js | Can't test crashreporter in a non-libxul build.\n" + ); + return; + } + + // Try crashing by closing a guarded file descriptor + await do_crash( + function () { + crashType = CrashTestUtils.CRASH_EXC_GUARD; + crashReporter.annotateCrashReport("TestKey", "TestValue"); + }, + async function (mdump, extra, extraFile) { + runMinidumpAnalyzer(mdump); + + // Refresh updated extra data + extra = await IOUtils.readJSON(extraFile.path); + + Assert.equal( + extra.StackTraces.crash_info.type, + "EXC_GUARD / GUARD_TYPE_FD / GUARD_EXC_CLOSE" + ); + Assert.equal(extra.TestKey, "TestValue"); + }, + // process will exit with a zero exit status + true + ); +}); diff --git a/toolkit/crashreporter/test/unit/test_crash_heap_corruption.js b/toolkit/crashreporter/test/unit/test_crash_heap_corruption.js new file mode 100644 index 0000000000..0530b8b074 --- /dev/null +++ b/toolkit/crashreporter/test/unit/test_crash_heap_corruption.js @@ -0,0 +1,27 @@ +add_task(async function run_test() { + if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) { + dump( + "INFO | test_crash_heap_corruption.js | Can't test crashreporter in a non-libxul build.\n" + ); + return; + } + + // Try crashing with a STATUS_HEAP_CORRUPTION exception + await do_crash( + function () { + crashType = CrashTestUtils.CRASH_HEAP_CORRUPTION; + crashReporter.annotateCrashReport("TestKey", "TestValue"); + }, + async function (mdump, extra, extraFile) { + runMinidumpAnalyzer(mdump); + + // Refresh updated extra data + extra = await IOUtils.readJSON(extraFile.path); + + Assert.equal(extra.StackTraces.crash_info.type, "STATUS_HEAP_CORRUPTION"); + Assert.equal(extra.TestKey, "TestValue"); + }, + // process will exit with a zero exit status + true + ); +}); diff --git a/toolkit/crashreporter/test/unit/test_crash_modules.js b/toolkit/crashreporter/test/unit/test_crash_modules.js new file mode 100644 index 0000000000..dd14e77dab --- /dev/null +++ b/toolkit/crashreporter/test/unit/test_crash_modules.js @@ -0,0 +1,44 @@ +add_task(async function run_test() { + if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) { + dump( + "INFO | test_crash_modules.js | Can't test crashreporter in a non-libxul build.\n" + ); + return; + } + + await do_crash( + function () { + const { ctypes } = ChromeUtils.importESModule( + "resource://gre/modules/ctypes.sys.mjs" + ); + // Load and unload a DLL so that it will show up as unloaded in the minidump + let lib = ctypes.open("wininet"); + lib.close(); + }, + async function (mdump, extra, extraFile) { + runMinidumpAnalyzer(mdump); + + // Refresh updated extra data + extra = await IOUtils.readJSON(extraFile.path); + + // Check unloaded modules + const unloadedModules = extra.StackTraces.unloaded_modules; + Assert.ok(!!unloadedModules, "The unloaded_modules field exists"); + Assert.notEqual(unloadedModules.find(e => e.filename == "wininet.DLL")); + + // Check the module signature information + const sigInfo = JSON.parse(extra.ModuleSignatureInfo); + Assert.ok( + !!sigInfo["Microsoft Windows"], + "The module signature info contains valid data" + ); + Assert.greater( + sigInfo["Microsoft Windows"].length, + 0, + "Multiple signed binaries were found" + ); + }, + // process will exit with a zero exit status + true + ); +}); diff --git a/toolkit/crashreporter/test/unit/test_crash_moz_crash.js b/toolkit/crashreporter/test/unit/test_crash_moz_crash.js new file mode 100644 index 0000000000..6016957d44 --- /dev/null +++ b/toolkit/crashreporter/test/unit/test_crash_moz_crash.js @@ -0,0 +1,17 @@ +add_task(async function run_test() { + // Try crashing with a runtime abort + await do_crash( + function () { + crashType = CrashTestUtils.CRASH_MOZ_CRASH; + crashReporter.annotateCrashReport("TestKey", "TestValue"); + }, + function (mdump, extra) { + Assert.equal(extra.TestKey, "TestValue"); + Assert.equal(false, "OOMAllocationSize" in extra); + Assert.equal(false, "JSOutOfMemory" in extra); + Assert.equal(false, "JSLargeAllocationFailure" in extra); + }, + // process will exit with a zero exit status + true + ); +}); diff --git a/toolkit/crashreporter/test/unit/test_crash_oom.js b/toolkit/crashreporter/test/unit/test_crash_oom.js new file mode 100644 index 0000000000..3b32da4ab8 --- /dev/null +++ b/toolkit/crashreporter/test/unit/test_crash_oom.js @@ -0,0 +1,21 @@ +add_task(async function run_test() { + if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) { + dump( + "INFO | test_crash_oom.js | Can't test crashreporter in a non-libxul build.\n" + ); + return; + } + + await do_crash( + function () { + crashType = CrashTestUtils.CRASH_OOM; + }, + function (mdump, extra) { + Assert.ok("OOMAllocationSize" in extra); + Assert.ok(Number(extra.OOMAllocationSize) > 0); + Assert.ok("TotalPhysicalMemory" in extra); + Assert.ok(Number(extra.TotalPhysicalMemory) >= 0); + }, + true + ); +}); diff --git a/toolkit/crashreporter/test/unit/test_crash_phc.js b/toolkit/crashreporter/test/unit/test_crash_phc.js new file mode 100644 index 0000000000..54bc8ec34e --- /dev/null +++ b/toolkit/crashreporter/test/unit/test_crash_phc.js @@ -0,0 +1,59 @@ +function check(extra, kind, size, hasFreeStack) { + Assert.equal(extra.PHCKind, kind); + + // This is a string holding a decimal address. + Assert.ok(/^\d+$/.test(extra.PHCBaseAddress)); + + Assert.equal(extra.PHCUsableSize, size); + + // These are strings holding comma-separated lists of decimal addresses. + // Sometimes on Mac they have a single entry. + Assert.ok(/^(\d+,)*\d+$/.test(extra.PHCAllocStack)); + if (hasFreeStack) { + Assert.ok(/^(\d+,)*\d+$/.test(extra.PHCFreeStack)); + } else { + Assert.ok(!extra.hasOwnProperty("PHCFreeStack")); + } +} + +add_task(async function run_test() { + if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) { + dump( + "INFO | test_crash_phc.js | Can't test crashreporter in a non-libxul build.\n" + ); + return; + } + + await do_crash( + function () { + crashType = CrashTestUtils.CRASH_PHC_USE_AFTER_FREE; + }, + function (mdump, extra) { + // CRASH_PHC_USE_AFTER_FREE uses 32 for the size. + check(extra, "FreedPage", 32, /* hasFreeStack */ true); + }, + true + ); + + await do_crash( + function () { + crashType = CrashTestUtils.CRASH_PHC_DOUBLE_FREE; + }, + function (mdump, extra) { + // CRASH_PHC_DOUBLE_FREE uses 64 for the size. + check(extra, "FreedPage", 64, /* hasFreeStack */ true); + }, + true + ); + + do_crash( + function () { + crashType = CrashTestUtils.CRASH_PHC_BOUNDS_VIOLATION; + }, + function (mdump, extra) { + // CRASH_PHC_BOUNDS_VIOLATION uses 96 for the size. + check(extra, "GuardPage", 96, /* hasFreeStack */ false); + }, + true + ); +}); diff --git a/toolkit/crashreporter/test/unit/test_crash_purevirtual.js b/toolkit/crashreporter/test/unit/test_crash_purevirtual.js new file mode 100644 index 0000000000..e482eaa415 --- /dev/null +++ b/toolkit/crashreporter/test/unit/test_crash_purevirtual.js @@ -0,0 +1,29 @@ +add_task(async function run_test() { + if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) { + dump( + "INFO | test_crash_purevirtual.js | Can't test crashreporter in a non-libxul build.\n" + ); + return; + } + + var isOSX = "nsILocalFileMac" in Ci; + if (isOSX) { + dump( + "INFO | test_crash_purevirtual.js | TODO: purecalls not caught on OS X\n" + ); + return; + } + + // Try crashing with a pure virtual call + await do_crash( + function () { + crashType = CrashTestUtils.CRASH_PURE_VIRTUAL_CALL; + crashReporter.annotateCrashReport("TestKey", "TestValue"); + }, + function (mdump, extra) { + Assert.equal(extra.TestKey, "TestValue"); + }, + // process will exit with a zero exit status + true + ); +}); diff --git a/toolkit/crashreporter/test/unit/test_crash_rust_panic.js b/toolkit/crashreporter/test/unit/test_crash_rust_panic.js new file mode 100644 index 0000000000..dff1d6281a --- /dev/null +++ b/toolkit/crashreporter/test/unit/test_crash_rust_panic.js @@ -0,0 +1,15 @@ +add_task(async function run_test() { + // Try crashing with a Rust panic + await do_crash( + function () { + Cc["@mozilla.org/xpcom/debug;1"] + .getService(Ci.nsIDebug2) + .rustPanic("OH NO"); + }, + function (mdump, extra) { + Assert.equal(extra.MozCrashReason, "OH NO"); + }, + // process will exit with a zero exit status + true + ); +}); diff --git a/toolkit/crashreporter/test/unit/test_crash_rust_panic_multiline.js b/toolkit/crashreporter/test/unit/test_crash_rust_panic_multiline.js new file mode 100644 index 0000000000..3908c76a2e --- /dev/null +++ b/toolkit/crashreporter/test/unit/test_crash_rust_panic_multiline.js @@ -0,0 +1,15 @@ +add_task(async function run_test() { + // Try crashing with a Rust panic + await do_crash( + function () { + Cc["@mozilla.org/xpcom/debug;1"] + .getService(Ci.nsIDebug2) + .rustPanic("OH NO\nOH NOES!"); + }, + function (mdump, extra) { + Assert.equal(extra.MozCrashReason, "OH NO\nOH NOES!"); + }, + // process will exit with a zero exit status + true + ); +}); diff --git a/toolkit/crashreporter/test/unit/test_crash_stack_overflow.js b/toolkit/crashreporter/test/unit/test_crash_stack_overflow.js new file mode 100644 index 0000000000..a47c217238 --- /dev/null +++ b/toolkit/crashreporter/test/unit/test_crash_stack_overflow.js @@ -0,0 +1,21 @@ +add_task(async function run_test() { + if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) { + dump( + "INFO | test_crash_stack_overflow.js | Can't test crashreporter in a non-libxul build.\n" + ); + return; + } + + // Try crashing by overflowing a thread's stack + await do_crash( + function () { + crashType = CrashTestUtils.CRASH_STACK_OVERFLOW; + crashReporter.annotateCrashReport("TestKey", "TestValue"); + }, + async function (mdump, extra, extraFile) { + Assert.equal(extra.TestKey, "TestValue"); + }, + // process will exit with a zero exit status + true + ); +}); diff --git a/toolkit/crashreporter/test/unit/test_crash_terminator.js b/toolkit/crashreporter/test/unit/test_crash_terminator.js new file mode 100644 index 0000000000..c051a7e277 --- /dev/null +++ b/toolkit/crashreporter/test/unit/test_crash_terminator.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the Shutdown Terminator report errors correctly + +function setup_crash() { + Services.prefs.setBoolPref("toolkit.terminator.testing", true); + Services.prefs.setIntPref("toolkit.asyncshutdown.crash_timeout", 150); + + // Initialize the terminator + // (normally, this is done through the manifest file, but xpcshell + // doesn't take them into account). + let terminator = Cc[ + "@mozilla.org/toolkit/shutdown-terminator;1" + ].createInstance(Ci.nsIObserver); + terminator.observe(null, "terminator-test-profile-after-change", null); + + // Inform the terminator that shutdown has started + // Pick an arbitrary notification + terminator.observe(null, "terminator-test-profile-before-change", null); + terminator.observe(null, "terminator-test-xpcom-will-shutdown", null); + + dump("Waiting (actively) for the crash\n"); + Services.tm.spinEventLoopUntil( + "Test(test_crash_terminator.js:setup_crash())", + () => false + ); +} + +function after_crash(mdump, extra) { + info("Crash signature: " + JSON.stringify(extra, null, "\t")); + Assert.equal(extra.ShutdownProgress, "xpcom-will-shutdown"); +} + +add_task(async function run_test() { + await do_crash(setup_crash, after_crash); +}); diff --git a/toolkit/crashreporter/test/unit/test_crash_uncaught_exception.js b/toolkit/crashreporter/test/unit/test_crash_uncaught_exception.js new file mode 100644 index 0000000000..bfcd2b0ef2 --- /dev/null +++ b/toolkit/crashreporter/test/unit/test_crash_uncaught_exception.js @@ -0,0 +1,17 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function run_test() { + // Try crashing with an uncaught exception. + await do_crash( + function () { + crashType = CrashTestUtils.CRASH_UNCAUGHT_EXCEPTION; + crashReporter.annotateCrashReport("TestKey", "TestValue"); + }, + function (mdump, extra) { + Assert.equal(extra.TestKey, "TestValue"); + }, + // process will exit with a zero exit status + true + ); +}); diff --git a/toolkit/crashreporter/test/unit/test_crash_win64cfi_alloc_large.js b/toolkit/crashreporter/test/unit/test_crash_win64cfi_alloc_large.js new file mode 100644 index 0000000000..a16fe20b4c --- /dev/null +++ b/toolkit/crashreporter/test/unit/test_crash_win64cfi_alloc_large.js @@ -0,0 +1,6 @@ +add_task(async function run_test() { + await do_x64CFITest("CRASH_X64CFI_ALLOC_LARGE", [ + { symbol: "CRASH_X64CFI_ALLOC_LARGE", trust: "context" }, + { symbol: "CRASH_X64CFI_LAUNCHER", trust: "cfi" }, + ]); +}); diff --git a/toolkit/crashreporter/test/unit/test_crash_win64cfi_alloc_small.js b/toolkit/crashreporter/test/unit/test_crash_win64cfi_alloc_small.js new file mode 100644 index 0000000000..0b321bac6f --- /dev/null +++ b/toolkit/crashreporter/test/unit/test_crash_win64cfi_alloc_small.js @@ -0,0 +1,6 @@ +add_task(async function run_test() { + await do_x64CFITest("CRASH_X64CFI_ALLOC_SMALL", [ + { symbol: "CRASH_X64CFI_ALLOC_SMALL", trust: "context" }, + { symbol: "CRASH_X64CFI_LAUNCHER", trust: "cfi" }, + ]); +}); diff --git a/toolkit/crashreporter/test/unit/test_crash_win64cfi_epilog.js b/toolkit/crashreporter/test/unit/test_crash_win64cfi_epilog.js new file mode 100644 index 0000000000..423b67f6f9 --- /dev/null +++ b/toolkit/crashreporter/test/unit/test_crash_win64cfi_epilog.js @@ -0,0 +1,6 @@ +add_task(async function run_test() { + await do_x64CFITest("CRASH_X64CFI_EPILOG", [ + { symbol: "CRASH_X64CFI_EPILOG", trust: "context" }, + { symbol: "CRASH_X64CFI_LAUNCHER", trust: "cfi" }, + ]); +}); diff --git a/toolkit/crashreporter/test/unit/test_crash_win64cfi_infinite_code_chain.exe b/toolkit/crashreporter/test/unit/test_crash_win64cfi_infinite_code_chain.exe 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.ini b/toolkit/crashreporter/test/unit/xpcshell-phc.ini new file mode 100644 index 0000000000..d1d0ada544 --- /dev/null +++ b/toolkit/crashreporter/test/unit/xpcshell-phc.ini @@ -0,0 +1,11 @@ +[DEFAULT] +head = head_crashreporter.js +skip-if = + toolkit == "android" # 1536217 + os == "win" && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1807922 +support-files = + crasher_subprocess_head.js + crasher_subprocess_tail.js + +[test_crash_phc.js] + diff --git a/toolkit/crashreporter/test/unit/xpcshell.ini b/toolkit/crashreporter/test/unit/xpcshell.ini new file mode 100644 index 0000000000..8ba9e894ba --- /dev/null +++ b/toolkit/crashreporter/test/unit/xpcshell.ini @@ -0,0 +1,131 @@ +[DEFAULT] +head = head_crashreporter.js +skip-if = + toolkit == 'android' + os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1807922 +support-files = + crasher_subprocess_head.js + crasher_subprocess_tail.js + +[test_crash_moz_crash.js] +[test_crash_purevirtual.js] +[test_crash_rust_panic.js] +[test_crash_rust_panic_multiline.js] +[test_crash_after_js_oom_reported.js] +[test_crash_after_js_oom_recovered.js] +[test_crash_after_js_oom_reported_2.js] +[test_crash_after_js_large_allocation_failure.js] +[test_crash_after_js_large_allocation_failure_reporting.js] +[test_crash_oom.js] +[test_oom_annotation.js] +run-sequentially = very high failure rate in parallel +[test_kill.js] + +[test_crash_abort.js] +skip-if = os == 'win' + +[test_crash_uncaught_exception.js] + +[test_crash_with_memory_report.js] +[test_crashreporter.js] +[test_crashreporter_crash.js] +run-sequentially = very high failure rate in parallel +[test_override_exception_handler.js] +skip-if = os != 'win' + +[test_crashreporter_appmem.js] +# we need to skip this due to bug 838613 +skip-if = (os != 'win' && os != 'linux') || (os=='linux' && bits==32) + +[test_crash_AsyncShutdown.js] +[test_event_files.js] +[test_crash_terminator.js] + +[test_crash_backgroundtask_moz_crash.js] + +[test_crash_heap_corruption.js] +skip-if = os != 'win' +reason = Test covering Windows-specific crash type +run-sequentially = very high failure rate in parallel + +[test_crash_exc_guard.js] +skip-if = os != 'mac' +reason = Test covering macOS-specific crash type + +[test_crash_modules.js] +skip-if = os != 'win' +reason = Test covering Windows-specific module handling +run-sequentially = very high failure rate in parallel + +[test_crash_win64cfi_unknown_op.js] +head = head_crashreporter.js head_win64cfi.js +skip-if = !(os == 'win' && bits == 64 && processor == 'x86_64') +reason = Windows test specific to the x86-64 architecture + +[test_crash_win64cfi_push_nonvol.js] +head = head_crashreporter.js head_win64cfi.js +skip-if = !(os == 'win' && bits == 64 && processor == 'x86_64') +reason = Windows test specific to the x86-64 architecture + +[test_crash_win64cfi_alloc_small.js] +head = head_crashreporter.js head_win64cfi.js +skip-if = !(os == 'win' && bits == 64 && processor == 'x86_64') +reason = Windows test specific to the x86-64 architecture + +[test_crash_win64cfi_alloc_large.js] +head = head_crashreporter.js head_win64cfi.js +skip-if = !(os == 'win' && bits == 64 && processor == 'x86_64') +reason = Windows test specific to the x86-64 architecture + +[test_crash_win64cfi_save_nonvol.js] +head = head_crashreporter.js head_win64cfi.js +skip-if = !(os == 'win' && bits == 64 && processor == 'x86_64') +reason = Windows test specific to the x86-64 architecture + +[test_crash_win64cfi_save_nonvol_far.js] +head = head_crashreporter.js head_win64cfi.js +skip-if = !(os == 'win' && bits == 64 && processor == 'x86_64') +reason = Windows test specific to the x86-64 architecture + +[test_crash_win64cfi_save_xmm128.js] +head = head_crashreporter.js head_win64cfi.js +skip-if = !(os == 'win' && bits == 64 && processor == 'x86_64') +reason = Windows test specific to the x86-64 architecture + +[test_crash_win64cfi_save_xmm128_far.js] +head = head_crashreporter.js head_win64cfi.js +skip-if = !(os == 'win' && bits == 64 && processor == 'x86_64') +reason = Windows test specific to the x86-64 architecture + +[test_crash_win64cfi_epilog.js] +head = head_crashreporter.js head_win64cfi.js +skip-if = !(os == 'win' && bits == 64 && processor == 'x86_64') +reason = Windows test specific to the x86-64 architecture + +[test_crash_win64cfi_infinite_entry_chain.js] +head = head_crashreporter.js head_win64cfi.js +skip-if = !(os == 'win' && bits == 64 && processor == 'x86_64') +reason = Windows test specific to the x86-64 architecture +support-files = test_crash_win64cfi_infinite_entry_chain.exe + +[test_crash_win64cfi_infinite_code_chain.js] +head = head_crashreporter.js head_win64cfi.js +skip-if = !(os == 'win' && bits == 64 && processor == 'x86_64') +reason = Windows test specific to the x86-64 architecture +support-files = test_crash_win64cfi_infinite_code_chain.exe + +[test_crash_win64cfi_invalid_exception_rva.js] +head = head_crashreporter.js head_win64cfi.js +skip-if = !(os == 'win' && bits == 64 && processor == 'x86_64') +reason = Windows test specific to the x86-64 architecture +support-files = test_crash_win64cfi_invalid_exception_rva.exe + +[test_crash_win64cfi_not_a_pe.js] +head = head_crashreporter.js head_win64cfi.js +skip-if = !(os == 'win' && bits == 64 && processor == 'x86_64') +reason = Windows test specific to the x86-64 architecture +support-files = test_crash_win64cfi_not_a_pe.exe + +[test_crash_stack_overflow.js] +skip-if = os != 'linux' +reason = Still broken on macOS and not yet supported on Windows |