/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim:set ts=2 sw=2 sts=2 et cindent: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "mozilla/dom/JSActor.h" #include "mozilla/dom/JSActorBinding.h" #include "chrome/common/ipc_channel.h" #include "mozilla/Attributes.h" #include "mozilla/FunctionRef.h" #include "mozilla/dom/AutoEntryScript.h" #include "mozilla/dom/ClonedErrorHolder.h" #include "mozilla/dom/ClonedErrorHolderBinding.h" #include "mozilla/dom/DOMException.h" #include "mozilla/dom/DOMExceptionBinding.h" #include "mozilla/dom/JSActorManager.h" #include "mozilla/dom/MessageManagerBinding.h" #include "mozilla/dom/PWindowGlobal.h" #include "mozilla/dom/Promise.h" #include "mozilla/dom/RootedDictionary.h" #include "mozilla/dom/ipc/StructuredCloneData.h" #include "mozilla/ProfilerMarkers.h" #include "js/Promise.h" #include "xpcprivate.h" #include "nsFrameMessageManager.h" #include "nsICrashReporter.h" namespace mozilla::dom { struct JSActorMessageMarker { static constexpr Span MarkerTypeName() { return MakeStringSpan("JSActorMessage"); } static void StreamJSONMarkerData(baseprofiler::SpliceableJSONWriter& aWriter, const ProfilerString8View& aActorName, const ProfilerString16View& aMessageName) { aWriter.StringProperty("actor", aActorName); aWriter.StringProperty("name", NS_ConvertUTF16toUTF8(aMessageName)); } static MarkerSchema MarkerTypeDisplay() { using MS = MarkerSchema; MS schema{MS::Location::MarkerChart, MS::Location::MarkerTable}; schema.AddKeyLabelFormatSearchable( "actor", "Actor Name", MS::Format::String, MS::Searchable::Searchable); schema.AddKeyLabelFormatSearchable( "name", "Message Name", MS::Format::String, MS::Searchable::Searchable); schema.SetTooltipLabel("JSActor - {marker.name}"); schema.SetTableLabel( "{marker.name} - [{marker.data.actor}] {marker.data.name}"); return schema; } }; NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(JSActor) NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTING_ADDREF(JSActor) NS_IMPL_CYCLE_COLLECTING_RELEASE(JSActor) NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(JSActor) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(JSActor) NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal) NS_IMPL_CYCLE_COLLECTION_UNLINK(mWrappedJS) tmp->mPendingQueries.Clear(); NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(JSActor) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWrappedJS) for (const auto& query : tmp->mPendingQueries.Values()) { CycleCollectionNoteChild(cb, query.mPromise.get(), "Pending Query Promise"); } NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END JSActor::JSActor(nsISupports* aGlobal) { mGlobal = do_QueryInterface(aGlobal); if (!mGlobal) { mGlobal = xpc::NativeGlobal(xpc::PrivilegedJunkScope()); } } void JSActor::StartDestroy() { mCanSend = false; } void JSActor::AfterDestroy() { mCanSend = false; // Take our queries out, in case somehow rejecting promises can trigger // additions or removals. const nsTHashMap pendingQueries = std::move(mPendingQueries); for (const auto& entry : pendingQueries.Values()) { nsPrintfCString message( "Actor '%s' destroyed before query '%s' was resolved", mName.get(), NS_LossyConvertUTF16toASCII(entry.mMessageName).get()); entry.mPromise->MaybeRejectWithAbortError(message); } InvokeCallback(CallbackFunction::DidDestroy); ClearManager(); } void JSActor::InvokeCallback(CallbackFunction callback) { MOZ_ASSERT(nsContentUtils::IsSafeToRunScript()); AutoEntryScript aes(GetParentObject(), "JSActor destroy callback"); JSContext* cx = aes.cx(); MozJSActorCallbacks callbacksHolder; JS::Rooted val(cx, JS::ObjectOrNullValue(GetWrapper())); if (NS_WARN_IF(!callbacksHolder.Init(cx, val))) { return; } // Destroy callback is optional. if (callback == CallbackFunction::DidDestroy) { if (callbacksHolder.mDidDestroy.WasPassed()) { callbacksHolder.mDidDestroy.Value()->Call(this); } } else { if (callbacksHolder.mActorCreated.WasPassed()) { callbacksHolder.mActorCreated.Value()->Call(this); } } } nsresult JSActor::QueryInterfaceActor(const nsIID& aIID, void** aPtr) { MOZ_ASSERT(nsContentUtils::IsSafeToRunScript()); if (!GetWrapperPreserveColor()) { // If we have no preserved wrapper, we won't implement any interfaces. return NS_NOINTERFACE; } if (!mWrappedJS) { AutoEntryScript aes(GetParentObject(), "JSActor query interface"); JSContext* cx = aes.cx(); JS::Rooted self(cx, GetWrapper()); JSAutoRealm ar(cx, self); RefPtr wrappedJS; nsresult rv = nsXPCWrappedJS::GetNewOrUsed( cx, self, NS_GET_IID(nsISupports), getter_AddRefs(wrappedJS)); NS_ENSURE_SUCCESS(rv, rv); mWrappedJS = do_QueryInterface(wrappedJS); MOZ_ASSERT(mWrappedJS); } return mWrappedJS->QueryInterface(aIID, aPtr); } void JSActor::SetName(const nsACString& aName) { MOZ_ASSERT(mName.IsEmpty(), "Cannot set name twice!"); mName = aName; } void JSActor::ThrowStateErrorForGetter(const char* aName, ErrorResult& aRv) const { if (mName.IsEmpty()) { aRv.ThrowInvalidStateError(nsPrintfCString( "Cannot access property '%s' before actor is initialized", aName)); } else { aRv.ThrowInvalidStateError(nsPrintfCString( "Cannot access property '%s' after actor '%s' has been destroyed", aName, mName.get())); } } static Maybe TryClone(JSContext* aCx, JS::Handle aValue) { Maybe data{std::in_place}; // Try to directly serialize the passed-in data, and return it to our caller. IgnoredErrorResult rv; data->Write(aCx, aValue, rv); if (rv.Failed()) { // Serialization failed, return `Nothing()` instead. JS_ClearPendingException(aCx); data.reset(); } return data; } static Maybe CloneJSStack( JSContext* aCx, JS::Handle aStack) { JS::Rooted stackVal(aCx, JS::ObjectOrNullValue(aStack)); return TryClone(aCx, stackVal); } static Maybe CaptureJSStack(JSContext* aCx) { JS::Rooted stack(aCx, nullptr); if (JS::IsAsyncStackCaptureEnabledForRealm(aCx) && !JS::CaptureCurrentStack(aCx, &stack)) { JS_ClearPendingException(aCx); } return CloneJSStack(aCx, stack); } void JSActor::SendAsyncMessage(JSContext* aCx, const nsAString& aMessageName, JS::Handle aObj, ErrorResult& aRv) { profiler_add_marker("SendAsyncMessage", geckoprofiler::category::IPC, {}, JSActorMessageMarker{}, mName, aMessageName); Maybe data{std::in_place}; if (!nsFrameMessageManager::GetParamsForMessage( aCx, aObj, JS::UndefinedHandleValue, *data)) { aRv.ThrowDataCloneError(nsPrintfCString( "Failed to serialize message '%s::%s'", NS_LossyConvertUTF16toASCII(aMessageName).get(), mName.get())); return; } JSActorMessageMeta meta; meta.actorName() = mName; meta.messageName() = aMessageName; meta.kind() = JSActorMessageKind::Message; SendRawMessage(meta, std::move(data), CaptureJSStack(aCx), aRv); } already_AddRefed JSActor::SendQuery(JSContext* aCx, const nsAString& aMessageName, JS::Handle aObj, ErrorResult& aRv) { profiler_add_marker("SendQuery", geckoprofiler::category::IPC, {}, JSActorMessageMarker{}, mName, aMessageName); Maybe data{std::in_place}; if (!nsFrameMessageManager::GetParamsForMessage( aCx, aObj, JS::UndefinedHandleValue, *data)) { aRv.ThrowDataCloneError(nsPrintfCString( "Failed to serialize message '%s::%s'", NS_LossyConvertUTF16toASCII(aMessageName).get(), mName.get())); return nullptr; } nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx); if (NS_WARN_IF(!global)) { aRv.ThrowUnknownError("Unable to get current native global"); return nullptr; } RefPtr promise = Promise::Create(global, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } JSActorMessageMeta meta; meta.actorName() = mName; meta.messageName() = aMessageName; meta.queryId() = mNextQueryId++; meta.kind() = JSActorMessageKind::Query; mPendingQueries.InsertOrUpdate(meta.queryId(), PendingQuery{promise, meta.messageName()}); SendRawMessage(meta, std::move(data), CaptureJSStack(aCx), aRv); return promise.forget(); } void JSActor::CallReceiveMessage(JSContext* aCx, const JSActorMessageMeta& aMetadata, JS::Handle aData, JS::MutableHandle aRetVal, ErrorResult& aRv) { // The argument which we want to pass to IPC. RootedDictionary argument(aCx); argument.mTarget = this; argument.mName = aMetadata.messageName(); argument.mData = aData; argument.mJson = aData; argument.mSync = false; if (GetWrapperPreserveColor()) { // Invoke the actual callback. JS::Rooted global(aCx, JS::GetNonCCWObjectGlobal(GetWrapper())); RefPtr messageListener = new MessageListener(GetWrapper(), global, nullptr, nullptr); messageListener->ReceiveMessage(argument, aRetVal, aRv, "JSActor receive message", MessageListener::eRethrowExceptions); } else { aRv.ThrowTypeError("Property 'receiveMessage'"); } } void JSActor::ReceiveMessage(JSContext* aCx, const JSActorMessageMeta& aMetadata, JS::Handle aData, ErrorResult& aRv) { MOZ_ASSERT(aMetadata.kind() == JSActorMessageKind::Message); profiler_add_marker("ReceiveMessage", geckoprofiler::category::IPC, {}, JSActorMessageMarker{}, mName, aMetadata.messageName()); JS::Rooted retval(aCx); CallReceiveMessage(aCx, aMetadata, aData, &retval, aRv); } void JSActor::ReceiveQuery(JSContext* aCx, const JSActorMessageMeta& aMetadata, JS::Handle aData, ErrorResult& aRv) { MOZ_ASSERT(aMetadata.kind() == JSActorMessageKind::Query); profiler_add_marker("ReceiveQuery", geckoprofiler::category::IPC, {}, JSActorMessageMarker{}, mName, aMetadata.messageName()); // This promise will be resolved or rejected once the listener has been // called. Our listener on this promise will then send the reply. RefPtr promise = Promise::Create(GetParentObject(), aRv); if (NS_WARN_IF(aRv.Failed())) { return; } RefPtr handler = new QueryHandler(this, aMetadata, promise); promise->AppendNativeHandler(handler); ErrorResult error; JS::Rooted retval(aCx); CallReceiveMessage(aCx, aMetadata, aData, &retval, error); // If we have a promise, resolve or reject it respectively. if (error.Failed()) { if (error.IsUncatchableException()) { promise->MaybeRejectWithTimeoutError( "Message handler threw uncatchable exception"); } else { promise->MaybeReject(std::move(error)); } } else { promise->MaybeResolve(retval); } error.SuppressException(); } void JSActor::ReceiveQueryReply(JSContext* aCx, const JSActorMessageMeta& aMetadata, JS::Handle aData, ErrorResult& aRv) { if (NS_WARN_IF(aMetadata.actorName() != mName)) { aRv.ThrowUnknownError("Mismatched actor name for query reply"); return; } Maybe query = mPendingQueries.Extract(aMetadata.queryId()); if (NS_WARN_IF(!query)) { aRv.ThrowUnknownError("Received reply for non-pending query"); return; } profiler_add_marker("ReceiveQueryReply", geckoprofiler::category::IPC, {}, JSActorMessageMarker{}, mName, aMetadata.messageName()); Promise* promise = query->mPromise; JSAutoRealm ar(aCx, promise->PromiseObj()); JS::RootedValue data(aCx, aData); if (NS_WARN_IF(!JS_WrapValue(aCx, &data))) { aRv.NoteJSContextException(aCx); return; } if (aMetadata.kind() == JSActorMessageKind::QueryResolve) { promise->MaybeResolve(data); } else { promise->MaybeReject(data); } } void JSActor::SendRawMessageInProcess(const JSActorMessageMeta& aMeta, Maybe&& aData, Maybe&& aStack, OtherSideCallback&& aGetOtherSide) { MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); NS_DispatchToMainThread(NS_NewRunnableFunction( "JSActor Async Message", [aMeta, data{std::move(aData)}, stack{std::move(aStack)}, getOtherSide{std::move(aGetOtherSide)}]() mutable { if (RefPtr otherSide = getOtherSide()) { otherSide->ReceiveRawMessage(aMeta, std::move(data), std::move(stack)); } })); } // Native handler for our generated promise which is used to handle Queries and // send the reply when their promises have been resolved. JSActor::QueryHandler::QueryHandler(JSActor* aActor, const JSActorMessageMeta& aMetadata, Promise* aPromise) : mActor(aActor), mPromise(aPromise), mMessageName(aMetadata.messageName()), mQueryId(aMetadata.queryId()) {} void JSActor::QueryHandler::RejectedCallback(JSContext* aCx, JS::Handle aValue, ErrorResult& aRv) { if (!mActor) { // Make sure that this rejection is reported. See comment below. if (!JS::CallOriginalPromiseReject(aCx, aValue)) { JS_ClearPendingException(aCx); } return; } JS::Rooted value(aCx, aValue); if (value.isObject()) { JS::Rooted error(aCx, &value.toObject()); if (RefPtr ceh = ClonedErrorHolder::Create(aCx, error, IgnoreErrors())) { JS::RootedObject obj(aCx); // Note: We can't use `ToJSValue` here because ClonedErrorHolder isn't // wrapper cached. if (ceh->WrapObject(aCx, nullptr, &obj)) { value.setObject(*obj); } else { JS_ClearPendingException(aCx); } } else { JS_ClearPendingException(aCx); } } Maybe data = TryClone(aCx, value); if (!data) { // Failed to clone the rejection value. Make sure that this // rejection is reported, despite being "handled". This is done by // creating a new promise in the rejected state, and throwing it // away. This will be reported as an unhandled rejected promise. if (!JS::CallOriginalPromiseReject(aCx, aValue)) { JS_ClearPendingException(aCx); } } SendReply(aCx, JSActorMessageKind::QueryReject, std::move(data)); } void JSActor::QueryHandler::ResolvedCallback(JSContext* aCx, JS::Handle aValue, ErrorResult& aRv) { if (!mActor) { return; } Maybe data{std::in_place}; data->InitScope(JS::StructuredCloneScope::DifferentProcess); IgnoredErrorResult error; data->Write(aCx, aValue, error); if (NS_WARN_IF(error.Failed())) { JS_ClearPendingException(aCx); nsAutoCString msg; msg.Append(mActor->Name()); msg.Append(':'); msg.Append(NS_LossyConvertUTF16toASCII(mMessageName)); msg.AppendLiteral(": message reply cannot be cloned."); auto exc = MakeRefPtr(msg, NS_ERROR_FAILURE, "DataCloneError"_ns, nullptr, nullptr); JS::Rooted val(aCx); if (ToJSValue(aCx, exc, &val)) { RejectedCallback(aCx, val, aRv); } else { JS_ClearPendingException(aCx); } return; } SendReply(aCx, JSActorMessageKind::QueryResolve, std::move(data)); } void JSActor::QueryHandler::SendReply(JSContext* aCx, JSActorMessageKind aKind, Maybe&& aData) { MOZ_ASSERT(mActor); profiler_add_marker("SendQueryReply", geckoprofiler::category::IPC, {}, JSActorMessageMarker{}, mActor->Name(), mMessageName); JSActorMessageMeta meta; meta.actorName() = mActor->Name(); meta.messageName() = mMessageName; meta.queryId() = mQueryId; meta.kind() = aKind; JS::Rooted promise(aCx, mPromise->PromiseObj()); JS::Rooted stack(aCx, JS::GetPromiseResolutionSite(promise)); mActor->SendRawMessage(meta, std::move(aData), CloneJSStack(aCx, stack), IgnoreErrors()); mActor = nullptr; mPromise = nullptr; } NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(JSActor::QueryHandler) NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTING_ADDREF(JSActor::QueryHandler) NS_IMPL_CYCLE_COLLECTING_RELEASE(JSActor::QueryHandler) NS_IMPL_CYCLE_COLLECTION(JSActor::QueryHandler, mActor, mPromise) } // namespace mozilla::dom