Bug 1810582 - [devtools] Implements nsIConsoleService.callFunctionAndLogException. r=peterv

This new API helps execute a function (2nd argument) and ensure that any thrown exception
will be associated against a given global (1st argument).

This is especially handy when any privileged code executes some content code.
Without such helper, the exception is thrown into the privileged global and loose
its original context and can no longer be associated with the content window it originates from.

It can also help associate pure-privileged exception to some particular content window.

Differential Revision: https://phabricator.services.mozilla.com/D167529
This commit is contained in:
Alexandre Poirot 2023-05-23 12:39:39 +00:00
Родитель 6aeeb8363e
Коммит aefb1b1f14
4 изменённых файлов: 328 добавлений и 0 удалений

Просмотреть файл

@ -23,9 +23,12 @@
#include "nsProxyRelease.h"
#include "nsIScriptError.h"
#include "nsISupportsPrimitives.h"
#include "nsGlobalWindowInner.h"
#include "js/friend/ErrorMessages.h"
#include "mozilla/dom/WindowGlobalParent.h"
#include "mozilla/dom/ContentParent.h"
#include "mozilla/dom/BrowserParent.h"
#include "mozilla/dom/ScriptSettings.h"
#include "mozilla/SchedulerGroup.h"
#include "mozilla/Services.h"
@ -394,6 +397,53 @@ nsresult nsConsoleService::LogMessageWithMode(
return NS_OK;
}
// See nsIConsoleService.idl for more info about this method
NS_IMETHODIMP
nsConsoleService::CallFunctionAndLogException(
JS::Handle<JS::Value> targetGlobal, JS::HandleValue function, JSContext* cx,
JS::MutableHandleValue retval) {
if (!targetGlobal.isObject() || !function.isObject()) {
return NS_ERROR_INVALID_ARG;
}
JS::Rooted<JS::Realm*> contextRealm(cx, JS::GetCurrentRealmOrNull(cx));
if (!contextRealm) {
return NS_ERROR_INVALID_ARG;
}
JS::Rooted<JSObject*> global(
cx, js::CheckedUnwrapDynamic(&targetGlobal.toObject(), cx));
if (!global) {
return NS_ERROR_INVALID_ARG;
}
// Use AutoJSAPI in order to trigger AutoJSAPI::ReportException
// which will do most of the work required for this function.
//
// We only have to pick the right global for which we want to flag
// the exception against.
dom::AutoJSAPI jsapi;
if (!jsapi.Init(global)) {
return NS_ERROR_UNEXPECTED;
}
JSContext* ccx = jsapi.cx();
// AutoJSAPI picks `targetGlobal` as execution compartment
// whereas we expect to run `function` from the callsites compartment.
JSAutoRealm ar(ccx, JS::GetRealmGlobalOrNull(contextRealm));
JS::RootedValue funVal(ccx, function);
if (!JS_WrapValue(ccx, &funVal)) {
return NS_ERROR_FAILURE;
}
if (!JS_CallFunctionValue(ccx, nullptr, funVal, JS::HandleValueArray::empty(),
retval)) {
return NS_ERROR_XPC_JAVASCRIPT_ERROR;
}
return NS_OK;
}
void nsConsoleService::CollectCurrentListeners(
nsCOMArray<nsIConsoleListener>& aListeners) {
MutexAutoLock lock(mLock);

Просмотреть файл

@ -13,6 +13,18 @@ interface nsIConsoleService : nsISupports
{
void logMessage(in nsIConsoleMessage message);
// This helper function executes `function` and redirects any exception
// that may be thrown while running it to the DevTools Console currently
// debugging `targetGlobal`.
//
// This helps flag the nsIScriptError with a particular innerWindowID
// which is especially useful for WebExtension content scripts
// where script are running in a Sandbox whose prototype is the content window.
// We expect content script exception to be flaged with the content window
// innerWindowID in order to appear in the tab's DevTools.
[implicit_jscontext]
jsval callFunctionAndLogException(in jsval targetGlobal, in jsval function);
// This is a variant of LogMessage which allows the caller to determine
// if the message should be output to an OS-specific log. This is used on
// B2G to control whether the message is logged to the android log or not.

Просмотреть файл

@ -0,0 +1,265 @@
let lastMessage;
const consoleListener = {
observe(message) {
dump(" >> new message: " + message.errorMessage + "\n");
lastMessage = message;
},
};
Services.console.registerListener(consoleListener);
// The Console Service notifies its listener after one event loop cycle.
// So wait for one tick after each action dispatching a message/error to the service.
function waitForATick() {
return new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
}
add_task(async function customScriptError() {
const scriptError = Cc["@mozilla.org/scripterror;1"].createInstance(
Ci.nsIScriptError
);
scriptError.init(
"foo",
"file.js",
null,
1,
2,
Ci.nsIScriptError.warningFlag,
"some javascript"
);
Services.console.logMessage(scriptError);
await waitForATick();
Assert.equal(
lastMessage,
scriptError,
"We receive the exact same nsIScriptError object"
);
Assert.equal(lastMessage.errorMessage, "foo");
Assert.equal(lastMessage.sourceName, "file.js");
Assert.equal(lastMessage.lineNumber, 1);
Assert.equal(lastMessage.columnNumber, 2);
Assert.equal(lastMessage.flags, Ci.nsIScriptError.warningFlag);
Assert.equal(lastMessage.category, "some javascript");
Assert.equal(
lastMessage.stack,
undefined,
"Custom nsIScriptError object created from JS can't convey any stack"
);
});
add_task(async function callFunctionAndLogExceptionWithChromeGlobal() {
try {
Services.console.callFunctionAndLogException(globalThis, function () {
throw new Error("custom exception");
});
Assert.fail("callFunctionAndLogException should throw");
} catch (e) {
Assert.equal(
e.name,
"NS_ERROR_XPC_JAVASCRIPT_ERROR",
"callFunctionAndLogException thrown"
);
}
await waitForATick();
Assert.ok(!!lastMessage, "Got the message");
Assert.ok(
lastMessage instanceof Ci.nsIScriptError,
"This is a nsIScriptError"
);
Assert.equal(lastMessage.errorMessage, "Error: custom exception");
Assert.equal(lastMessage.sourceName, _TEST_FILE);
Assert.equal(lastMessage.lineNumber, 56);
Assert.equal(lastMessage.columnNumber, 13);
Assert.equal(lastMessage.flags, Ci.nsIScriptError.errorFlag);
Assert.equal(lastMessage.category, "chrome javascript");
Assert.ok(lastMessage.stack, "It has a stack");
Assert.equal(lastMessage.stack.source, _TEST_FILE);
Assert.equal(lastMessage.stack.line, 56);
Assert.equal(lastMessage.stack.column, 13);
Assert.ok(!!lastMessage.stack.parent, "stack has a parent frame");
Assert.equal(
lastMessage.innerWindowID,
0,
"The message isn't bound to any WindowGlobal"
);
});
add_task(async function callFunctionAndLogExceptionWithContentGlobal() {
const window = createContentWindow();
try {
Services.console.callFunctionAndLogException(window, function () {
throw new Error("another custom exception");
});
Assert.fail("callFunctionAndLogException should throw");
} catch (e) {
Assert.equal(
e.name,
"NS_ERROR_XPC_JAVASCRIPT_ERROR",
"callFunctionAndLogException thrown"
);
}
await waitForATick();
Assert.ok(!!lastMessage, "Got the message");
Assert.ok(
lastMessage instanceof Ci.nsIScriptError,
"This is a nsIScriptError"
);
Assert.equal(lastMessage.errorMessage, "Error: another custom exception");
Assert.equal(lastMessage.sourceName, _TEST_FILE);
Assert.equal(lastMessage.lineNumber, 97);
Assert.equal(lastMessage.columnNumber, 13);
Assert.equal(lastMessage.flags, Ci.nsIScriptError.errorFlag);
Assert.equal(lastMessage.category, "content javascript");
Assert.ok(lastMessage.stack, "It has a stack");
Assert.equal(lastMessage.stack.source, _TEST_FILE);
Assert.equal(lastMessage.stack.line, 97);
Assert.equal(lastMessage.stack.column, 13);
Assert.ok(!!lastMessage.stack.parent, "stack has a parent frame");
Assert.ok(
!!window.windowGlobalChild.innerWindowId,
"The window has a innerWindowId"
);
Assert.equal(
lastMessage.innerWindowID,
window.windowGlobalChild.innerWindowId,
"The message is bound to the content window"
);
});
add_task(async function callFunctionAndLogExceptionForContentScriptSandboxes() {
const { sandbox, window } = createContentScriptSandbox();
Cu.evalInSandbox(
`function foo() { throw new Error("sandbox exception"); }`,
sandbox,
null,
"sandbox-file.js",
1,
0
);
try {
Services.console.callFunctionAndLogException(window, sandbox.foo);
Assert.fail("callFunctionAndLogException should throw");
} catch (e) {
Assert.equal(
e.name,
"NS_ERROR_XPC_JAVASCRIPT_ERROR",
"callFunctionAndLogException thrown"
);
}
await waitForATick();
Assert.ok(!!lastMessage, "Got the message");
// Note that it is important to "instanceof" in order to expose the nsIScriptError attributes.
Assert.ok(
lastMessage instanceof Ci.nsIScriptError,
"This is a nsIScriptError"
);
Assert.equal(lastMessage.errorMessage, "Error: sandbox exception");
Assert.equal(lastMessage.sourceName, "sandbox-file.js");
Assert.equal(lastMessage.lineNumber, 1);
Assert.equal(lastMessage.columnNumber, 24);
Assert.equal(lastMessage.flags, Ci.nsIScriptError.errorFlag);
Assert.equal(lastMessage.category, "content javascript");
Assert.ok(lastMessage.stack, "It has a stack");
Assert.equal(lastMessage.stack.source, "sandbox-file.js");
Assert.equal(lastMessage.stack.line, 1);
Assert.equal(lastMessage.stack.column, 24);
Assert.ok(!!lastMessage.stack.parent, "stack has a parent frame");
Assert.ok(
!!window.windowGlobalChild.innerWindowId,
"The sandbox's prototype is a window and has a innerWindowId"
);
Assert.equal(
lastMessage.innerWindowID,
window.windowGlobalChild.innerWindowId,
"The message is bound to the sandbox's prototype WindowGlobal"
);
});
add_task(
async function callFunctionAndLogExceptionForContentScriptSandboxesWrappedInChrome() {
const { sandbox, window } = createContentScriptSandbox();
Cu.evalInSandbox(
`function foo() { throw new Error("sandbox exception"); }`,
sandbox,
null,
"sandbox-file.js",
1,
0
);
try {
Services.console.callFunctionAndLogException(window, function () {
sandbox.foo();
});
Assert.fail("callFunctionAndLogException should throw");
} catch (e) {
Assert.equal(
e.name,
"NS_ERROR_XPC_JAVASCRIPT_ERROR",
"callFunctionAndLogException thrown"
);
}
await waitForATick();
Assert.ok(!!lastMessage, "Got the message");
// Note that it is important to "instanceof" in order to expose the nsIScriptError attributes.
Assert.ok(
lastMessage instanceof Ci.nsIScriptError,
"This is a nsIScriptError"
);
Assert.ok(
!!window.windowGlobalChild.innerWindowId,
"The sandbox's prototype is a window and has a innerWindowId"
);
Assert.equal(
lastMessage.innerWindowID,
window.windowGlobalChild.innerWindowId,
"The message is bound to the sandbox's prototype WindowGlobal"
);
}
);
add_task(function teardown() {
Services.console.unregisterListener(consoleListener);
});
// We are in xpcshell, so we can't have a real DOM Window as in Firefox
// but let's try to have a fake one.
function createContentWindow() {
const principal =
Services.scriptSecurityManager.createContentPrincipalFromOrigin(
"http://example.com/"
);
const webnav = Services.appShell.createWindowlessBrowser(false);
webnav.docShell.createAboutBlankContentViewer(principal, principal);
return webnav.document.defaultView;
}
// Create a Sandbox as in WebExtension content scripts
function createContentScriptSandbox() {
const window = createContentWindow();
// The sandboxPrototype is the key here in order to
// make xpc::SandboxWindowOrNull ignore the sandbox
// and instead retrieve its prototype and link the error message
// to the window instead of the sandbox.
return {
sandbox: Cu.Sandbox(window, { sandboxPrototype: window }),
window,
};
}

Просмотреть файл

@ -19,6 +19,7 @@ skip-if = os == "win"
fail-if = os == "android"
[test_bug478086.js]
[test_bug1434856.js]
[test_console_service_callFunctionAndLogException.js]
[test_debugger_malloc_size_of.js]
[test_file_createUnique.js]
[test_file_equality.js]