commit fa35c395d66b02787a6eef65e478a37f34e7fc9d
parent 8fd0424527173fd491a269921ecf8e5357378ba1
Author: Matthew Gaudet <mgaudet@mozilla.com>
Date: Wed, 1 Oct 2025 21:38:49 +0000
Bug 1983154 - Refactor CallSetup r=smaug
This refactoring splits the CallSetup constructor into some recyclable pieces.
Differential Revision: https://phabricator.services.mozilla.com/D265352
Diffstat:
2 files changed, 155 insertions(+), 85 deletions(-)
diff --git a/dom/bindings/CallbackObject.cpp b/dom/bindings/CallbackObject.cpp
@@ -189,102 +189,83 @@ void CallbackObjectBase::GetDescription(nsACString& aOutString) {
aOutString.Append(")");
}
-CallSetup::CallSetup(CallbackObjectBase* aCallback, ErrorResult& aRv,
- const char* aExecutionReason,
- CallbackObjectBase::ExceptionHandling aExceptionHandling,
- JS::Realm* aRealm, bool aIsJSImplementedWebIDL)
- : mCx(nullptr),
- mRealm(aRealm),
- mErrorResult(aRv),
- mExceptionHandling(aExceptionHandling),
- mIsMainThread(NS_IsMainThread()) {
- MOZ_ASSERT_IF(
- aExceptionHandling == CallbackObjectBase::eReportExceptions ||
- aExceptionHandling == CallbackObjectBase::eRethrowExceptions,
- !aRealm);
-
- CycleCollectedJSContext* ccjs = CycleCollectedJSContext::Get();
- if (ccjs) {
- ccjs->EnterMicroTask();
+// Get the global for this callback: Note that this can return nullptr
+// if it doesn't make sense to invoke the callback or global here .
+//
+// Note that for the case of JS-implemented WebIDL we never have a window here.
+nsIGlobalObject* CallSetup::GetActiveGlobalObjectForCall(
+ JS::Handle<JSObject*> callbackOrGlobal, bool aIsMainThread,
+ bool aIsJSImplementedWebIDL, ErrorResult& aRv) {
+ nsGlobalWindowInner* win = aIsMainThread && !aIsJSImplementedWebIDL
+ ? xpc::WindowGlobalOrNull(callbackOrGlobal)
+ : nullptr;
+ if (win) {
+ // We don't want to run script in windows that have been navigated away
+ // from.
+ if (!win->HasActiveDocument()) {
+ aRv.ThrowNotSupportedError(
+ "Refusing to execute function from window whose document is no "
+ "longer active.");
+ return nullptr;
+ }
+ return win;
}
- // Compute the caller's subject principal (if necessary) early, before we
- // do anything that might perturb the relevant state.
- nsIPrincipal* webIDLCallerPrincipal = nullptr;
- if (aIsJSImplementedWebIDL) {
- webIDLCallerPrincipal =
- nsContentUtils::SubjectPrincipalOrSystemIfNativeCaller();
- }
+ // No DOM Window. Store the global.
+ auto* globalObject = xpc::NativeGlobal(callbackOrGlobal);
+ MOZ_ASSERT(globalObject);
+ return globalObject;
+}
- JSObject* wrappedCallback = aCallback->CallbackPreserveColor();
- if (!wrappedCallback) {
+// Check that it's OK & possible to execute script in the given global, and if
+// not return false and fill in aRv.
+bool CallSetup::CheckBeforeExecution(nsIGlobalObject* aGlobalObject,
+ JSObject* aCallbackOrGlobal,
+ bool aIsJSImplementedWebIDL,
+ ErrorResult& aRv) {
+ if (aGlobalObject->IsScriptForbidden(aCallbackOrGlobal,
+ aIsJSImplementedWebIDL)) {
aRv.ThrowNotSupportedError(
- "Cannot execute callback from a nuked compartment.");
- return;
- }
-
- nsIGlobalObject* globalObject = nullptr;
-
- {
- // First, find the real underlying callback.
- JS::Rooted<JSObject*> realCallback(ccjs->RootingCx(),
- js::UncheckedUnwrap(wrappedCallback));
-
- // Get the global for this callback. Note that for the case of
- // JS-implemented WebIDL we never have a window here.
- nsGlobalWindowInner* win = mIsMainThread && !aIsJSImplementedWebIDL
- ? xpc::WindowGlobalOrNull(realCallback)
- : nullptr;
- if (win) {
- // We don't want to run script in windows that have been navigated away
- // from.
- if (!win->HasActiveDocument()) {
- aRv.ThrowNotSupportedError(
- "Refusing to execute function from window whose document is no "
- "longer active.");
- return;
- }
- globalObject = win;
- } else {
- // No DOM Window. Store the global.
- globalObject = xpc::NativeGlobal(realCallback);
- MOZ_ASSERT(globalObject);
- }
-
- // Make sure to use realCallback to get the global of the callback
- // object, not the wrapper.
- if (globalObject->IsScriptForbidden(realCallback, aIsJSImplementedWebIDL)) {
- aRv.ThrowNotSupportedError(
- "Refusing to execute function from global in which script is "
- "disabled.");
- return;
- }
+ "Refusing to execute function from global in which script is "
+ "disabled.");
+ return false;
}
// Bail out if there's no useful global.
- if (!globalObject->HasJSGlobal()) {
+ if (!aGlobalObject->HasJSGlobal()) {
aRv.ThrowNotSupportedError(
"Refusing to execute function from global which is being torn down.");
- return;
+ return false;
}
+ return true;
+}
+
+void CallSetup::SetupForExecution(nsIGlobalObject* aGlobalObject,
+ nsIGlobalObject* aIncumbentGlobal,
+ JS::Handle<JSObject*> aCallbackOrGlobal,
+ JS::Handle<JSObject*> aCallbackGlobal,
+ JS::Handle<JSObject*> aCreationStack,
+ nsIPrincipal* aWebIDLCallerPrincipal,
+ const char* aExecutionReason,
+ ErrorResult& aRv) {
AutoAllowLegacyScriptExecution exemption;
- mAutoEntryScript.emplace(globalObject, aExecutionReason, mIsMainThread);
- mAutoEntryScript->SetWebIDLCallerPrincipal(webIDLCallerPrincipal);
- nsIGlobalObject* incumbent = aCallback->IncumbentGlobalOrNull();
- if (incumbent) {
+ mAutoEntryScript.emplace(aGlobalObject, aExecutionReason, mIsMainThread);
+ mAutoEntryScript->SetWebIDLCallerPrincipal(aWebIDLCallerPrincipal);
+
+ if (aIncumbentGlobal) {
// The callback object traces its incumbent JS global, so in general it
// should be alive here. However, it's possible that we could run afoul
// of the same IPC global weirdness described above, wherein the
// nsIGlobalObject has severed its reference to the JS global. Let's just
// be safe here, so that nobody has to waste a day debugging gaia-ui tests.
- if (!incumbent->HasJSGlobal()) {
+ if (!aIncumbentGlobal->HasJSGlobal()) {
aRv.ThrowNotSupportedError(
"Refusing to execute function because our incumbent global is being "
"torn down.");
return;
}
- mAutoIncumbentScript.emplace(incumbent);
+ mAutoIncumbentScript.emplace(aIncumbentGlobal);
}
JSContext* cx = mAutoEntryScript->cx();
@@ -296,10 +277,10 @@ CallSetup::CallSetup(CallbackObjectBase* aCallback, ErrorResult& aRv,
// until here it do the unmark. This allows us to construct mRootedCallable
// with the cx from mAutoEntryScript, avoiding the cost of finding another
// JSContext. (Rooted<> does not care about requests or compartments.)
- mRootedCallable.emplace(cx, aCallback->CallbackOrNull());
- JSObject* asyncStack = aCallback->GetCreationStack();
- if (asyncStack) {
- mAsyncStackSetter.emplace(cx, asyncStack, aExecutionReason);
+ mRootedCallable.emplace(cx, aCallbackOrGlobal);
+
+ if (aCreationStack) {
+ mAsyncStackSetter.emplace(cx, aCreationStack, aExecutionReason);
}
// Enter the realm of our callback, so we can actually work with it.
@@ -307,7 +288,7 @@ CallSetup::CallSetup(CallbackObjectBase* aCallback, ErrorResult& aRv,
// Note that if the callback is a wrapper, this will not be the same
// realm that we ended up in with mAutoEntryScript above, because the
// entry point is based off of the unwrapped callback (realCallback).
- mAr.emplace(cx, aCallback->CallbackGlobalOrNull());
+ mAr.emplace(cx, aCallbackGlobal);
// And now we're ready to go.
mCx = cx;
@@ -317,6 +298,76 @@ CallSetup::CallSetup(CallbackObjectBase* aCallback, ErrorResult& aRv,
mCallContext.emplace(cx, nullptr);
}
+CallSetup::CallSetup(CallbackObjectBase* aCallback, ErrorResult& aRv,
+ const char* aExecutionReason,
+ CallbackObjectBase::ExceptionHandling aExceptionHandling,
+ JS::Realm* aRealm, bool aIsJSImplementedWebIDL)
+ : mCx(nullptr),
+ mRealm(aRealm),
+ mErrorResult(aRv),
+ mExceptionHandling(aExceptionHandling),
+ mIsMainThread(NS_IsMainThread()) {
+ MOZ_ASSERT_IF(
+ aExceptionHandling == CallbackObjectBase::eReportExceptions ||
+ aExceptionHandling == CallbackObjectBase::eRethrowExceptions,
+ !aRealm);
+
+ CycleCollectedJSContext* ccjs = CycleCollectedJSContext::Get();
+ MOZ_ASSERT(ccjs);
+ ccjs->EnterMicroTask();
+ JS::RootedTuple<JSObject*, JSObject*, JSObject*> roots(ccjs->RootingCx());
+
+ // Compute the caller's subject principal (if necessary) early, before we
+ // do anything that might perturb the relevant state.
+ nsIPrincipal* webIDLCallerPrincipal = nullptr;
+ if (aIsJSImplementedWebIDL) {
+ webIDLCallerPrincipal =
+ nsContentUtils::SubjectPrincipalOrSystemIfNativeCaller();
+ }
+
+ JSObject* wrappedCallback = aCallback->CallbackPreserveColor();
+ if (!wrappedCallback) {
+ aRv.ThrowNotSupportedError(
+ "Cannot execute callback from a nuked compartment.");
+ return;
+ }
+
+ nsIGlobalObject* globalObject = nullptr;
+
+ {
+ // First, find the real underlying callback.
+ JS::RootedField<JSObject*, 0> realCallback(
+ roots, js::UncheckedUnwrap(wrappedCallback));
+
+ globalObject = GetActiveGlobalObjectForCall(realCallback, mIsMainThread,
+ aIsJSImplementedWebIDL, aRv);
+ if (!globalObject) {
+ MOZ_ASSERT(aRv.Failed());
+ return;
+ }
+
+ // Make sure to use realCallback to get the global of the callback
+ // object, not the wrapper.
+ if (!CheckBeforeExecution(globalObject, realCallback,
+ aIsJSImplementedWebIDL, aRv)) {
+ return;
+ }
+ }
+
+ nsIGlobalObject* incumbent = aCallback->IncumbentGlobalOrNull();
+
+ // Start the execution setup -- if it succeeds, this will set mCx
+ JS::RootedField<JSObject*, 0> rootedCallback(roots,
+ aCallback->CallbackOrNull());
+ JS::RootedField<JSObject*, 1> rootedCallbackGlobal(
+ roots, aCallback->CallbackGlobalOrNull());
+ JS::RootedField<JSObject*, 2> rootedCreationStack(
+ roots, aCallback->GetCreationStack());
+ SetupForExecution(globalObject, incumbent, rootedCallback,
+ rootedCallbackGlobal, rootedCreationStack,
+ webIDLCallerPrincipal, aExecutionReason, aRv);
+}
+
bool CallSetup::ShouldRethrowException(JS::Handle<JS::Value> aException) {
if (mExceptionHandling == CallbackObjectBase::eRethrowExceptions) {
MOZ_ASSERT(!mRealm);
diff --git a/dom/bindings/CallbackObject.h b/dom/bindings/CallbackObject.h
@@ -238,13 +238,13 @@ class CallbackObjectBase {
JS::TenuredHeap<JSObject*> mIncumbentJSGlobal;
};
+/**
+ * A class that performs whatever setup we need to safely make a
+ * call while this class is on the stack, After the constructor
+ * returns, the call is safe to make if GetContext() returns
+ * non-null.
+ */
class MOZ_STACK_CLASS CallSetup {
- /**
- * A class that performs whatever setup we need to safely make a
- * call while this class is on the stack, After the constructor
- * returns, the call is safe to make if GetContext() returns
- * non-null.
- */
public:
// If aExceptionHandling == eRethrowContentExceptions then aRealm
// needs to be set to the realm in which exceptions will be rethrown.
@@ -271,6 +271,25 @@ class MOZ_STACK_CLASS CallSetup {
bool ShouldRethrowException(JS::Handle<JS::Value> aException);
+ static nsIGlobalObject* GetActiveGlobalObjectForCall(
+ JS::Handle<JSObject*> callbackOrGlobal, bool aIsMainThread,
+ bool aIsJSImplementedWebIDL, ErrorResult& aRv);
+
+ static bool CheckBeforeExecution(nsIGlobalObject* aGlobalObject,
+ JSObject* aCallbackOrGlobal,
+ bool aIsJSImplementedWebIDL,
+ ErrorResult& aRv);
+
+ // Perform the final setup work. If this succeeds, mCx is set and we are able
+ // to run the callback with the appropriate environment.
+ void SetupForExecution(nsIGlobalObject* aGlobalObject,
+ nsIGlobalObject* aIncumbentGlobal,
+ JS::Handle<JSObject*> aCallbackOrGlobal,
+ JS::Handle<JSObject*> aCallbackGlobal,
+ JS::Handle<JSObject*> aCreationStack,
+ nsIPrincipal* aWebIDLCallerPrincipal,
+ const char* aExecutionReason, ErrorResult& aRv);
+
// Members which can go away whenever
JSContext* mCx;