commit ccc22f306e8ef979d41feefc6ba0a98762d931d6 parent 40a21e31ae511494dd4caca4706cf009d3b14984 Author: Andrew McCreight <continuation@gmail.com> Date: Mon, 17 Nov 2025 21:52:30 +0000 Bug 1999383 - IPDL serialization of JS values. r=nika,sfink This patch implements serialization and deserialization of JS values using IPDL. This is similar to the structured clone algorithm, but the target is chrome JS, so it does not need to be standards compliant. The idea is that by serializing to a data type, it is easier to write a type checking algorithm in later patches in bug 1885221 by traversing the data structure. This also allows us to tightly control what is allowed to be sent. This patch removes MMPrinter::Print logging for JS IPC messages, but a more sophisticated logging will be added in bug 1885221 part 5, once the type infrastructure is in place. Differential Revision: https://phabricator.services.mozilla.com/D227393 Diffstat:
35 files changed, 1707 insertions(+), 143 deletions(-)
diff --git a/dom/chrome-webidl/JSActorTypeUtils.webidl b/dom/chrome-webidl/JSActorTypeUtils.webidl @@ -0,0 +1,15 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + + +[ChromeOnly, Exposed=Window] +namespace JSActorTypeUtils { + /* + * Convert the argument to a JSIPCValue and back again. Throw on failure. + */ + [Throws] + any serializeDeserialize(boolean strict, any val); +}; diff --git a/dom/chrome-webidl/moz.build b/dom/chrome-webidl/moz.build @@ -66,6 +66,7 @@ WEBIDL_FILES = [ "InspectorUtils.webidl", "IteratorResult.webidl", "JSActor.webidl", + "JSActorTypeUtils.webidl", "JSProcessActor.webidl", "JSWindowActor.webidl", "L10nOverlays.webidl", diff --git a/dom/ipc/ContentChild.cpp b/dom/ipc/ContentChild.cpp @@ -4646,19 +4646,14 @@ already_AddRefed<JSActor> ContentChild::InitJSActor( } IPCResult ContentChild::RecvRawMessage( - const JSActorMessageMeta& aMeta, const UniquePtr<ClonedMessageData>& aData, + const JSActorMessageMeta& aMeta, JSIPCValue&& aData, const UniquePtr<ClonedMessageData>& aStack) { - UniquePtr<StructuredCloneData> data; - if (aData) { - data = MakeUnique<StructuredCloneData>(); - data->BorrowFromClonedMessageData(*aData); - } UniquePtr<StructuredCloneData> stack; if (aStack) { stack = MakeUnique<StructuredCloneData>(); stack->BorrowFromClonedMessageData(*aStack); } - ReceiveRawMessage(aMeta, std::move(data), std::move(stack)); + ReceiveRawMessage(aMeta, std::move(aData), std::move(stack)); return IPC_OK(); } diff --git a/dom/ipc/ContentChild.h b/dom/ipc/ContentChild.h @@ -765,8 +765,7 @@ class ContentChild final : public PContentChild, const uint32_t aStopFlags); mozilla::ipc::IPCResult RecvRawMessage( - const JSActorMessageMeta& aMeta, - const UniquePtr<ClonedMessageData>& aData, + const JSActorMessageMeta& aMeta, JSIPCValue&& aData, const UniquePtr<ClonedMessageData>& aStack); already_AddRefed<JSActor> InitJSActor(JS::Handle<JSObject*> aMaybeActor, diff --git a/dom/ipc/ContentParent.cpp b/dom/ipc/ContentParent.cpp @@ -7912,21 +7912,14 @@ void ContentParent::StartRemoteWorkerService() { } IPCResult ContentParent::RecvRawMessage( - const JSActorMessageMeta& aMeta, const UniquePtr<ClonedMessageData>& aData, + const JSActorMessageMeta& aMeta, JSIPCValue&& aData, const UniquePtr<ClonedMessageData>& aStack) { - UniquePtr<StructuredCloneData> data; - if (aData) { - data = MakeUnique<StructuredCloneData>(); - data->BorrowFromClonedMessageData(*aData); - } UniquePtr<StructuredCloneData> stack; if (aStack) { stack = MakeUnique<StructuredCloneData>(); stack->BorrowFromClonedMessageData(*aStack); } - MMPrinter::Print("ContentParent::RecvRawMessage", aMeta.actorName(), - aMeta.messageName(), aData); - ReceiveRawMessage(aMeta, std::move(data), std::move(stack)); + ReceiveRawMessage(aMeta, std::move(aData), std::move(stack)); return IPC_OK(); } diff --git a/dom/ipc/ContentParent.h b/dom/ipc/ContentParent.h @@ -1296,8 +1296,7 @@ class ContentParent final : public PContentParent, ServiceWorkerShutdownState::Progress aProgress); mozilla::ipc::IPCResult RecvRawMessage( - const JSActorMessageMeta& aMeta, - const UniquePtr<ClonedMessageData>& aData, + const JSActorMessageMeta& aMeta, JSIPCValue&& aData, const UniquePtr<ClonedMessageData>& aStack); mozilla::ipc::IPCResult RecvAbortOtherOrientationPendingPromises( diff --git a/dom/ipc/PContent.ipdl b/dom/ipc/PContent.ipdl @@ -53,6 +53,7 @@ include WindowGlobalTypes; include IPCBlob; include IPCStream; include IPCTransferable; +include JSIPCValue; include PPrintingTypes; include PTabContext; include ProtocolTypes; @@ -582,7 +583,7 @@ both: async PWebBrowserPersistDocument(nullable PBrowser aBrowser, MaybeDiscardedBrowsingContext aContext); - async RawMessage(JSActorMessageMeta aMetadata, UniquePtr<ClonedMessageData> aData, + async RawMessage(JSActorMessageMeta aMetadata, JSIPCValue aData, UniquePtr<ClonedMessageData> aStack); child: diff --git a/dom/ipc/PWindowGlobal.ipdl b/dom/ipc/PWindowGlobal.ipdl @@ -19,6 +19,7 @@ include protocol PWebIdentity; include DOMTypes; include ClientIPCTypes; +include JSIPCValue; include NeckoChannelParams; include SessionStoreTypes; @@ -98,7 +99,7 @@ child: async RestoreTabContent(nullable SessionStoreRestoreData aData) returns (bool success); both: - async RawMessage(JSActorMessageMeta aMetadata, UniquePtr<ClonedMessageData> aData, + async RawMessage(JSActorMessageMeta aMetadata, JSIPCValue aData, UniquePtr<ClonedMessageData> aStack); parent: diff --git a/dom/ipc/WindowGlobalChild.cpp b/dom/ipc/WindowGlobalChild.cpp @@ -569,19 +569,14 @@ mozilla::ipc::IPCResult WindowGlobalChild::RecvRestoreTabContent( } IPCResult WindowGlobalChild::RecvRawMessage( - const JSActorMessageMeta& aMeta, const UniquePtr<ClonedMessageData>& aData, + const JSActorMessageMeta& aMeta, JSIPCValue&& aData, const UniquePtr<ClonedMessageData>& aStack) { - UniquePtr<StructuredCloneData> data; - if (aData) { - data = MakeUnique<StructuredCloneData>(); - data->BorrowFromClonedMessageData(*aData); - } UniquePtr<StructuredCloneData> stack; if (aStack) { stack = MakeUnique<StructuredCloneData>(); stack->BorrowFromClonedMessageData(*aStack); } - ReceiveRawMessage(aMeta, std::move(data), std::move(stack)); + ReceiveRawMessage(aMeta, std::move(aData), std::move(stack)); return IPC_OK(); } diff --git a/dom/ipc/WindowGlobalChild.h b/dom/ipc/WindowGlobalChild.h @@ -162,8 +162,7 @@ class WindowGlobalChild final : public WindowGlobalActor, // IPC messages mozilla::ipc::IPCResult RecvRawMessage( - const JSActorMessageMeta& aMeta, - const UniquePtr<ClonedMessageData>& aData, + const JSActorMessageMeta& aMeta, JSIPCValue&& aData, const UniquePtr<ClonedMessageData>& aStack); MOZ_CAN_RUN_SCRIPT_BOUNDARY diff --git a/dom/ipc/WindowGlobalParent.cpp b/dom/ipc/WindowGlobalParent.cpp @@ -552,21 +552,14 @@ IPCResult WindowGlobalParent::RecvDestroy() { } IPCResult WindowGlobalParent::RecvRawMessage( - const JSActorMessageMeta& aMeta, const UniquePtr<ClonedMessageData>& aData, + const JSActorMessageMeta& aMeta, JSIPCValue&& aData, const UniquePtr<ClonedMessageData>& aStack) { - UniquePtr<StructuredCloneData> data; - if (aData) { - data = MakeUnique<StructuredCloneData>(); - data->BorrowFromClonedMessageData(*aData); - } UniquePtr<StructuredCloneData> stack; if (aStack) { stack = MakeUnique<StructuredCloneData>(); stack->BorrowFromClonedMessageData(*aStack); } - MMPrinter::Print("WindowGlobalParent::RecvRawMessage", aMeta.actorName(), - aMeta.messageName(), aData); - ReceiveRawMessage(aMeta, std::move(data), std::move(stack)); + ReceiveRawMessage(aMeta, std::move(aData), std::move(stack)); return IPC_OK(); } diff --git a/dom/ipc/WindowGlobalParent.h b/dom/ipc/WindowGlobalParent.h @@ -283,8 +283,7 @@ class WindowGlobalParent final : public WindowContext, const IPCClientInfo& aIPCClientInfo); mozilla::ipc::IPCResult RecvDestroy(); mozilla::ipc::IPCResult RecvRawMessage( - const JSActorMessageMeta& aMeta, - const UniquePtr<ClonedMessageData>& aData, + const JSActorMessageMeta& aMeta, JSIPCValue&& aData, const UniquePtr<ClonedMessageData>& aStack); mozilla::ipc::IPCResult RecvGetContentBlockingEvents( diff --git a/dom/ipc/jsactor/JSActor.cpp b/dom/ipc/jsactor/JSActor.cpp @@ -15,6 +15,8 @@ #include "mozilla/dom/DOMExceptionBinding.h" #include "mozilla/dom/JSActorBinding.h" #include "mozilla/dom/JSActorManager.h" +#include "mozilla/dom/JSIPCValue.h" +#include "mozilla/dom/JSIPCValueUtils.h" #include "mozilla/dom/MessageManagerBinding.h" #include "mozilla/dom/PWindowGlobal.h" #include "mozilla/dom/Promise.h" @@ -150,9 +152,10 @@ nsresult JSActor::QueryInterfaceActor(const nsIID& aIID, void** aPtr) { return mWrappedJS->QueryInterface(aIID, aPtr); } -void JSActor::Init(const nsACString& aName) { +void JSActor::Init(const nsACString& aName, bool aSendTyped) { MOZ_ASSERT(mName.IsEmpty(), "Cannot set name twice!"); mName = aName; + mSendTyped = aSendTyped; InvokeCallback(CallbackFunction::ActorCreated); } @@ -205,9 +208,11 @@ void JSActor::SendAsyncMessage(JSContext* aCx, const nsAString& aMessageName, ErrorResult& aRv) { profiler_add_marker("SendAsyncMessage", geckoprofiler::category::IPC, {}, JSActorMessageMarker{}, mName, aMessageName); - auto data = MakeUnique<ipc::StructuredCloneData>(); - if (!nsFrameMessageManager::GetParamsForMessage(aCx, aObj, aTransfers, - *data)) { + JSIPCValueUtils::Context cx(aCx, /* aStrict = */ false); + IgnoredErrorResult error; + auto data = + JSIPCValueUtils::FromJSVal(cx, aObj, aTransfers, mSendTyped, error); + if (error.Failed()) { aRv.ThrowDataCloneError(nsPrintfCString( "Failed to serialize message '%s::%s'", NS_LossyConvertUTF16toASCII(aMessageName).get(), mName.get())); @@ -228,9 +233,10 @@ already_AddRefed<Promise> JSActor::SendQuery(JSContext* aCx, ErrorResult& aRv) { profiler_add_marker("SendQuery", geckoprofiler::category::IPC, {}, JSActorMessageMarker{}, mName, aMessageName); - auto data = MakeUnique<ipc::StructuredCloneData>(); - if (!nsFrameMessageManager::GetParamsForMessage( - aCx, aObj, JS::UndefinedHandleValue, *data)) { + JSIPCValueUtils::Context cx(aCx, /* aStrict = */ false); + IgnoredErrorResult error; + auto data = JSIPCValueUtils::FromJSVal(cx, aObj, mSendTyped, error); + if (error.Failed()) { aRv.ThrowDataCloneError(nsPrintfCString( "Failed to serialize message '%s::%s'", NS_LossyConvertUTF16toASCII(aMessageName).get(), mName.get())); @@ -369,7 +375,7 @@ void JSActor::ReceiveQueryReply(JSContext* aCx, } void JSActor::SendRawMessageInProcess( - const JSActorMessageMeta& aMeta, UniquePtr<ipc::StructuredCloneData> aData, + const JSActorMessageMeta& aMeta, JSIPCValue&& aData, UniquePtr<ipc::StructuredCloneData> aStack, OtherSideCallback&& aGetOtherSide) { MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); @@ -418,8 +424,15 @@ void JSActor::QueryHandler::RejectedCallback(JSContext* aCx, } } - UniquePtr<ipc::StructuredCloneData> data = TryClone(aCx, value); - if (!data) { + // The only valid type a QueryReject message can have is "any", so serialize + // it as an untyped value, and never log anything for it. Ideally, we would + // require that this is an error object (bug 1907175). + JSIPCValueUtils::Context cx(aCx); + IgnoredErrorResult error; + auto data = + JSIPCValueUtils::FromJSVal(cx, value, /* aSendTyped = */ false, error); + + if (error.Failed()) { // 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 @@ -427,9 +440,15 @@ void JSActor::QueryHandler::RejectedCallback(JSContext* aCx, if (!JS::CallOriginalPromiseReject(aCx, aValue)) { JS_ClearPendingException(aCx); } + + // Unlike other cases, we want to send a reject reply message even if + // serialization failed, so send the JS value undefined, rather than + // returning. + data = JSIPCValue(void_t()); } - SendReply(aCx, JSActorMessageKind::QueryReject, std::move(data)); + const JSActorMessageKind kind = JSActorMessageKind::QueryReject; + SendReply(aCx, kind, std::move(data)); } void JSActor::QueryHandler::ResolvedCallback(JSContext* aCx, @@ -439,8 +458,10 @@ void JSActor::QueryHandler::ResolvedCallback(JSContext* aCx, return; } - UniquePtr<ipc::StructuredCloneData> data = TryClone(aCx, aValue); - if (!data) { + JSIPCValueUtils::Context cx(aCx); + IgnoredErrorResult error; + auto data = JSIPCValueUtils::FromJSVal(cx, aValue, mActor->mSendTyped, error); + if (error.Failed()) { nsAutoCString msg; msg.Append(mActor->Name()); msg.Append(':'); @@ -459,12 +480,12 @@ void JSActor::QueryHandler::ResolvedCallback(JSContext* aCx, return; } - SendReply(aCx, JSActorMessageKind::QueryResolve, std::move(data)); + const JSActorMessageKind kind = JSActorMessageKind::QueryResolve; + SendReply(aCx, kind, std::move(data)); } -void JSActor::QueryHandler::SendReply( - JSContext* aCx, JSActorMessageKind aKind, - UniquePtr<ipc::StructuredCloneData> aData) { +void JSActor::QueryHandler::SendReply(JSContext* aCx, JSActorMessageKind aKind, + JSIPCValue&& aData) { MOZ_ASSERT(mActor); profiler_add_marker("SendQueryReply", geckoprofiler::category::IPC, {}, JSActorMessageMarker{}, mActor->Name(), mMessageName); diff --git a/dom/ipc/jsactor/JSActor.h b/dom/ipc/jsactor/JSActor.h @@ -9,6 +9,7 @@ #include "ipc/EnumSerializer.h" #include "js/TypeDecls.h" +#include "mozilla/dom/JSIPCValue.h" #include "mozilla/dom/PromiseNativeHandler.h" #include "nsCycleCollectionParticipant.h" #include "nsTHashMap.h" @@ -65,21 +66,20 @@ class JSActor : public nsISupports, public nsWrapperCache { // message metadata |aMetadata|. The underlying transport should call the // |ReceiveMessage| method on the other side asynchronously. virtual void SendRawMessage(const JSActorMessageMeta& aMetadata, - UniquePtr<ipc::StructuredCloneData> aData, + JSIPCValue&& aData, UniquePtr<ipc::StructuredCloneData> aStack, ErrorResult& aRv) = 0; // Helper method to send an in-process raw message. using OtherSideCallback = std::function<already_AddRefed<JSActorManager>()>; static void SendRawMessageInProcess( - const JSActorMessageMeta& aMeta, - UniquePtr<ipc::StructuredCloneData> aData, + const JSActorMessageMeta& aMeta, JSIPCValue&& aData, UniquePtr<ipc::StructuredCloneData> aStack, OtherSideCallback&& aGetOtherSide); virtual ~JSActor() = default; - void Init(const nsACString& aName); + void Init(const nsACString& aName, bool aSendTyped); bool CanSend() const { return mCanSend; } @@ -134,7 +134,7 @@ class JSActor : public nsISupports, public nsWrapperCache { ~QueryHandler() = default; void SendReply(JSContext* aCx, JSActorMessageKind aKind, - UniquePtr<ipc::StructuredCloneData> aData); + JSIPCValue&& aData); RefPtr<JSActor> mActor; RefPtr<Promise> mPromise; @@ -155,6 +155,12 @@ class JSActor : public nsISupports, public nsWrapperCache { nsTHashMap<nsUint64HashKey, PendingQuery> mPendingQueries; uint64_t mNextQueryId = 0; bool mCanSend = true; + + // If this is false, the receiver won't be doing type checking, so + // use structured clone when sending. The security of the receiver does not + // depend on this value, because it will make its own independent judgment + // about whether the message needs to be typed. + bool mSendTyped = true; }; } // namespace dom diff --git a/dom/ipc/jsactor/JSActorManager.cpp b/dom/ipc/jsactor/JSActorManager.cpp @@ -15,6 +15,8 @@ #include "mozilla/ScopeExit.h" #include "mozilla/dom/AutoEntryScript.h" #include "mozilla/dom/JSActorService.h" +#include "mozilla/dom/JSIPCValue.h" +#include "mozilla/dom/JSIPCValueUtils.h" #include "mozilla/dom/JSProcessActorProtocol.h" #include "mozilla/dom/JSWindowActorProtocol.h" #include "mozilla/dom/MessagePort.h" @@ -132,8 +134,7 @@ already_AddRefed<JSActor> JSActorManager::GetExistingActor( } while (0) void JSActorManager::ReceiveRawMessage( - const JSActorMessageMeta& aMetadata, - UniquePtr<ipc::StructuredCloneData> aData, + const JSActorMessageMeta& aMetadata, JSIPCValue&& aData, UniquePtr<ipc::StructuredCloneData> aStack) { MOZ_ASSERT(nsContentUtils::IsSafeToRunScript()); @@ -197,23 +198,11 @@ void JSActorManager::ReceiveRawMessage( #endif // DEBUG JS::Rooted<JS::Value> data(cx); - if (aData) { - aData->Read(cx, &data, error); - // StructuredCloneHolder populates an array of ports for MessageEvent.ports - // which we don't need, but which StructuredCloneHolder's destructor will - // assert on for thread safety reasons (that do not apply in this case) if - // we do not consume the array. It's possible for the Read call above to - // populate this array even in event of an error, so we must consume the - // array before processing the error. - nsTArray<RefPtr<MessagePort>> ports = aData->TakeTransferredPorts(); - // Cast to void so that the ports will actually be moved, and then - // discarded. - (void)ports; - if (error.Failed()) { - CHILD_DIAGNOSTIC_ASSERT(CycleCollectedJSRuntime::Get()->OOMReported(), - "Should not receive non-decodable data"); - return; - } + JSIPCValueUtils::ToJSVal(cx, std::move(aData), &data, error); + if (error.Failed()) { + CHILD_DIAGNOSTIC_ASSERT(CycleCollectedJSRuntime::Get()->OOMReported(), + "Should not receive non-decodable data"); + return; } switch (aMetadata.kind()) { diff --git a/dom/ipc/jsactor/JSActorManager.h b/dom/ipc/jsactor/JSActorManager.h @@ -9,6 +9,7 @@ #include "js/TypeDecls.h" #include "mozilla/dom/JSActor.h" +#include "mozilla/dom/JSIPCValue.h" #include "nsRefPtrHashtable.h" #include "nsString.h" @@ -44,7 +45,7 @@ class JSActorManager : public nsISupports { * Handle receiving a raw message from the other side. */ void ReceiveRawMessage(const JSActorMessageMeta& aMetadata, - UniquePtr<ipc::StructuredCloneData> aData, + JSIPCValue&& aData, UniquePtr<ipc::StructuredCloneData> aStack); virtual const nsACString& GetRemoteType() const = 0; diff --git a/dom/ipc/jsactor/JSActorTypeUtils.cpp b/dom/ipc/jsactor/JSActorTypeUtils.cpp @@ -0,0 +1,27 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "JSActorTypeUtils.h" + +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/JSIPCValue.h" +#include "mozilla/dom/JSIPCValueUtils.h" + +namespace mozilla::dom { + +void JSActorTypeUtils::SerializeDeserialize( + const GlobalObject& aGlobal, bool aStrict, JS::Handle<JS::Value> aVal, + JS::MutableHandle<JS::Value> aRetVal, ErrorResult& aError) { + JSIPCValueUtils::Context cx(aGlobal.Context(), aStrict); + auto ipcValue = JSIPCValueUtils::TypedFromJSVal(cx, aVal, aError); + if (aError.Failed()) { + return; + } + + JSIPCValueUtils::ToJSVal(cx, std::move(ipcValue), aRetVal, aError); +} + +} // namespace mozilla::dom diff --git a/dom/ipc/jsactor/JSActorTypeUtils.h b/dom/ipc/jsactor/JSActorTypeUtils.h @@ -0,0 +1,33 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef MOZILLA_DOM_IPC_JSACTOR_JSACTORTYPEUTILS_H_ +#define MOZILLA_DOM_IPC_JSACTOR_JSACTORTYPEUTILS_H_ + +#include "js/RootingAPI.h" +#include "js/Value.h" +#include "jstypes.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/JSActorTypeUtilsBinding.h" + +namespace mozilla { + +class ErrorResult; + +namespace dom { + +class JSActorTypeUtils { + public: + static void SerializeDeserialize(const GlobalObject& aGlobal, bool aStrict, + JS::Handle<JS::Value> aVal, + JS::MutableHandle<JS::Value> aRetVal, + ErrorResult& aError); +}; + +} // namespace dom +} // namespace mozilla + +#endif // MOZILLA_DOM_IPC_JSACTOR_JSACTORTYPEUTILS_H_ diff --git a/dom/ipc/jsactor/JSIPCValue.ipdlh b/dom/ipc/jsactor/JSIPCValue.ipdlh @@ -0,0 +1,109 @@ +/* -*- Mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 8 -*- */ +/* vim: set sw=4 ts=8 et tw=80 ft=cpp : */ +/* 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/. */ + +using mozilla::dom::MaybeDiscardedBrowsingContext from "mozilla/dom/BrowsingContext.h"; +using mozilla::null_t from "mozilla/ipc/IPCCore.h"; +using class mozilla::dom::ipc::StructuredCloneData from "mozilla/dom/ipc/StructuredCloneData.h"; +using mozilla::void_t from "mozilla/ipc/IPCCore.h"; + +include ClientIPCTypes; +include DOMTypes; + +/* + +JSIPCValue is used to serialize JS values for IPC, similar to structured cloning. + +Some major differences from structured cloning are: +1. It is a tightly controlled subset, to prevent weird values from being sent + that may behave in unexpected ways that could lead to security problems. +2. It is defined like an algebraic data type rather than a lower-level + serialization format, which makes it easier to do type checking on. +3. JSIPCValue is not used for any web APIs, so it does not need to be compatible + with any standard. + +JSIPCValue supports falling back to structured cloning for some or all of the +JS value for compatibility. This fallback can potentially involve the quirky +JSON fallback behavior implemented by nsFrameMessageManager::GetParamsForMessage. + +Some specific differences with structured cloning: +1. Cyclic data structures can't be serialized. +2. DAGs won't be preserved. +2. Non-indexed properties on Arrays will be dropped. +3. Any holes in an Array will be filled with undefined. + +Like with structured clone, non-standard prototypes on things like Arrays and +Sets will be replaced with the standard ones. + +If a property on an object being serialized is a getter, then serialization +will evaluate the getter, and the resulting object will have a plain data +property where the value is whatever the evaluation result was. There are no +guarantees about what will happen if anything being serialized is mutated by +the getter. + +*/ + +namespace mozilla { +namespace dom { + +struct JSIPCDOMRect { + double x; + double y; + double width; + double height; +}; + +struct JSIPCProperty { + nsString name; + JSIPCValue value; +}; + +struct JSIPCArray { + JSIPCValue[] elements; +}; + +struct JSIPCSet { + JSIPCValue[] elements; +}; + +struct JSIPCMapEntry { + JSIPCValue key; + JSIPCValue value; +}; + +union JSIPCValue { + // Basic JS primitive values. + void_t; // undefined + null_t; + nsString; + bool; + // double and int32_t together implement numbers, to match JS::Value. + double; + int32_t; + + // Structured clone objects are used as a fallback when serialization + // fails, or for places like WebExtensions which send these directly. + // The ClonedMessageData version is used for sending over IPC. + UniquePtr<StructuredCloneData>; + UniquePtr<ClonedMessageData>; + + // Commonly used DOM objects. + nsIPrincipal; + MaybeDiscardedBrowsingContext; + JSIPCDOMRect; + + // Plain JS object. + JSIPCProperty[]; + + JSIPCArray; + + JSIPCSet; + + // JS Map. + JSIPCMapEntry[]; +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/ipc/jsactor/JSIPCValueUtils.cpp b/dom/ipc/jsactor/JSIPCValueUtils.cpp @@ -0,0 +1,806 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "JSIPCValueUtils.h" + +#include <stdint.h> + +#include <utility> + +#include "js/Array.h" +#include "js/Class.h" // ESClass +#include "js/Id.h" +#include "js/Object.h" +#include "js/Object.h" // JS::GetBuiltinClass +#include "js/RootingAPI.h" +#include "js/String.h" +#include "js/Value.h" +#include "js/friend/DumpFunctions.h" +#include "js/friend/StackLimits.h" // js::AutoCheckRecursionLimit +#include "mozilla/Assertions.h" +#include "mozilla/CycleCollectedJSRuntime.h" // OOMReported +#include "mozilla/Logging.h" +#include "mozilla/NotNull.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/DOMRect.h" +#include "mozilla/dom/DOMRectBinding.h" +#include "mozilla/dom/MessagePort.h" +#include "mozilla/dom/StructuredCloneHolderBinding.h" +#include "nsContentUtils.h" +#include "nsFrameMessageManager.h" +#include "nsJSUtils.h" +#include "xpcpublic.h" // Logging of nsIPrincipal being unhandled. + +using js::ESClass; +using JS::GetBuiltinClass; + +namespace mozilla::dom { + +bool JSActorSupportsTypedSend(const nsACString& aName) { + if (!StaticPrefs::dom_jsipc_send_typed()) { + return false; + } + + // The Conduits and ProcessConduits actors send arguments for WebExtension + // calls in JS arrays, which means that WebExtensions are exposed to the + // idiosyncracies of nsFrameMessageManager::GetParamsForMessage(). There is + // even a subtest in browser_ext_sessions_window_tab_value.js that checks some + // specific behavior of the JSON serializer fallback. See 1960449 bug for + // details. Therefore, for now we don't want to use the typed serializer for + // those actors to reduce compatibility risk. + if (aName == "Conduits" || aName == "ProcessConduits") { + return false; + } + + // Send messages from these actors untyped. Their messages are complex, so + // using IPDL serialization might cause problems, and the actors have a lot + // of privilege, so type checking won't add much safety. + return aName != "SpecialPowers" && aName != "MarionetteCommands"; +} + +using Context = JSIPCValueUtils::Context; + +static mozilla::LazyLogModule sSerializerLogger("JSIPCSerializer"); + +#define MOZ_LOG_SERIALIZE_WARN(_arg) \ + MOZ_LOG(sSerializerLogger, mozilla::LogLevel::Warning, (_arg)) + +static JSIPCValue NoteJSAPIFailure(Context& aCx, ErrorResult& aError, + const char* aWarning) { + MOZ_LOG(sSerializerLogger, mozilla::LogLevel::Warning, ("%s", aWarning)); + aError.NoteJSContextException(aCx); + return JSIPCValue(void_t()); +} + +/* + * Conversion from JS values to JSIPCValues. + */ + +static JSIPCValue FromJSObject(Context& aCx, JS::Handle<JSObject*> aObj, + ErrorResult& aError) { + JS::RootedVector<JS::PropertyKey> idv(aCx); + + // As with TryAppendNativeProperties from StructuredClone.cpp, this ignores + // symbols by not having the JSITER_SYMBOLS flag. + if (!js::GetPropertyKeys(aCx, aObj, JSITER_OWNONLY, &idv)) { + return NoteJSAPIFailure(aCx, aError, "GetPropertyKeys failed"); + } + + nsTArray<JSIPCProperty> properties; + JS::Rooted<JS::PropertyKey> id(aCx); + JS::Rooted<Maybe<JS::PropertyDescriptor>> desc(aCx); + JS::Rooted<JS::Value> val(aCx); + for (size_t i = 0; i < idv.length(); ++i) { + id = idv[i]; + nsString stringName; + bool isSymbol = false; + if (!ConvertIdToString(aCx, id, stringName, isSymbol)) { + return NoteJSAPIFailure(aCx, aError, + "FromJSObject id string conversion failed"); + } + MOZ_ASSERT(!isSymbol); + + if (!JS_GetPropertyById(aCx, aObj, id, &val)) { + return NoteJSAPIFailure(aCx, aError, "FromJSObject get property failed"); + } + auto ipcVal = JSIPCValueUtils::TypedFromJSVal(aCx, val, aError); + if (aError.Failed()) { + MOZ_LOG_SERIALIZE_WARN("FromJSObject value conversion failed"); + return ipcVal; + } + + // If the property value has failed to serialize in non-strict mode, we want + // to drop the property entirely. Introducing a property with the value + // undefined in an object doesn't help anything, and this is also the + // behavior of GetParamsForMessage() in this situation, so this should + // increase compatibility. + // + // We always serialize to undefined when failing to serialize in non-strict + // mode. We also serialize to undefined if there was an error, but we've + // already checked aError.Failed() above. The original value could also have + // been undefined, so we must check !val.isUndefined(). + if (ipcVal.type() == JSIPCValue::Tvoid_t && !val.isUndefined()) { + MOZ_ASSERT(!aCx.mStrict, "serialized to undefined with non-strict"); + continue; + } + + properties.EmplaceBack(JSIPCProperty(stringName, std::move(ipcVal))); + } + + return JSIPCValue(std::move(properties)); +} + +static JSIPCValue FromJSArray(Context& aCx, JS::Handle<JSObject*> aObj, + ErrorResult& aError) { + uint32_t len = 0; + if (!JS::GetArrayLength(aCx, aObj, &len)) { + return NoteJSAPIFailure(aCx, aError, "FromJSArray GetArrayLength failed"); + } + + JS::Rooted<JS::Value> elt(aCx); + nsTArray<JSIPCValue> elements(len); + + for (uint32_t i = 0; i < len; i++) { + if (!JS_GetElement(aCx, aObj, i, &elt)) { + return NoteJSAPIFailure(aCx, aError, "FromJSArray GetElement failed"); + } + + auto ipcElt = JSIPCValueUtils::TypedFromJSVal(aCx, elt, aError); + if (aError.Failed()) { + MOZ_LOG_SERIALIZE_WARN("FromJSArray element conversion failed"); + return ipcElt; + } + elements.AppendElement(std::move(ipcElt)); + } + return JSIPCValue(JSIPCArray(std::move(elements))); +} + +// This is based on JSStructuredCloneWriter::traverseSet(). +static JSIPCValue FromJSSet(Context& aCx, JS::Handle<JSObject*> aObj, + ErrorResult& aError) { + JS::Rooted<JS::GCVector<JS::Value>> elements( + aCx, JS::GCVector<JS::Value>(aCx.mCx)); + if (!js::GetSetObjectKeys(aCx, aObj, &elements)) { + return NoteJSAPIFailure(aCx, aError, "FromJSSet GetSetObjectKeys failed"); + } + + nsTArray<JSIPCValue> ipcElements(elements.length()); + for (size_t i = 0; i < elements.length(); ++i) { + JSIPCValue ipcElement = + JSIPCValueUtils::TypedFromJSVal(aCx, elements[i], aError); + if (aError.Failed()) { + MOZ_LOG_SERIALIZE_WARN("FromJSSet element conversion failed"); + return ipcElement; + } + ipcElements.AppendElement(std::move(ipcElement)); + } + + return JSIPCValue(JSIPCSet(std::move(ipcElements))); +} + +static JSIPCValue FromJSMap(Context& aCx, JS::Handle<JSObject*> aObj, + ErrorResult& aError) { + JS::Rooted<JS::GCVector<JS::Value>> entries(aCx, + JS::GCVector<JS::Value>(aCx.mCx)); + if (!js::GetMapObjectKeysAndValuesInterleaved(aCx, aObj, &entries)) { + return NoteJSAPIFailure(aCx, aError, + "GetMapObjectKeysAndValuesInterleaved failed"); + } + + // The entries vector contains a sequence of key/value pairs for each entry + // in the map, and always has a length that is a multiple of 2. + MOZ_ASSERT(entries.length() % 2 == 0); + nsTArray<JSIPCMapEntry> ipcEntries(entries.length() / 2); + for (size_t i = 0; i < entries.length(); i += 2) { + auto ipcKey = JSIPCValueUtils::TypedFromJSVal(aCx, entries[i], aError); + if (aError.Failed()) { + MOZ_LOG_SERIALIZE_WARN("FromJSMap key conversion failed"); + return ipcKey; + } + auto ipcVal = JSIPCValueUtils::TypedFromJSVal(aCx, entries[i + 1], aError); + if (aError.Failed()) { + MOZ_LOG_SERIALIZE_WARN("FromJSMap value conversion failed"); + return ipcVal; + } + ipcEntries.AppendElement( + JSIPCMapEntry(std::move(ipcKey), std::move(ipcVal))); + } + + return JSIPCValue(std::move(ipcEntries)); +} + +// Log information about the JS value that we had trouble serializing. +// This can be useful when debugging why IPDL serialization is failing on +// specific objects. +static void FallbackLogging(Context& aCx, JS::Handle<JS::Value> aVal) { + if (!MOZ_LOG_TEST(sSerializerLogger, LogLevel::Info)) { + return; + } + + // For investigating certain kinds of problems, it is useful to also call + // js::DumpValue(aVal) here. Unfortunately it is not as helpful as you'd hope + // because it only gives information about the top level value. + + nsAutoString json; + if (!nsContentUtils::StringifyJSON(aCx, aVal, json, + UndefinedIsNullStringLiteral)) { + JS_ClearPendingException(aCx); + return; + } + MOZ_LOG(sSerializerLogger, mozilla::LogLevel::Info, + ("JSON serialization to: %s", NS_ConvertUTF16toUTF8(json).get())); +} + +// Use structured clone to serialize the JS value without type information. +// If aTopLevel is false, then this method is being called as a fallback in the +// middle of a fully typed IPDL serialization, so we may want to do some extra +// logging to understand any serialization failures. +static JSIPCValue UntypedFromJSVal(Context& aCx, JS::Handle<JS::Value> aVal, + ErrorResult& aError, + bool aTopLevel = false) { + js::AssertSameCompartment(aCx, aVal); + + if (!aTopLevel) { + FallbackLogging(aCx, aVal); + } + + auto data = MakeUnique<ipc::StructuredCloneData>(); + IgnoredErrorResult rv; + data->Write(aCx, aVal, JS::UndefinedHandleValue, JS::CloneDataPolicy(), rv); + if (!rv.Failed()) { + return JSIPCValue(std::move(data)); + } + + JS_ClearPendingException(aCx); + + if (!aTopLevel) { + MOZ_LOG_SERIALIZE_WARN("structured clone failed"); + } + + if (aCx.mStrict) { + aError.ThrowInvalidStateError( + "structured clone failed for strict serialization"); + return JSIPCValue(void_t()); + } + + // Unlike in nsFrameMessageManager::GetParamsForMessage(), we are probably + // right at whatever value can't be serialized, so return a dummy value in + // order to usefully serialize the rest of the value. + // + // The fallback serializer for GetParamsForMessage() does the equivalent of + // structuredClone(JSON.parse(JSON.stringify(v))), which can result in some + // odd behavior for parts of a value that are structured clonable but not + // representable in JSON. + // + // We are deliberately not fully compatible with the odd behavior of + // GetParamsForMessage(). See bug 1960449 for an example of how that can cause + // problems. Also see the second half of browser_jsat_serialize.js for + // examples of various corner cases with GetParamsForMessage() and how that + // compares to the behavior of this serializer. + return JSIPCValue(void_t()); +} + +#define MOZ_LOG_UNTYPED_FALLBACK_WARN(_str) \ + MOZ_LOG(sSerializerLogger, mozilla::LogLevel::Warning, \ + ("UntypedFromJSVal fallback: %s", _str)) + +// Try to serialize an ESClass::Other JS Object in a typeable way. +static JSIPCValue TypedFromOther(Context& aCx, JS::Handle<JS::Value> aVal, + ErrorResult& aError) { + if (!aVal.isObject()) { + MOZ_LOG_UNTYPED_FALLBACK_WARN("non-object ESClass::Other"); + return UntypedFromJSVal(aCx, aVal, aError); + } + + JS::Rooted<JSObject*> obj(aCx, &aVal.toObject()); + if (!xpc::IsReflector(obj, aCx)) { + MOZ_LOG_UNTYPED_FALLBACK_WARN("ESClass::Other without reflector"); + return UntypedFromJSVal(aCx, aVal, aError); + } + + // Checking for BrowsingContext. Based on + // StructuredCloneHolder::CustomWriteHandler(). + { + BrowsingContext* holder = nullptr; + if (NS_SUCCEEDED(UNWRAP_OBJECT(BrowsingContext, &obj, holder))) { + return JSIPCValue(MaybeDiscardedBrowsingContext(holder)); + } + } + // Checking for nsIPrincipal. Based on + // StructuredCloneHolder::WriteFullySerializableObjects(). + { + nsCOMPtr<nsISupports> base = xpc::ReflectorToISupportsStatic(obj); + nsCOMPtr<nsIPrincipal> principal = do_QueryInterface(base); + if (principal) { + // Warning: If you pass in principal directly, it gets silently converted + // to a bool. + return JSIPCValue(WrapNotNull<nsIPrincipal*>(principal)); + } + } + { + DOMRect* holder = nullptr; + if (NS_SUCCEEDED(UNWRAP_OBJECT(DOMRect, &obj, holder))) { + return JSIPCValue(JSIPCDOMRect(holder->X(), holder->Y(), holder->Width(), + holder->Height())); + } + } + + // TODO: In the case of a StructuredCloneHolder, it should be possible to + // avoid the copy of the StructuredCloneHolder's buffer list into the wrapping + // structured clone. + + // Don't warn if this is a StructuredCloneBlob: in that case, doing a + // structured clone is the preferred outcome. + if (MOZ_LOG_TEST(sSerializerLogger, LogLevel::Warning) && + !IS_INSTANCE_OF(StructuredCloneHolder, obj)) { + MOZ_LOG_UNTYPED_FALLBACK_WARN( + nsPrintfCString("ESClass::Other %s", JS::GetClass(obj)->name).get()); + } + + return UntypedFromJSVal(aCx, aVal, aError); +} + +JSIPCValue JSIPCValueUtils::TypedFromJSVal(Context& aCx, + JS::Handle<JS::Value> aVal, + ErrorResult& aError) { + js::AutoCheckRecursionLimit recursion(aCx); + if (!recursion.check(aCx)) { + return NoteJSAPIFailure(aCx, aError, "TypedFromJSVal recursion"); + } + js::AssertSameCompartment(aCx, aVal); + + switch (aVal.type()) { + case JS::ValueType::Undefined: + return JSIPCValue(void_t()); + + case JS::ValueType::Null: + return JSIPCValue(null_t()); + + case JS::ValueType::String: { + nsAutoJSString stringVal; + if (!stringVal.init(aCx, aVal.toString())) { + return NoteJSAPIFailure(aCx, aError, "String init failed"); + } + return JSIPCValue(stringVal); + } + + case JS::ValueType::Boolean: + return JSIPCValue(aVal.toBoolean()); + + case JS::ValueType::Double: + return JSIPCValue(aVal.toDouble()); + + case JS::ValueType::Int32: + return JSIPCValue(aVal.toInt32()); + + case JS::ValueType::Object: { + JS::Rooted<JSObject*> obj(aCx, &aVal.toObject()); + js::ESClass cls; + if (!JS::GetBuiltinClass(aCx, obj, &cls)) { + return NoteJSAPIFailure(aCx, aError, "GetBuiltinClass failed"); + } + + switch (cls) { + case ESClass::Object: + return FromJSObject(aCx, obj, aError); + + case ESClass::Array: + return FromJSArray(aCx, obj, aError); + + case ESClass::Set: + return FromJSSet(aCx, obj, aError); + + case ESClass::Map: + return FromJSMap(aCx, obj, aError); + + case ESClass::Other: + return TypedFromOther(aCx, aVal, aError); + + default: + MOZ_LOG_UNTYPED_FALLBACK_WARN("Unhandled ESClass"); + return UntypedFromJSVal(aCx, aVal, aError); + } + } + + default: + MOZ_LOG_UNTYPED_FALLBACK_WARN("Unhandled JS::ValueType"); + return UntypedFromJSVal(aCx, aVal, aError); + } +} + +// If we are trying to structured clone an entire JS value, we need the JSON +// serialization fallback implemented by GetParamsForMessage(). There are some +// places that attempt to send, for instance, an object where one property is a +// function, and they want the object to be sent with the function property +// removed. This behavior is (hopefully) not needed when we are doing a deeper +// serialization using JSIPCValue, because that supports only sending the +// parts of the object that are serializable, when in non-strict mode. +static JSIPCValue UntypedFromJSValWithJSONFallback( + Context& aCx, JS::Handle<JS::Value> aVal, JS::Handle<JS::Value> aTransfer, + ErrorResult& aError) { + auto scd = MakeUnique<ipc::StructuredCloneData>(); + if (!nsFrameMessageManager::GetParamsForMessage(aCx, aVal, aTransfer, *scd)) { + aError.ThrowDataCloneError("UntypedFromJSValWithJSONFallback"); + return JSIPCValue(void_t()); + } + return JSIPCValue(std::move(scd)); +} + +JSIPCValue JSIPCValueUtils::FromJSVal(Context& aCx, JS::Handle<JS::Value> aVal, + bool aSendTyped, ErrorResult& aError) { + if (aSendTyped) { + return TypedFromJSVal(aCx, aVal, aError); + } + if (aCx.mStrict) { + return UntypedFromJSVal(aCx, aVal, aError, /* aTopLevel = */ true); + } + return UntypedFromJSValWithJSONFallback(aCx, aVal, JS::UndefinedHandleValue, + aError); +} + +JSIPCValue JSIPCValueUtils::FromJSVal(Context& aCx, JS::Handle<JS::Value> aVal, + JS::Handle<JS::Value> aTransferable, + bool aSendTyped, ErrorResult& aError) { + bool hasTransferable = + !aTransferable.isNull() && !aTransferable.isUndefined(); + if (!aSendTyped || hasTransferable) { + if (hasTransferable) { + // We might be able to make this case more typeable, but as of November + // 2024, we never send a message with a transferable that we care about + // typing from child to parent, outside of testing. Support for + // transferables may be required in the future if we start typing JSActor + // messages from trusted processes. + MOZ_LOG_SERIALIZE_WARN( + "Falling back to structured clone due to transferable"); + } + MOZ_ASSERT(!aCx.mStrict, "We could support this, but we don't"); + return UntypedFromJSValWithJSONFallback(aCx, aVal, aTransferable, aError); + } + return TypedFromJSVal(aCx, aVal, aError); +} + +bool JSIPCValueUtils::PrepareForSending(SCDHolder& aHolder, + JSIPCValue& aValue) { + switch (aValue.type()) { + case JSIPCValue::Tvoid_t: + case JSIPCValue::TnsString: + case JSIPCValue::Tnull_t: + case JSIPCValue::Tbool: + case JSIPCValue::Tdouble: + case JSIPCValue::Tint32_t: + case JSIPCValue::TnsIPrincipal: + case JSIPCValue::TMaybeDiscardedBrowsingContext: + case JSIPCValue::TJSIPCDOMRect: + return true; + + case JSIPCValue::TArrayOfJSIPCProperty: { + auto& properties = aValue.get_ArrayOfJSIPCProperty(); + for (auto& p : properties) { + if (!PrepareForSending(aHolder, p.value())) { + return false; + } + } + return true; + } + + case JSIPCValue::TJSIPCArray: + for (auto& e : aValue.get_JSIPCArray().elements()) { + if (!PrepareForSending(aHolder, e)) { + return false; + } + } + return true; + + case JSIPCValue::TJSIPCSet: + for (auto& e : aValue.get_JSIPCSet().elements()) { + if (!PrepareForSending(aHolder, e)) { + return false; + } + } + return true; + + case JSIPCValue::TArrayOfJSIPCMapEntry: + for (auto& e : aValue.get_ArrayOfJSIPCMapEntry()) { + if (!PrepareForSending(aHolder, e.key())) { + return false; + } + if (!PrepareForSending(aHolder, e.value())) { + return false; + } + } + return true; + + case JSIPCValue::TStructuredCloneData: { + UniquePtr<ipc::StructuredCloneData>* scd = aHolder.mSCDs.AppendElement( + std::move(aValue.get_StructuredCloneData())); + UniquePtr<ClonedMessageData> msgData = MakeUnique<ClonedMessageData>(); + if (!(*scd)->BuildClonedMessageData(*msgData)) { + MOZ_LOG_SERIALIZE_WARN("BuildClonedMessageData failed"); + return false; + } + aValue = std::move(msgData); + return true; + } + + case JSIPCValue::TClonedMessageData: + MOZ_ASSERT(false, "ClonedMessageData in PrepareForSending"); + return false; + + default: + MOZ_ASSERT_UNREACHABLE("Invalid unhandled case"); + return false; + } +} + +static void ToJSObject(JSContext* aCx, nsTArray<JSIPCProperty>&& aProperties, + JS::MutableHandle<JS::Value> aOut, ErrorResult& aError) { + JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx)); + if (!obj) { + aError.NoteJSContextException(aCx); + return; + } + JS::Rooted<JS::PropertyKey> id(aCx); + JS::Rooted<JS::Value> jsStringName(aCx); + JS::Rooted<JS::Value> newVal(aCx); + + for (auto&& prop : aProperties) { + if (!xpc::NonVoidStringToJsval(aCx, prop.name(), &jsStringName)) { + aError.NoteJSContextException(aCx); + return; + } + if (!JS_ValueToId(aCx, jsStringName, &id)) { + aError.NoteJSContextException(aCx); + return; + } + JSIPCValueUtils::ToJSVal(aCx, std::move(prop.value()), &newVal, aError); + if (aError.Failed()) { + return; + } + if (!JS_DefinePropertyById(aCx, obj, id, newVal, JSPROP_ENUMERATE)) { + aError.NoteJSContextException(aCx); + return; + } + } + + aOut.setObject(*obj); +} + +static void ToJSArray(JSContext* aCx, nsTArray<JSIPCValue>&& aElements, + JS::MutableHandle<JS::Value> aOut, ErrorResult& aError) { + JS::Rooted<JSObject*> array(aCx, JS::NewArrayObject(aCx, aElements.Length())); + if (!array) { + aError.NoteJSContextException(aCx); + return; + } + + JS::Rooted<JS::Value> value(aCx); + for (uint32_t i = 0; i < aElements.Length(); i++) { + JSIPCValueUtils::ToJSVal(aCx, std::move(aElements.ElementAt(i)), &value, + aError); + if (aError.Failed()) { + return; + } + if (!JS_DefineElement(aCx, array, i, value, JSPROP_ENUMERATE)) { + aError.NoteJSContextException(aCx); + return; + } + } + + aOut.setObject(*array); +} + +static void ToJSSet(JSContext* aCx, nsTArray<JSIPCValue>&& aElements, + JS::MutableHandle<JS::Value> aOut, ErrorResult& aError) { + JS::Rooted<JSObject*> setObject(aCx, JS::NewSetObject(aCx)); + if (!setObject) { + aError.NoteJSContextException(aCx); + return; + } + + JS::Rooted<JS::Value> value(aCx); + for (auto&& e : aElements) { + JSIPCValueUtils::ToJSVal(aCx, std::move(e), &value, aError); + if (aError.Failed()) { + return; + } + if (!JS::SetAdd(aCx, setObject, value)) { + aError.NoteJSContextException(aCx); + return; + } + } + + aOut.setObject(*setObject); +} + +static void ToJSMap(JSContext* aCx, nsTArray<JSIPCMapEntry>&& aEntries, + JS::MutableHandle<JS::Value> aOut, ErrorResult& aError) { + JS::Rooted<JSObject*> mapObject(aCx, JS::NewMapObject(aCx)); + if (!mapObject) { + aError.NoteJSContextException(aCx); + return; + } + + JS::Rooted<JS::Value> key(aCx); + JS::Rooted<JS::Value> value(aCx); + for (auto&& e : aEntries) { + JSIPCValueUtils::ToJSVal(aCx, std::move(e.key()), &key, aError); + if (aError.Failed()) { + return; + } + JSIPCValueUtils::ToJSVal(aCx, std::move(e.value()), &value, aError); + if (aError.Failed()) { + return; + } + if (!JS::MapSet(aCx, mapObject, key, value)) { + aError.NoteJSContextException(aCx); + return; + } + } + + aOut.setObject(*mapObject); +} + +// Copied from JSActorManager.cpp. +#define CHILD_DIAGNOSTIC_ASSERT(test, msg) \ + do { \ + if (XRE_IsParentProcess()) { \ + MOZ_ASSERT(test, msg); \ + } else { \ + MOZ_DIAGNOSTIC_ASSERT(test, msg); \ + } \ + } while (0) + +static void UntypedToJSVal(JSContext* aCx, ipc::StructuredCloneData& aData, + JS::MutableHandle<JS::Value> aOut, + ErrorResult& aError) { + JS::Rooted<JS::Value> dataValue(aCx); + aData.Read(aCx, &dataValue, aError); + // StructuredCloneHolder populates an array of ports for MessageEvent.ports + // which we don't need, but which StructuredCloneHolder's destructor will + // assert on for thread safety reasons (that do not apply in this case) if + // we do not consume the array. It's possible for the Read call above to + // populate this array even in event of an error, so we must consume the + // array before processing the error. + nsTArray<RefPtr<MessagePort>> ports = aData.TakeTransferredPorts(); + // Cast to void so that the ports will actually be moved, and then + // discarded. + (void)ports; + if (aError.Failed()) { + CHILD_DIAGNOSTIC_ASSERT(CycleCollectedJSRuntime::Get()->OOMReported(), + "Should not receive non-decodable data"); + return; + } + + aOut.set(dataValue); +} + +void JSIPCValueUtils::ToJSVal(JSContext* aCx, JSIPCValue&& aIn, + JS::MutableHandle<JS::Value> aOut, + ErrorResult& aError) { + js::AutoCheckRecursionLimit recursion(aCx); + if (!recursion.check(aCx)) { + aError.NoteJSContextException(aCx); + return; + } + + switch (aIn.type()) { + case JSIPCValue::Tvoid_t: + aOut.setUndefined(); + return; + + case JSIPCValue::Tnull_t: + aOut.setNull(); + return; + + case JSIPCValue::TnsString: { + JS::Rooted<JS::Value> stringVal(aCx); + if (!xpc::StringToJsval(aCx, aIn.get_nsString(), &stringVal)) { + aError.NoteJSContextException(aCx); + return; + } + aOut.set(stringVal); + return; + } + + case JSIPCValue::Tbool: + aOut.setBoolean(aIn.get_bool()); + return; + + case JSIPCValue::Tdouble: + aOut.setDouble(aIn.get_double()); + return; + + case JSIPCValue::Tint32_t: + aOut.setInt32(aIn.get_int32_t()); + return; + + case JSIPCValue::TnsIPrincipal: { + JS::Rooted<JS::Value> result(aCx); + nsCOMPtr<nsIPrincipal> principal = aIn.get_nsIPrincipal(); + if (!ToJSValue(aCx, principal, &result)) { + aError.NoteJSContextException(aCx); + return; + } + aOut.set(result); + return; + } + + case JSIPCValue::TMaybeDiscardedBrowsingContext: { + JS::Rooted<JS::Value> result(aCx, JS::NullValue()); + const MaybeDiscardedBrowsingContext& bc = + aIn.get_MaybeDiscardedBrowsingContext(); + + // Succeed with the value null if the BC is discarded, to match the + // behavior of BrowsingContext::ReadStructuredClone(). + if (!bc.IsNullOrDiscarded()) { + if (!ToJSValue(aCx, bc.get(), &result)) { + aError.NoteJSContextException(aCx); + return; + } + if (!result.isObject()) { + aError.ThrowInvalidStateError( + "Non-object when wrapping BrowsingContext"); + return; + } + } + aOut.set(result); + return; + } + + case JSIPCValue::TJSIPCDOMRect: { + nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx); + if (!global) { + aError.ThrowInvalidStateError("No global"); + return; + } + const JSIPCDOMRect& idr = aIn.get_JSIPCDOMRect(); + RefPtr<DOMRect> domRect = + new DOMRect(global, idr.x(), idr.y(), idr.width(), idr.height()); + JS::Rooted<JS::Value> result(aCx, JS::NullValue()); + if (!ToJSValue(aCx, domRect, &result)) { + aError.NoteJSContextException(aCx); + return; + } + aOut.set(result); + return; + } + + case JSIPCValue::TArrayOfJSIPCProperty: + return ToJSObject(aCx, std::move(aIn.get_ArrayOfJSIPCProperty()), aOut, + aError); + + case JSIPCValue::TJSIPCArray: + return ToJSArray(aCx, std::move(aIn.get_JSIPCArray().elements()), aOut, + aError); + + case JSIPCValue::TJSIPCSet: + return ToJSSet(aCx, std::move(aIn.get_JSIPCSet().elements()), aOut, + aError); + + case JSIPCValue::TArrayOfJSIPCMapEntry: + return ToJSMap(aCx, std::move(aIn.get_ArrayOfJSIPCMapEntry()), aOut, + aError); + + case JSIPCValue::TStructuredCloneData: { + return UntypedToJSVal(aCx, *aIn.get_StructuredCloneData(), aOut, aError); + } + + case JSIPCValue::TClonedMessageData: { + ipc::StructuredCloneData data; + data.BorrowFromClonedMessageData(*aIn.get_ClonedMessageData()); + return UntypedToJSVal(aCx, data, aOut, aError); + } + + default: + MOZ_ASSERT_UNREACHABLE("Invalid unhandled case"); + aError.ThrowInvalidStateError("Invalid unhandled case"); + return; + } +} + +} // namespace mozilla::dom diff --git a/dom/ipc/jsactor/JSIPCValueUtils.h b/dom/ipc/jsactor/JSIPCValueUtils.h @@ -0,0 +1,86 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef MOZILLA_DOM_IPC_JSACTOR_JSIPCVALUEUTILS_H_ +#define MOZILLA_DOM_IPC_JSACTOR_JSIPCVALUEUTILS_H_ + +#include "js/RootingAPI.h" +#include "js/Value.h" +#include "jstypes.h" +#include "mozilla/Maybe.h" +#include "mozilla/dom/JSActor.h" +#include "mozilla/dom/JSIPCValue.h" + +// This file contains a number of useful methods for JSIPCValue, mostly dealing +// with turning it to and from a JS value. + +namespace mozilla::dom { + +// Return true if a message for an actor with the given name should be +// sent typed. +bool JSActorSupportsTypedSend(const nsACString& aName); + +class JSIPCValueUtils { + public: + struct Context { + explicit Context(JSContext* aCx, bool aStrict = true) + : mCx(aCx), mStrict(aStrict) {} + + MOZ_IMPLICIT operator JSContext*() const { return mCx; } + + JSContext* mCx; + + // If we encounter a JS value that can't be directly serialized to + // JSIPCValue, we fall back to using structured cloning. mStrict + // determines the behavior if this structured cloning fails. If mStrict is + // true, then the entire serialization will fail. If it is false, we'll + // instead serialize to a fallback value. See UntypedFromJSVal for details. + bool mStrict; + }; + + // Convert a JS value to an IPDL representation of that value, or return + // Nothing if the value isn't supported. If aSendTyped is false, the result + // will always just be a wrapper around a StructuredCloneData. + static JSIPCValue FromJSVal(Context& aCx, JS::Handle<JS::Value> aVal, + bool aSendTyped, ErrorResult& aError); + + // Same as the above, except with support for a transfers object, if needed. + static JSIPCValue FromJSVal(Context& aCx, JS::Handle<JS::Value> aVal, + JS::Handle<JS::Value> aTransferable, + bool aSendTyped, ErrorResult& aError); + + // This is equivalent to calling FromJSVal with aSendTyped equal to true. + static JSIPCValue TypedFromJSVal(Context& aCx, JS::Handle<JS::Value> aVal, + ErrorResult& aError); + + // Wrapper class to abstract away the details of the auxiliary data structure + // needed for PrepareForSending. + class SCDHolder final { + public: + SCDHolder() = default; + ~SCDHolder() = default; + friend class JSIPCValueUtils; + + private: + nsTArray<UniquePtr<ipc::StructuredCloneData>> mSCDs; + }; + + // Prepare a JSIPCValue for IPC by turning any StructuredCloneData it + // contains into ClonedMessageData. Auxiliary data needed for IPC + // serialization will be added to aHolder, so it needs to be kept alive + // until aValue is sent over IPC. + [[nodiscard]] static bool PrepareForSending(SCDHolder& aHolder, + JSIPCValue& aValue); + + // Convert the IPDL representation of a JS value back into the equivalent + // JS value. This will return false on failure. + static void ToJSVal(JSContext* aCx, JSIPCValue&& aIn, + JS::MutableHandle<JS::Value> aOut, ErrorResult& aError); +}; + +} // namespace mozilla::dom + +#endif // MOZILLA_DOM_IPC_JSACTOR_JSIPCVALUEUTILS_H_ diff --git a/dom/ipc/jsactor/JSProcessActorChild.cpp b/dom/ipc/jsactor/JSProcessActorChild.cpp @@ -9,6 +9,8 @@ #include "mozilla/dom/ContentChild.h" #include "mozilla/dom/InProcessChild.h" #include "mozilla/dom/InProcessParent.h" +#include "mozilla/dom/JSIPCValue.h" +#include "mozilla/dom/JSIPCValueUtils.h" #include "mozilla/dom/JSProcessActorBinding.h" namespace mozilla::dom { @@ -27,7 +29,7 @@ JSObject* JSProcessActorChild::WrapObject(JSContext* aCx, } void JSProcessActorChild::SendRawMessage( - const JSActorMessageMeta& aMeta, UniquePtr<ipc::StructuredCloneData> aData, + const JSActorMessageMeta& aMeta, JSIPCValue&& aData, UniquePtr<ipc::StructuredCloneData> aStack, ErrorResult& aRv) { if (NS_WARN_IF(!CanSend() || !mManager || !mManager->GetCanSend())) { aRv.ThrowInvalidStateError("JSProcessActorChild cannot send at the moment"); @@ -45,16 +47,13 @@ void JSProcessActorChild::SendRawMessage( } // Cross-process case - send data over ContentChild to other side. - UniquePtr<ClonedMessageData> msgData; - if (aData) { - msgData = MakeUnique<ClonedMessageData>(); - if (NS_WARN_IF(!aData->BuildClonedMessageData(*msgData))) { - aRv.ThrowDataCloneError( - nsPrintfCString("JSProcessActorChild serialization error: cannot " - "clone, in actor '%s'", - PromiseFlatCString(aMeta.actorName()).get())); - return; - } + JSIPCValueUtils::SCDHolder holder; + if (NS_WARN_IF(!JSIPCValueUtils::PrepareForSending(holder, aData))) { + aRv.ThrowDataCloneError( + nsPrintfCString("JSProcessActorChild serialization error: cannot " + "clone, in actor '%s'", + PromiseFlatCString(aMeta.actorName()).get())); + return; } UniquePtr<ClonedMessageData> stackData; @@ -65,7 +64,7 @@ void JSProcessActorChild::SendRawMessage( } } - if (NS_WARN_IF(!contentChild->SendRawMessage(aMeta, msgData, stackData))) { + if (NS_WARN_IF(!contentChild->SendRawMessage(aMeta, aData, stackData))) { aRv.ThrowOperationError( nsPrintfCString("JSProcessActorChild send error in actor '%s'", PromiseFlatCString(aMeta.actorName()).get())); @@ -77,7 +76,9 @@ void JSProcessActorChild::Init(const nsACString& aName, nsIDOMProcessChild* aManager) { MOZ_ASSERT(!mManager, "Cannot Init() a JSProcessActorChild twice!"); mManager = aManager; - JSActor::Init(aName); + bool sendTyped = + !!mManager->AsContentChild() && JSActorSupportsTypedSend(aName); + JSActor::Init(aName, sendTyped); } void JSProcessActorChild::ClearManager() { mManager = nullptr; } diff --git a/dom/ipc/jsactor/JSProcessActorChild.h b/dom/ipc/jsactor/JSProcessActorChild.h @@ -39,8 +39,7 @@ class JSProcessActorChild final : public JSActor { // Send the message described by the structured clone data |aData|, and the // message metadata |aMetadata|. The underlying transport should call the // |ReceiveMessage| method on the other side asynchronously. - void SendRawMessage(const JSActorMessageMeta& aMetadata, - UniquePtr<ipc::StructuredCloneData> aData, + void SendRawMessage(const JSActorMessageMeta& aMetadata, JSIPCValue&& aData, UniquePtr<ipc::StructuredCloneData> aStack, ErrorResult& aRv) override; diff --git a/dom/ipc/jsactor/JSProcessActorParent.cpp b/dom/ipc/jsactor/JSProcessActorParent.cpp @@ -8,6 +8,8 @@ #include "mozilla/dom/InProcessChild.h" #include "mozilla/dom/InProcessParent.h" +#include "mozilla/dom/JSIPCValue.h" +#include "mozilla/dom/JSIPCValueUtils.h" #include "mozilla/dom/JSProcessActorBinding.h" namespace mozilla::dom { @@ -29,13 +31,13 @@ void JSProcessActorParent::Init(const nsACString& aName, nsIDOMProcessParent* aManager) { MOZ_ASSERT(!mManager, "Cannot Init() a JSProcessActorParent twice!"); mManager = aManager; - JSActor::Init(aName); + JSActor::Init(aName, /* aSendTyped= */ false); } JSProcessActorParent::~JSProcessActorParent() { MOZ_ASSERT(!mManager); } void JSProcessActorParent::SendRawMessage( - const JSActorMessageMeta& aMeta, UniquePtr<ipc::StructuredCloneData> aData, + const JSActorMessageMeta& aMeta, JSIPCValue&& aData, UniquePtr<ipc::StructuredCloneData> aStack, ErrorResult& aRv) { if (NS_WARN_IF(!CanSend() || !mManager || !mManager->GetCanSend())) { aRv.ThrowInvalidStateError( @@ -56,16 +58,13 @@ void JSProcessActorParent::SendRawMessage( } // Cross-process case - send data over ContentParent to other side. - UniquePtr<ClonedMessageData> msgData; - if (aData) { - msgData = MakeUnique<ClonedMessageData>(); - if (NS_WARN_IF(!aData->BuildClonedMessageData(*msgData))) { - aRv.ThrowDataCloneError( - nsPrintfCString("Actor '%s' cannot send message '%s': cannot clone.", - PromiseFlatCString(aMeta.actorName()).get(), - NS_ConvertUTF16toUTF8(aMeta.messageName()).get())); - return; - } + JSIPCValueUtils::SCDHolder holder; + if (NS_WARN_IF(!JSIPCValueUtils::PrepareForSending(holder, aData))) { + aRv.ThrowDataCloneError( + nsPrintfCString("Actor '%s' cannot send message '%s': cannot clone.", + PromiseFlatCString(aMeta.actorName()).get(), + NS_ConvertUTF16toUTF8(aMeta.messageName()).get())); + return; } UniquePtr<ClonedMessageData> stackData; @@ -76,7 +75,7 @@ void JSProcessActorParent::SendRawMessage( } } - if (NS_WARN_IF(!contentParent->SendRawMessage(aMeta, msgData, stackData))) { + if (NS_WARN_IF(!contentParent->SendRawMessage(aMeta, aData, stackData))) { aRv.ThrowOperationError( nsPrintfCString("JSProcessActorParent send error in actor '%s'", PromiseFlatCString(aMeta.actorName()).get())); diff --git a/dom/ipc/jsactor/JSProcessActorParent.h b/dom/ipc/jsactor/JSProcessActorParent.h @@ -46,7 +46,7 @@ class JSProcessActorParent final : public JSActor { // message metadata |aMetadata|. The underlying transport should call the // |ReceiveMessage| method on the other side asynchronously. virtual void SendRawMessage(const JSActorMessageMeta& aMetadata, - UniquePtr<ipc::StructuredCloneData> aData, + JSIPCValue&& aData, UniquePtr<ipc::StructuredCloneData> aStack, ErrorResult& aRv) override; diff --git a/dom/ipc/jsactor/JSWindowActorChild.cpp b/dom/ipc/jsactor/JSWindowActorChild.cpp @@ -6,8 +6,11 @@ #include "mozilla/dom/JSWindowActorChild.h" +#include "JSIPCValueUtils.h" #include "mozilla/dom/BrowsingContext.h" #include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/JSIPCValue.h" +#include "mozilla/dom/JSIPCValueUtils.h" #include "mozilla/dom/JSWindowActorBinding.h" #include "mozilla/dom/MessageManagerBinding.h" #include "mozilla/dom/WindowGlobalChild.h" @@ -34,11 +37,12 @@ void JSWindowActorChild::Init(const nsACString& aName, WindowGlobalChild* aManager) { MOZ_ASSERT(!mManager, "Cannot Init() a JSWindowActorChild twice!"); mManager = aManager; - JSActor::Init(aName); + bool sendTyped = !mManager->IsInProcess() && JSActorSupportsTypedSend(aName); + JSActor::Init(aName, sendTyped); } void JSWindowActorChild::SendRawMessage( - const JSActorMessageMeta& aMeta, UniquePtr<ipc::StructuredCloneData> aData, + const JSActorMessageMeta& aMeta, JSIPCValue&& aData, UniquePtr<ipc::StructuredCloneData> aStack, ErrorResult& aRv) { if (!CanSend() || !mManager || !mManager->CanSend()) { aRv.ThrowInvalidStateError("JSWindowActorChild cannot send at the moment"); @@ -53,16 +57,13 @@ void JSWindowActorChild::SendRawMessage( } // Cross-process case - send data over WindowGlobalChild to other side. - UniquePtr<ClonedMessageData> msgData; - if (aData) { - msgData = MakeUnique<ClonedMessageData>(); - if (!aData->BuildClonedMessageData(*msgData)) { - aRv.ThrowDataCloneError( - nsPrintfCString("JSWindowActorChild serialization error: cannot " - "clone, in actor '%s'", - PromiseFlatCString(aMeta.actorName()).get())); - return; - } + JSIPCValueUtils::SCDHolder holder; + if (!JSIPCValueUtils::PrepareForSending(holder, aData)) { + aRv.ThrowDataCloneError( + nsPrintfCString("JSWindowActorChild serialization error: cannot " + "clone, in actor '%s'", + PromiseFlatCString(aMeta.actorName()).get())); + return; } UniquePtr<ClonedMessageData> stackData; @@ -73,7 +74,7 @@ void JSWindowActorChild::SendRawMessage( } } - if (!mManager->SendRawMessage(aMeta, msgData, stackData)) { + if (!mManager->SendRawMessage(aMeta, aData, stackData)) { aRv.ThrowOperationError( nsPrintfCString("JSWindowActorChild send error in actor '%s'", PromiseFlatCString(aMeta.actorName()).get())); diff --git a/dom/ipc/jsactor/JSWindowActorChild.h b/dom/ipc/jsactor/JSWindowActorChild.h @@ -64,8 +64,7 @@ class JSWindowActorChild final : public JSActor { Nullable<WindowProxyHolder> GetContentWindow(ErrorResult& aRv); protected: - void SendRawMessage(const JSActorMessageMeta& aMeta, - UniquePtr<ipc::StructuredCloneData> aData, + void SendRawMessage(const JSActorMessageMeta& aMeta, JSIPCValue&& aData, UniquePtr<ipc::StructuredCloneData> aStack, ErrorResult& aRv) override; diff --git a/dom/ipc/jsactor/JSWindowActorParent.cpp b/dom/ipc/jsactor/JSWindowActorParent.cpp @@ -8,6 +8,8 @@ #include "mozilla/dom/BrowserParent.h" #include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/JSIPCValue.h" +#include "mozilla/dom/JSIPCValueUtils.h" #include "mozilla/dom/JSWindowActorBinding.h" #include "mozilla/dom/MessageManagerBinding.h" #include "mozilla/dom/WindowGlobalChild.h" @@ -32,11 +34,11 @@ void JSWindowActorParent::Init(const nsACString& aName, WindowGlobalParent* aManager) { MOZ_ASSERT(!mManager, "Cannot Init() a JSWindowActorParent twice!"); mManager = aManager; - JSActor::Init(aName); + JSActor::Init(aName, /* aSendTyped= */ false); } void JSWindowActorParent::SendRawMessage( - const JSActorMessageMeta& aMeta, UniquePtr<ipc::StructuredCloneData> aData, + const JSActorMessageMeta& aMeta, JSIPCValue&& aData, UniquePtr<ipc::StructuredCloneData> aStack, ErrorResult& aRv) { if (NS_WARN_IF(!CanSend() || !mManager || !mManager->CanSend())) { aRv.ThrowInvalidStateError("JSWindowActorParent cannot send at the moment"); @@ -50,16 +52,13 @@ void JSWindowActorParent::SendRawMessage( return; } - UniquePtr<ClonedMessageData> msgData; - if (aData) { - msgData = MakeUnique<ClonedMessageData>(); - if (NS_WARN_IF(!aData->BuildClonedMessageData(*msgData))) { - aRv.ThrowDataCloneError( - nsPrintfCString("JSWindowActorParent serialization error: cannot " - "clone, in actor '%s'", - PromiseFlatCString(aMeta.actorName()).get())); - return; - } + JSIPCValueUtils::SCDHolder holder; + if (NS_WARN_IF(!JSIPCValueUtils::PrepareForSending(holder, aData))) { + aRv.ThrowDataCloneError( + nsPrintfCString("JSWindowActorParent serialization error: cannot " + "clone, in actor '%s'", + PromiseFlatCString(aMeta.actorName()).get())); + return; } UniquePtr<ClonedMessageData> stackData; @@ -70,7 +69,7 @@ void JSWindowActorParent::SendRawMessage( } } - if (NS_WARN_IF(!mManager->SendRawMessage(aMeta, msgData, stackData))) { + if (NS_WARN_IF(!mManager->SendRawMessage(aMeta, aData, stackData))) { aRv.ThrowOperationError( nsPrintfCString("JSWindowActorParent send error in actor '%s'", PromiseFlatCString(aMeta.actorName()).get())); diff --git a/dom/ipc/jsactor/JSWindowActorParent.h b/dom/ipc/jsactor/JSWindowActorParent.h @@ -49,8 +49,7 @@ class JSWindowActorParent final : public JSActor { CanonicalBrowsingContext* GetBrowsingContext(ErrorResult& aRv); protected: - void SendRawMessage(const JSActorMessageMeta& aMeta, - UniquePtr<ipc::StructuredCloneData> aData, + void SendRawMessage(const JSActorMessageMeta& aMeta, JSIPCValue&& aData, UniquePtr<ipc::StructuredCloneData> aStack, ErrorResult& aRv) override; diff --git a/dom/ipc/jsactor/moz.build b/dom/ipc/jsactor/moz.build @@ -8,6 +8,8 @@ EXPORTS.mozilla.dom += [ "JSActor.h", "JSActorManager.h", "JSActorService.h", + "JSActorTypeUtils.h", + "JSIPCValueUtils.h", "JSProcessActorChild.h", "JSProcessActorParent.h", "JSProcessActorProtocol.h", @@ -24,6 +26,8 @@ UNIFIED_SOURCES += [ "JSActor.cpp", "JSActorManager.cpp", "JSActorService.cpp", + "JSActorTypeUtils.cpp", + "JSIPCValueUtils.cpp", "JSProcessActorChild.cpp", "JSProcessActorParent.cpp", "JSProcessActorProtocol.cpp", @@ -37,6 +41,10 @@ LOCAL_INCLUDES += [ "/js/xpconnect/src", ] +IPDL_SOURCES += [ + "JSIPCValue.ipdlh", +] + include("/ipc/chromium/chromium-config.mozbuild") FINAL_LIBRARY = "xul" diff --git a/dom/ipc/tests/browser.toml b/dom/ipc/tests/browser.toml @@ -74,6 +74,8 @@ skip-if = [ "win11_2009 && opt", # Bug 1890386 ] +["browser_jsat_serialize.js"] + ["browser_layers_unloaded_while_interruptingJS.js"] ["browser_memory_distribution_telemetry.js"] diff --git a/dom/ipc/tests/browser_jsat_serialize.js b/dom/ipc/tests/browser_jsat_serialize.js @@ -0,0 +1,415 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* global JSActorTypeUtils */ + +function equivArrays(src, dst, m) { + ok(Array.isArray(src), "src array isArray"); + ok(Array.isArray(dst), "dst array isArray"); + ok(dst instanceof Array, "dst array is an instance of Array"); + is(src.length, dst.length, m + ": arrays need same length"); + for (let i = 0; i < src.length; i++) { + if (Array.isArray(src[i])) { + equivArrays(src[i], dst[i], m); + } else { + is(src[i], dst[i], m + ": element " + i + " should match"); + } + } +} + +add_task(async () => { + function testPrimitive(v1) { + let v2 = JSActorTypeUtils.serializeDeserialize(true, v1); + is(v1, v2, "initial and deserialized values are the same"); + } + + // Undefined. + testPrimitive(undefined); + + // String. + testPrimitive("a string"); + testPrimitive(""); + + // Null. + testPrimitive(null); + + // Boolean. + testPrimitive(true); + testPrimitive(false); + + // Double. + testPrimitive(3.14159); + testPrimitive(-1.1); + let nan2 = JSActorTypeUtils.serializeDeserialize(true, NaN); + ok(Number.isNaN(nan2), "NaN deserialization works"); + testPrimitive(Infinity); + testPrimitive(-Infinity); + + // int32. + testPrimitive(0); + testPrimitive(10001); + testPrimitive(-94892); + testPrimitive(2147483647); + testPrimitive(-2147483648); + + // nsIPrincipal + var sp = Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal); + testPrimitive(sp); + + // BrowsingContext + let bc1; + let TEST_URL = "https://example.org/document-builder.sjs?html=empty-document"; + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + bc1 = browser.browsingContext; + ok(bc1, "found a BC in new tab"); + ok(!bc1.isDiscarded, "BC isn't discarded before we close the tab"); + testPrimitive(bc1); + }); + ok(bc1.isDiscarded, "BC is discarded after we close the tab"); + is( + JSActorTypeUtils.serializeDeserialize(true, bc1), + null, + "discarded BC should serialize to null" + ); + + // DOMRect. + let r1 = new DOMRect(1.5, -2.8, 1e10, 0); + let r2 = JSActorTypeUtils.serializeDeserialize(true, r1); + ok(DOMRect.isInstance(r2)); + is(r1.x, r2.x, "DOMRect x"); + is(r1.y, r2.y, "DOMRect y"); + is(r1.width, r2.width, "DOMRect width"); + is(r1.height, r2.height, "DOMRect height"); + + // Objects. + let o1 = { a: true, 4: "int", b: 123 }; + let o2 = JSActorTypeUtils.serializeDeserialize(true, o1); + equivArrays(Object.keys(o1), ["4", "a", "b"], "sorted keys, before"); + equivArrays(Object.keys(o2), ["4", "a", "b"], "sorted keys, after"); + is(o1.a, o2.a, "sorted keys, first property"); + is(o1[4], o2[4], "sorted keys, second property"); + is(o1.b, o2.b, "sorted keys, third property"); + + // If an object's property is a getter, then the serialized version will have + // that property as a plain data property. + o1 = { + get a() { + return 0; + }, + }; + o2 = JSActorTypeUtils.serializeDeserialize(true, o1); + equivArrays(Object.keys(o2), ["a"], "getter keys, after"); + is(o1.a, o2.a, "value of getter matches"); + is( + typeof Object.getOwnPropertyDescriptor(o1, "a").get, + "function", + "getter is a function" + ); + let desc2 = Object.getOwnPropertyDescriptor(o2, "a"); + is(desc2.get, undefined, "getter turned into a plain data property"); + is(desc2.value, o1.a, "new data property has the correct value"); + + // Object serialization should preserve the order of properties, because this + // is visible to JS, and some code depends on it, like the receiver of + // DevToolsProcessChild:packet messages. + o1 = { b: "string", a: null }; + o2 = JSActorTypeUtils.serializeDeserialize(true, o1); + equivArrays(Object.keys(o1), ["b", "a"], "unsorted keys, before"); + equivArrays(Object.keys(o2), ["b", "a"], "unsorted keys, after"); + is(o1.a, o2.a, "unsorted keys, first property"); + is(o1.b, o2.b, "unsorted keys, second property"); + + // Array. + let emptyArray = JSActorTypeUtils.serializeDeserialize(true, []); + ok(emptyArray instanceof Array, "empty array is an array"); + is(emptyArray.length, 0, "empty array is empty"); + + let array1 = [1, "hello", [true, -3.14159], undefined]; + let array2 = JSActorTypeUtils.serializeDeserialize(true, array1); + equivArrays(array1, array2, "array before and after"); + + // Don't preserve weird prototypes for arrays. + Object.setPrototypeOf(array1, {}); + ok(!(array1 instanceof Array), "array1 has a non-Array prototype"); + array2 = JSActorTypeUtils.serializeDeserialize(true, array1); + equivArrays(array1, array2, "array before and after"); + + // An array with a hole in it gets serialized into an array without any + // holes, but with undefined at the hole indexes. + array1 = [1, 2, 3, 4, 5]; + delete array1[1]; + array2 = JSActorTypeUtils.serializeDeserialize(true, array1); + ok(!(1 in array1), "array1 has a hole at 1"); + ok(1 in array2, "array2 does not have a hole at 1"); + is(array2[1], undefined); + equivArrays(array1, array2, "array with hole before and after"); + + // An array with a non-indexed property will not have it copied over. + array1 = [1, 2, 3]; + array1.whatever = "whatever"; + array2 = JSActorTypeUtils.serializeDeserialize(true, array1); + ok("whatever" in array1, "array1 has a non-indexed property"); + ok(!("whatever" in array2), "array2 does not have a non-indexed property"); + equivArrays( + array1, + array2, + "array with non-indexed property before and after" + ); + + // Set. + let emptySet = JSActorTypeUtils.serializeDeserialize(true, new Set([])); + ok(emptySet instanceof Set, "empty set is a set"); + is(emptySet.size, 0, "empty set is empty"); + + let set1 = new Set([1, "hello", new Set([true])]); + let set2 = JSActorTypeUtils.serializeDeserialize(true, set1); + ok(set2 instanceof Set, "set2 is a set"); + is(set2.size, 3, "set2 has correct size"); + ok(set2.has(1), "1 is in the set"); + ok(set2.has("hello"), "string is in the set"); + let setCount = 0; + for (let e of set2) { + if (setCount == 0) { + is(e, 1, "first element is 1"); + } else if (setCount == 1) { + is(e, "hello", "second element is the right string"); + } else if (setCount == 2) { + ok(e instanceof Set, "third set element is a set"); + is(e.size, 1, "inner set has correct size"); + ok(e.has(true), "inner set contains true"); + } else { + ok(false, "too many set elements"); + } + setCount += 1; + } + is(setCount, 3, "found all set elements"); + + // Map. + let emptyMap = JSActorTypeUtils.serializeDeserialize(true, new Map([])); + ok(emptyMap instanceof Map, "empty map is a map"); + is(emptyMap.size, 0, "empty map is empty"); + + let map1 = new Map([ + [2, new Set([true])], + [1, "hello"], + ["bye", -11], + ]); + let map2 = JSActorTypeUtils.serializeDeserialize(true, map1); + ok(map2 instanceof Map, "map2 is a map"); + is(map2.size, 3, "map has correct size"); + ok(map2.has(1), "1 is in the map"); + ok(map2.has(2), "2 is in the map"); + ok(map2.has("bye"), "string is in the map"); + let mapCount = 0; + for (let e of map2) { + if (mapCount == 0) { + is(e[0], 2, "first key is 2"); + ok(e[1] instanceof Set, "first value is a set"); + is(e[1].size, 1, "set value has the correct size"); + ok(e[1].has(true), "set value contains true"); + } else if (mapCount == 1) { + is(e[0], 1, "second key is 1"); + is(e[1], "hello", "second value is the right string"); + } else if (mapCount == 2) { + is(e[0], "bye", "third key is the right string"); + is(e[1], -11, "third value is the right int"); + } else { + ok(false, "too many map elements"); + } + mapCount += 1; + } + is(mapCount, 3, "found all map elements"); + + // Test that JS values that require the use of JSIPCValue's structured clone + // fallback are serialized and deserialized properly. + await SpecialPowers.pushPrefEnv({ + set: [["dom.testing.structuredclonetester.enabled", true]], + }); + let sct1 = new StructuredCloneTester(true, true); + let sct2 = JSActorTypeUtils.serializeDeserialize(true, sct1); + ok(StructuredCloneTester.isInstance(sct2)); + is(sct1.serializable, sct2.serializable, "SC serializable"); + is(sct1.deserializable, sct2.deserializable, "SC serializable"); + + // Cyclic data structures can't be serialized. + let infiniteArray = []; + infiniteArray[0] = infiniteArray; + try { + JSActorTypeUtils.serializeDeserialize(true, infiniteArray); + ok(false, "serialization should have failed"); + } catch (e) { + is(e.name, "InternalError", "expected name"); + is(e.message, "too much recursion", "expected message"); + } + + // Serialization doesn't preserve DAGs. + let someObj = { num: -1 }; + let dag1 = { x: someObj, y: someObj }; + let dag2 = JSActorTypeUtils.serializeDeserialize(true, dag1); + is(dag1.x, dag1.y, "shared object"); + isnot(dag2.x, dag2.y, "serialization doesn't preserve object DAGs"); + is(dag2.x.num, dag2.y.num, "values are copied"); + + array1 = [3]; + let r = JSActorTypeUtils.serializeDeserialize(true, [array1, array1]); + isnot(r[0], r[1], "serialization doesn't preserve array DAGs"); + equivArrays(r[0], r[1], "DAG array values are copied"); +}); + +add_task(async () => { + // Test the behavior of attempting to serialize a JS value that has a + // component that can't be serialized. This will also demonstrate some + // deliberate incompatibilities with nsFrameMessageManager::GetParamsForMessage(). + // In GetParamsForMessage(), if structured cloning a JS value v fails, + // it instead attempts to structured clone JSON.parse(JSON.stringify(v)), + // which can result in some odd behavior. + + function assertThrows(f, expected, desc) { + let didThrow = false; + try { + f(); + } catch (e) { + didThrow = true; + let error = e.toString(); + let errorIncluded = error.includes(expected); + ok(errorIncluded, desc + " exception didn't contain expected string"); + if (!errorIncluded) { + info(`actual error: ${error}\n`); + } + } + ok(didThrow, desc + " should throw an exception."); + } + + function assertStrictSerializationFails(v) { + assertThrows( + () => JSActorTypeUtils.serializeDeserialize(true, v), + "structured clone failed for strict serialization", + "Strict serialization" + ); + } + + function assertStructuredCloneFails(v) { + assertThrows( + () => structuredClone(v), + "could not be cloned", + "Structured clone" + ); + } + + // nsFrameMessageManager::GetParamsForMessage() takes values that can't be + // structured cloned and turns them into a string via JSON.stringify(), then + // turns them back into a value via JSON.parse(), then attempts to structured + // clone that value. This test function emulates that behavior. + function getParamsForMessage(v) { + try { + return structuredClone(v); + } catch (e) { + let vString = JSON.stringify(v); + if (vString == undefined) { + throw new Error("not valid JSON"); + } + return structuredClone(JSON.parse(vString)); + } + } + + function assertGetParamsForMessageThrows(v) { + assertThrows( + () => getParamsForMessage(v), + "not valid JSON", + "JSON serialize" + ); + } + + // Functions are neither serializable nor valid JSON. + let nonSerializable = () => true; + + // A. Top level non-serializable value. + assertStrictSerializationFails(nonSerializable); + is( + JSActorTypeUtils.serializeDeserialize(false, nonSerializable), + undefined, + "non-serializable value turns into undefined" + ); + assertStructuredCloneFails(nonSerializable); + assertGetParamsForMessageThrows(nonSerializable); + + // B. Arrays. + // Undefined and NaN are serializable, but not valid JSON. + // In an array, both are turned into null by JSON.stringify(). + + // An array consisting entirely of serializable elements is serialized + // without any changes by either method, even if it contains undefined + // and NaN. + let array1 = [undefined, NaN, -1]; + equivArrays( + array1, + JSActorTypeUtils.serializeDeserialize(true, array1), + "array with non-JSON" + ); + equivArrays(array1, getParamsForMessage(array1)); + + // If we add a new non-serializable element, undefined and Nan become null + // when serialized via GetParamsForMessage(). The unserializable element + // becomes undefined with the typed serializer and undefined with + // GetParamsForMessage(). + let array2 = [undefined, NaN, -1, nonSerializable]; + assertStrictSerializationFails(array2); + equivArrays( + [undefined, NaN, -1, undefined], + JSActorTypeUtils.serializeDeserialize(false, array2), + "array with both non-JSON and non-serializable" + ); + equivArrays([null, null, -1, null], getParamsForMessage(array2)); + + // C. Objects. + // An object with only serializable property values is serialized without any + // changes by either method, even if some property values are undefined or NaN. + let obj1a = { x: undefined, y: NaN }; + + let obj1b = JSActorTypeUtils.serializeDeserialize(true, obj1a); + equivArrays( + Object.keys(obj1b), + ["x", "y"], + "keys after typed serialization, only serializable" + ); + is(obj1b.x, undefined, "undefined value preserved"); + ok(Number.isNaN(obj1b.y), "NaN value preserved"); + + let obj1c = getParamsForMessage(obj1a); + equivArrays( + Object.keys(obj1c), + ["x", "y"], + "keys after getParamsForMessage, only serializable" + ); + is(obj1c.x, undefined, "undefined value preserved"); + ok(Number.isNaN(obj1c.y), "NaN value preserved"); + + // Now we add a property with a non-serializable value. + let obj2a = { x: undefined, y: NaN, z: nonSerializable }; + + // With typed serialization, the property with a non-serializable value gets + // dropped, but everything else is preserved. + assertStrictSerializationFails(obj2a); + let obj2b = JSActorTypeUtils.serializeDeserialize(false, obj2a); + equivArrays( + Object.keys(obj2b), + ["x", "y"], + "keys after typed serialization, with non-serializable" + ); + is(obj2b.x, undefined, "undefined value preserved"); + ok(Number.isNaN(obj2b.y), "NaN value preserved"); + + // With GetParamsForMessage(), the property with a non-serializable value + // gets dropped. However, due to the behavior of JSON.stringify(), the + // property with a value of null is also dropped, while the property with a + // NaN value is kept, but the value is changed to null. + let obj2c = getParamsForMessage(obj2a); + equivArrays( + Object.keys(obj2c), + ["y"], + "keys after getParamsForMessage, with non-serializable" + ); + is(obj2c.y, null, "NaN property value turned to null"); +}); diff --git a/js/src/builtin/MapObject.cpp b/js/src/builtin/MapObject.cpp @@ -5,6 +5,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "builtin/MapObject-inl.h" +#include "builtin/MapObject.h" #include "jsapi.h" @@ -2029,3 +2030,55 @@ JS_PUBLIC_API bool JS::SetForEach(JSContext* cx, HandleObject obj, HandleValue callbackFn, HandleValue thisVal) { return forEach("SetForEach", cx, obj, callbackFn, thisVal); } + +JS_PUBLIC_API bool js::GetSetObjectKeys( + JSContext* cx, JS::HandleObject obj, + JS::MutableHandle<JS::GCVector<JS::Value>> keys) { + CHECK_THREAD(cx); + cx->check(obj); + + if (obj->is<SetObject>()) { + return obj->as<SetObject>().keys(keys); + } + + { + AutoEnterTableRealm<SetObject> enter(cx, obj); + if (!enter.unwrapped()->keys(keys)) { + return false; + } + } + + for (uint32_t i = 0; i < keys.length(); i++) { + if (!JS_WrapValue(cx, keys[i])) { + return false; + } + } + + return true; +} + +JS_PUBLIC_API bool js::GetMapObjectKeysAndValuesInterleaved( + JSContext* cx, JS::HandleObject obj, + JS::MutableHandle<JS::GCVector<JS::Value>> entries) { + CHECK_THREAD(cx); + cx->check(obj); + + if (obj->is<MapObject>()) { + return obj->as<MapObject>().getKeysAndValuesInterleaved(entries); + } + + { + AutoEnterTableRealm<MapObject> enter(cx, obj); + if (!enter.unwrapped()->getKeysAndValuesInterleaved(entries)) { + return false; + } + } + + for (uint32_t i = 0; i < entries.length(); i++) { + if (!JS_WrapValue(cx, entries[i])) { + return false; + } + } + + return true; +} diff --git a/js/src/jsfriendapi.h b/js/src/jsfriendapi.h @@ -13,6 +13,7 @@ #include "js/Class.h" #include "js/ColumnNumber.h" // JS::LimitedColumnNumberOneOrigin #include "js/GCAPI.h" +#include "js/GCVector.h" #include "js/HeapAPI.h" #include "js/Object.h" // JS::GetClass #include "js/shadow/Function.h" // JS::shadow::Function @@ -428,6 +429,18 @@ JS_PUBLIC_API bool AppendUnique(JSContext* cx, JS::MutableHandleIdVector base, JS::HandleIdVector others); /** + * Direct embedder access for retrieving a copy of all entries in a Set or Map + * object. + */ +JS_PUBLIC_API bool GetSetObjectKeys( + JSContext* cx, JS::HandleObject obj, + JS::MutableHandle<JS::GCVector<JS::Value>> keys); + +JS_PUBLIC_API bool GetMapObjectKeysAndValuesInterleaved( + JSContext* cx, JS::HandleObject obj, + JS::MutableHandle<JS::GCVector<JS::Value>> entries); + +/** * Determine whether the given string is an array index in the sense of * <https://tc39.github.io/ecma262/#array-index>. * diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml @@ -3452,6 +3452,14 @@ mirror: always #endif +# This controls whether JS IPC messages are sent from child to parent using +# the typeable JSIPCValue encoding, or simply sent using structured cloning. +# This being true is necessary to type check messages. +- name: dom.jsipc.send_typed + type: bool + value: false + mirror: always + # Support for input type=month, type=week. By default, disabled. - name: dom.forms.datetime.others type: bool