summaryrefslogtreecommitdiffstats
path: root/dom/ipc/SharedMap.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--dom/ipc/SharedMap.cpp466
1 files changed, 466 insertions, 0 deletions
diff --git a/dom/ipc/SharedMap.cpp b/dom/ipc/SharedMap.cpp
new file mode 100644
index 0000000000..70e12784e9
--- /dev/null
+++ b/dom/ipc/SharedMap.cpp
@@ -0,0 +1,466 @@
+/* -*- 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 "SharedMap.h"
+#include "SharedMapChangeEvent.h"
+
+#include "MemMapSnapshot.h"
+#include "ScriptPreloader-inl.h"
+
+#include "mozilla/dom/AutoEntryScript.h"
+#include "mozilla/dom/BlobImpl.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/ContentProcessMessageManager.h"
+#include "mozilla/dom/IPCBlobUtils.h"
+#include "mozilla/dom/RootedDictionary.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/IOBuffers.h"
+#include "mozilla/ScriptPreloader.h"
+
+using namespace mozilla::loader;
+
+namespace mozilla {
+
+using namespace ipc;
+
+namespace dom::ipc {
+
+// Align to size of uintptr_t here, to be safe. It's probably not strictly
+// necessary, though.
+constexpr size_t kStructuredCloneAlign = sizeof(uintptr_t);
+
+static inline void AlignTo(size_t* aOffset, size_t aAlign) {
+ if (auto mod = *aOffset % aAlign) {
+ *aOffset += aAlign - mod;
+ }
+}
+
+SharedMap::SharedMap() : DOMEventTargetHelper() {}
+
+SharedMap::SharedMap(nsIGlobalObject* aGlobal, const FileDescriptor& aMapFile,
+ size_t aMapSize, nsTArray<RefPtr<BlobImpl>>&& aBlobs)
+ : DOMEventTargetHelper(aGlobal), mBlobImpls(std::move(aBlobs)) {
+ mMapFile.reset(new FileDescriptor(aMapFile));
+ mMapSize = aMapSize;
+}
+
+bool SharedMap::Has(const nsACString& aName) {
+ Unused << MaybeRebuild();
+ return mEntries.Contains(aName);
+}
+
+void SharedMap::Get(JSContext* aCx, const nsACString& aName,
+ JS::MutableHandle<JS::Value> aRetVal, ErrorResult& aRv) {
+ auto res = MaybeRebuild();
+ if (res.isErr()) {
+ aRv.Throw(res.unwrapErr());
+ return;
+ }
+
+ Entry* entry = mEntries.Get(aName);
+ if (!entry) {
+ aRetVal.setNull();
+ return;
+ }
+
+ entry->Read(aCx, aRetVal, aRv);
+}
+
+void SharedMap::Entry::Read(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aRetVal,
+ ErrorResult& aRv) {
+ if (mData.is<StructuredCloneData>()) {
+ // We have a temporary buffer for a key that was changed after the last
+ // snapshot. Just decode it directly.
+ auto& holder = mData.as<StructuredCloneData>();
+ holder.Read(aCx, aRetVal, aRv);
+ return;
+ }
+
+ // We have a pointer to a shared memory region containing our structured
+ // clone data. Create a temporary buffer to decode that data, and then
+ // discard it so that we don't keep a separate process-local copy around any
+ // longer than necessary.
+ StructuredCloneData holder;
+ if (!holder.CopyExternalData(Data(), Size())) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+ if (mBlobCount) {
+ holder.BlobImpls().AppendElements(Blobs());
+ }
+ holder.Read(aCx, aRetVal, aRv);
+}
+
+FileDescriptor SharedMap::CloneMapFile() const {
+ if (mMap.initialized()) {
+ return mMap.cloneHandle();
+ }
+ return *mMapFile;
+}
+
+void SharedMap::Update(const FileDescriptor& aMapFile, size_t aMapSize,
+ nsTArray<RefPtr<BlobImpl>>&& aBlobs,
+ nsTArray<nsCString>&& aChangedKeys) {
+ MOZ_DIAGNOSTIC_ASSERT(!mWritable);
+
+ mMap.reset();
+ if (mMapFile) {
+ *mMapFile = aMapFile;
+ } else {
+ mMapFile.reset(new FileDescriptor(aMapFile));
+ }
+ mMapSize = aMapSize;
+ mEntries.Clear();
+ mEntryArray.reset();
+
+ mBlobImpls = std::move(aBlobs);
+
+ AutoEntryScript aes(GetParentObject(), "SharedMap change event");
+ JSContext* cx = aes.cx();
+
+ RootedDictionary<MozSharedMapChangeEventInit> init(cx);
+ if (!init.mChangedKeys.SetCapacity(aChangedKeys.Length(), fallible)) {
+ NS_WARNING("Failed to dispatch SharedMap change event");
+ return;
+ }
+ for (auto& key : aChangedKeys) {
+ Unused << init.mChangedKeys.AppendElement(NS_ConvertUTF8toUTF16(key),
+ fallible);
+ }
+
+ RefPtr<SharedMapChangeEvent> event =
+ SharedMapChangeEvent::Constructor(this, u"change"_ns, init);
+ event->SetTrusted(true);
+
+ DispatchEvent(*event);
+}
+
+const nsTArray<SharedMap::Entry*>& SharedMap::EntryArray() const {
+ if (mEntryArray.isNothing()) {
+ MaybeRebuild();
+
+ mEntryArray.emplace(mEntries.Count());
+ auto& array = mEntryArray.ref();
+ for (auto& entry : mEntries) {
+ array.AppendElement(entry.GetWeak());
+ }
+ }
+
+ return mEntryArray.ref();
+}
+
+const nsString SharedMap::GetKeyAtIndex(uint32_t aIndex) const {
+ return NS_ConvertUTF8toUTF16(EntryArray()[aIndex]->Name());
+}
+
+bool SharedMap::GetValueAtIndex(JSContext* aCx, uint32_t aIndex,
+ JS::MutableHandle<JS::Value> aResult) const {
+ ErrorResult rv;
+ EntryArray()[aIndex]->Read(aCx, aResult, rv);
+ if (rv.MaybeSetPendingException(aCx)) {
+ return false;
+ }
+ return true;
+}
+
+void SharedMap::Entry::TakeData(StructuredCloneData&& aHolder) {
+ mData = AsVariant(std::move(aHolder));
+
+ mSize = Holder().Data().Size();
+ mBlobCount = Holder().BlobImpls().Length();
+}
+
+void SharedMap::Entry::ExtractData(char* aDestPtr, uint32_t aNewOffset,
+ uint16_t aNewBlobOffset) {
+ if (mData.is<StructuredCloneData>()) {
+ char* ptr = aDestPtr;
+ Holder().Data().ForEachDataChunk([&](const char* aData, size_t aSize) {
+ memcpy(ptr, aData, aSize);
+ ptr += aSize;
+ return true;
+ });
+ MOZ_ASSERT(uint32_t(ptr - aDestPtr) == mSize);
+ } else {
+ memcpy(aDestPtr, Data(), mSize);
+ }
+
+ mData = AsVariant(aNewOffset);
+ mBlobOffset = aNewBlobOffset;
+}
+
+Result<Ok, nsresult> SharedMap::MaybeRebuild() {
+ if (!mMapFile) {
+ return Ok();
+ }
+
+ // This function maps a shared memory region created by Serialize() and reads
+ // its header block to build a new mEntries hashtable of its contents.
+ //
+ // The entries created by this function contain a pointer to this SharedMap
+ // instance, and the offsets and sizes of their structured clone data within
+ // its shared memory region. When needed, that structured clone data is
+ // retrieved directly as indexes into the SharedMap's shared memory region.
+
+ MOZ_TRY(mMap.initWithHandle(*mMapFile, mMapSize));
+ mMapFile.reset();
+
+ // We should be able to pass this range as an initializer list or an immediate
+ // param, but gcc currently chokes on that if optimization is enabled, and
+ // initializes everything to 0.
+ Range<uint8_t> range(&mMap.get<uint8_t>()[0], mMap.size());
+ InputBuffer buffer(range);
+
+ uint32_t count;
+ buffer.codeUint32(count);
+
+ for (uint32_t i = 0; i < count; i++) {
+ auto entry = MakeUnique<Entry>(*this);
+ entry->Code(buffer);
+
+ // This buffer was created at runtime, during this session, so any errors
+ // indicate memory corruption, and are fatal.
+ MOZ_RELEASE_ASSERT(!buffer.error());
+
+ // Note: While the order of evaluation of the arguments to Put doesn't
+ // matter for this (the actual move will only happen within Put), to be
+ // clear about this, we call entry->Name() before calling Put.
+ const auto& name = entry->Name();
+ mEntries.InsertOrUpdate(name, std::move(entry));
+ }
+
+ return Ok();
+}
+
+void SharedMap::MaybeRebuild() const {
+ Unused << const_cast<SharedMap*>(this)->MaybeRebuild();
+}
+
+WritableSharedMap::WritableSharedMap() : SharedMap() {
+ mWritable = true;
+ // Serialize the initial empty contents of the map immediately so that we
+ // always have a file descriptor to send to callers of CloneMapFile().
+ Unused << Serialize();
+ MOZ_RELEASE_ASSERT(mMap.initialized());
+}
+
+SharedMap* WritableSharedMap::GetReadOnly() {
+ if (!mReadOnly) {
+ nsTArray<RefPtr<BlobImpl>> blobs(mBlobImpls.Clone());
+ mReadOnly =
+ new SharedMap(ContentProcessMessageManager::Get()->GetParentObject(),
+ CloneMapFile(), MapSize(), std::move(blobs));
+ }
+ return mReadOnly;
+}
+
+Result<Ok, nsresult> WritableSharedMap::Serialize() {
+ // Serializes a new snapshot of the map, initializes a new read-only shared
+ // memory region with its contents, and updates all entries to point to that
+ // new snapshot.
+ //
+ // The layout of the snapshot is as follows:
+ //
+ // - A header containing a uint32 count field containing the number of
+ // entries in the map, followed by that number of serialized entry headers,
+ // as produced by Entry::Code.
+ //
+ // - A data block containing structured clone data for each of the entries'
+ // values. This data is referenced by absolute byte offsets from the start
+ // of the shared memory region, encoded in each of the entry header values.
+ // Each entry's data is aligned to kStructuredCloneAlign, and therefore may
+ // have alignment padding before it.
+ //
+ // This serialization format is decoded by the MaybeRebuild() method of
+ // read-only SharedMap() instances, and used to populate their mEntries
+ // hashtables.
+ //
+ // Writable instances never read the header blocks, but instead directly
+ // update their Entry instances to point to the appropriate offsets in the
+ // shared memory region created by this function.
+
+ uint32_t count = mEntries.Count();
+
+ size_t dataSize = 0;
+ size_t headerSize = sizeof(count);
+ size_t blobCount = 0;
+
+ for (const auto& entry : mEntries.Values()) {
+ headerSize += entry->HeaderSize();
+ blobCount += entry->BlobCount();
+
+ dataSize += entry->Size();
+ AlignTo(&dataSize, kStructuredCloneAlign);
+ }
+
+ size_t offset = headerSize;
+ AlignTo(&offset, kStructuredCloneAlign);
+
+ OutputBuffer header;
+ header.codeUint32(count);
+
+ MemMapSnapshot mem;
+ MOZ_TRY(mem.Init(offset + dataSize));
+
+ auto ptr = mem.Get<char>();
+
+ // We need to build the new array of blobs before we overwrite the existing
+ // one, since previously-serialized entries will store their blob references
+ // as indexes into our blobs array.
+ nsTArray<RefPtr<BlobImpl>> blobImpls(blobCount);
+
+ for (const auto& entry : mEntries.Values()) {
+ AlignTo(&offset, kStructuredCloneAlign);
+
+ size_t blobOffset = blobImpls.Length();
+ if (entry->BlobCount()) {
+ blobImpls.AppendElements(entry->Blobs());
+ }
+
+ entry->ExtractData(&ptr[offset], offset, blobOffset);
+ entry->Code(header);
+
+ offset += entry->Size();
+ }
+
+ mBlobImpls = std::move(blobImpls);
+
+ // FIXME: We should create a separate OutputBuffer class which can encode to
+ // a static memory region rather than dynamically allocating and then
+ // copying.
+ MOZ_ASSERT(header.cursor() == headerSize);
+ memcpy(ptr.get(), header.Get(), header.cursor());
+
+ // We've already updated offsets at this point. We need this to succeed.
+ mMap.reset();
+ MOZ_RELEASE_ASSERT(mem.Finalize(mMap).isOk());
+
+ return Ok();
+}
+
+void WritableSharedMap::SendTo(ContentParent* aParent) const {
+ nsTArray<IPCBlob> blobs(mBlobImpls.Length());
+
+ for (auto& blobImpl : mBlobImpls) {
+ nsresult rv = IPCBlobUtils::Serialize(blobImpl, *blobs.AppendElement());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+ }
+
+ Unused << aParent->SendUpdateSharedData(CloneMapFile(), mMap.size(), blobs,
+ mChangedKeys);
+}
+
+void WritableSharedMap::BroadcastChanges() {
+ if (mChangedKeys.IsEmpty()) {
+ return;
+ }
+
+ if (!Serialize().isOk()) {
+ return;
+ }
+
+ nsTArray<ContentParent*> parents;
+ ContentParent::GetAll(parents);
+ for (auto& parent : parents) {
+ SendTo(parent);
+ }
+
+ if (mReadOnly) {
+ nsTArray<RefPtr<BlobImpl>> blobImpls(mBlobImpls.Clone());
+ mReadOnly->Update(CloneMapFile(), mMap.size(), std::move(blobImpls),
+ std::move(mChangedKeys));
+ }
+
+ mChangedKeys.Clear();
+}
+
+void WritableSharedMap::Delete(const nsACString& aName) {
+ if (mEntries.Remove(aName)) {
+ KeyChanged(aName);
+ }
+}
+
+void WritableSharedMap::Set(JSContext* aCx, const nsACString& aName,
+ JS::Handle<JS::Value> aValue, ErrorResult& aRv) {
+ StructuredCloneData holder;
+
+ holder.Write(aCx, aValue, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ if (!holder.InputStreams().IsEmpty()) {
+ aRv.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+
+ Entry* entry = mEntries.GetOrInsertNew(aName, *this, aName);
+ entry->TakeData(std::move(holder));
+
+ KeyChanged(aName);
+}
+
+void WritableSharedMap::Flush() { BroadcastChanges(); }
+
+void WritableSharedMap::IdleFlush() {
+ mPendingFlush = false;
+ Flush();
+}
+
+nsresult WritableSharedMap::KeyChanged(const nsACString& aName) {
+ if (!mChangedKeys.ContainsSorted(aName)) {
+ mChangedKeys.InsertElementSorted(aName);
+ }
+ mEntryArray.reset();
+
+ if (!mPendingFlush) {
+ MOZ_TRY(NS_DispatchToCurrentThreadQueue(
+ NewRunnableMethod("WritableSharedMap::IdleFlush", this,
+ &WritableSharedMap::IdleFlush),
+ EventQueuePriority::Idle));
+ mPendingFlush = true;
+ }
+ return NS_OK;
+}
+
+JSObject* SharedMap::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return MozSharedMap_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+JSObject* WritableSharedMap::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return MozWritableSharedMap_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+/* static */
+already_AddRefed<SharedMapChangeEvent> SharedMapChangeEvent::Constructor(
+ EventTarget* aEventTarget, const nsAString& aType,
+ const MozSharedMapChangeEventInit& aInit) {
+ RefPtr<SharedMapChangeEvent> event = new SharedMapChangeEvent(aEventTarget);
+
+ bool trusted = event->Init(aEventTarget);
+ event->InitEvent(aType, aInit.mBubbles, aInit.mCancelable);
+ event->SetTrusted(trusted);
+ event->SetComposed(aInit.mComposed);
+
+ event->mChangedKeys = aInit.mChangedKeys;
+
+ return event.forget();
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(WritableSharedMap, SharedMap, mReadOnly)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WritableSharedMap)
+NS_INTERFACE_MAP_END_INHERITING(SharedMap)
+
+NS_IMPL_ADDREF_INHERITED(WritableSharedMap, SharedMap)
+NS_IMPL_RELEASE_INHERITED(WritableSharedMap, SharedMap)
+
+} // namespace dom::ipc
+} // namespace mozilla