Bug 1356546: Part 1 - Add a StructuredCloneHolder JS helper to hold opaque structured clone blobs. r=billm

There are several places in the WebExtensions framework where we currently
need to repeatedly serialize and deserialize structured clone data as it
passes through message managers, which can lead to significant performance
issues.

This helper class lets us serialize a value directly from the source extension
context into an opaque blob, and then directly deserialize it into the target
context on the other end, with no X-ray overhead or clones into privileged
scopes in-between.

MozReview-Commit-ID: 4QzHi89onxc

--HG--
extra : rebase_source : 2ec196ca9ce9be90b7eadf136c938373ac7d3fdd
This commit is contained in:
Kris Maglione 2017-05-20 15:09:24 -07:00
Родитель f47b467c52
Коммит 761a458cda
10 изменённых файлов: 363 добавлений и 0 удалений

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

@ -0,0 +1,159 @@
/* -*- 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 "mozilla/dom/StructuredCloneBlob.h"
#include "js/StructuredClone.h"
#include "js/Utility.h"
#include "jswrapper.h"
#include "xpcpublic.h"
#include "mozilla/Maybe.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/dom/StructuredCloneTags.h"
namespace mozilla {
namespace dom {
StructuredCloneBlob::StructuredCloneBlob()
: StructuredCloneHolder(CloningSupported, TransferringNotSupported,
StructuredCloneScope::DifferentProcess)
{};
/* static */ already_AddRefed<StructuredCloneBlob>
StructuredCloneBlob::Constructor(GlobalObject& aGlobal, JS::HandleValue aValue,
JS::HandleObject aTargetGlobal,
ErrorResult& aRv)
{
JSContext* cx = aGlobal.Context();
RefPtr<StructuredCloneBlob> holder = new StructuredCloneBlob();
Maybe<JSAutoCompartment> ac;
JS::RootedValue value(cx, aValue);
if (aTargetGlobal) {
ac.emplace(cx, aTargetGlobal);
if (!JS_WrapValue(cx, &value)) {
aRv.NoteJSContextException(cx);
return nullptr;
}
} else if (value.isObject()) {
JS::RootedObject obj(cx, js::CheckedUnwrap(&value.toObject()));
if (!obj) {
js::ReportAccessDenied(cx);
aRv.NoteJSContextException(cx);
return nullptr;
}
ac.emplace(cx, obj);
value = JS::ObjectValue(*obj);
}
holder->Write(cx, value, aRv);
if (aRv.Failed()) {
return nullptr;
}
return holder.forget();
}
void
StructuredCloneBlob::Deserialize(JSContext* aCx, JS::HandleObject aTargetScope,
JS::MutableHandleValue aResult, ErrorResult& aRv)
{
JS::RootedObject scope(aCx, js::CheckedUnwrap(aTargetScope));
if (!scope) {
js::ReportAccessDenied(aCx);
aRv.NoteJSContextException(aCx);
return;
}
{
JSAutoCompartment ac(aCx, scope);
Read(xpc::NativeGlobal(scope), aCx, aResult, aRv);
if (aRv.Failed()) {
return;
}
}
if (!JS_WrapValue(aCx, aResult)) {
aResult.set(JS::UndefinedValue());
aRv.NoteJSContextException(aCx);
}
}
/* static */ JSObject*
StructuredCloneBlob::ReadStructuredClone(JSContext* aCx, JSStructuredCloneReader* aReader)
{
RefPtr<StructuredCloneBlob> holder = new StructuredCloneBlob();
if (!holder->ReadStructuredCloneInternal(aCx, aReader)) {
return nullptr;
}
JS::RootedObject obj(aCx);
if (holder->WrapObject(aCx, nullptr, &obj)) {
return obj.get();
}
return nullptr;
}
bool
StructuredCloneBlob::ReadStructuredCloneInternal(JSContext* aCx, JSStructuredCloneReader* aReader)
{
uint32_t length;
uint32_t version;
if (!JS_ReadUint32Pair(aReader, &length, &version)) {
return false;
}
JSStructuredCloneData data(length, length, 4096);
if (!JS_ReadBytes(aReader, data.Start(), length)) {
return false;
}
mBuffer = MakeUnique<JSAutoStructuredCloneBuffer>(mStructuredCloneScope,
&StructuredCloneHolder::sCallbacks,
this);
mBuffer->adopt(Move(data), version, &StructuredCloneHolder::sCallbacks);
return true;
}
bool
StructuredCloneBlob::WriteStructuredClone(JSContext* aCx, JSStructuredCloneWriter* aWriter)
{
auto& data = mBuffer->data();
if (!JS_WriteUint32Pair(aWriter, SCTAG_DOM_STRUCTURED_CLONE_HOLDER, 0) ||
!JS_WriteUint32Pair(aWriter, data.Size(), JS_STRUCTURED_CLONE_VERSION)) {
return false;
}
auto iter = data.Iter();
while (!iter.Done()) {
if (!JS_WriteBytes(aWriter, iter.Data(), iter.RemainingInSegment())) {
return false;
}
iter.Advance(data, iter.RemainingInSegment());
}
return true;
}
bool
StructuredCloneBlob::WrapObject(JSContext* aCx, JS::HandleObject aGivenProto, JS::MutableHandleObject aResult)
{
return StructuredCloneHolderBinding::Wrap(aCx, this, aGivenProto, aResult);
}
} // namespace dom
} // namespace mozilla

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

@ -0,0 +1,55 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
/* 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_StructuredCloneBlob_h
#define mozilla_dom_StructuredCloneBlob_h
#include "mozilla/dom/BindingDeclarations.h"
#include "mozilla/dom/StructuredCloneHolder.h"
#include "mozilla/dom/StructuredCloneHolderBinding.h"
#include "mozilla/RefCounted.h"
#include "jsapi.h"
#include "nsISupports.h"
namespace mozilla {
namespace dom {
class StructuredCloneBlob : public StructuredCloneHolder
, public RefCounted<StructuredCloneBlob>
{
public:
explicit StructuredCloneBlob();
static JSObject* ReadStructuredClone(JSContext* aCx, JSStructuredCloneReader* aReader);
bool WriteStructuredClone(JSContext* aCx, JSStructuredCloneWriter* aWriter);
static already_AddRefed<StructuredCloneBlob>
Constructor(GlobalObject& aGlobal, JS::HandleValue aValue, JS::HandleObject aTargetGlobal, ErrorResult& aRv);
void Deserialize(JSContext* aCx, JS::HandleObject aTargetScope,
JS::MutableHandleValue aResult, ErrorResult& aRv);
nsISupports* GetParentObject() const { return nullptr; }
JSObject* GetWrapper() const { return nullptr; }
bool WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto, JS::MutableHandleObject aResult);
protected:
template <typename T, detail::RefCountAtomicity>
friend class detail::RefCounted;
~StructuredCloneBlob() = default;
private:
bool ReadStructuredCloneInternal(JSContext* aCx, JSStructuredCloneReader* aReader);
};
} // namespace dom
} // namespace mozilla
#endif // mozilla_dom_StructuredCloneBlob_h

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

@ -10,6 +10,7 @@
#include "mozilla/AutoRestore.h"
#include "mozilla/dom/BlobBinding.h"
#include "mozilla/dom/CryptoKey.h"
#include "mozilla/dom/StructuredCloneBlob.h"
#include "mozilla/dom/Directory.h"
#include "mozilla/dom/DirectoryBinding.h"
#include "mozilla/dom/File.h"
@ -357,6 +358,10 @@ StructuredCloneHolder::ReadFullySerializableObjects(JSContext* aCx,
return ReadStructuredCloneImageData(aCx, aReader);
}
if (aTag == SCTAG_DOM_STRUCTURED_CLONE_HOLDER) {
return StructuredCloneBlob::ReadStructuredClone(aCx, aReader);
}
if (aTag == SCTAG_DOM_WEBCRYPTO_KEY || aTag == SCTAG_DOM_URLSEARCHPARAMS) {
nsIGlobalObject *global = xpc::NativeGlobal(JS::CurrentGlobalOrNull(aCx));
if (!global) {
@ -453,6 +458,14 @@ StructuredCloneHolder::WriteFullySerializableObjects(JSContext* aCx,
}
}
// See if this is a StructuredCloneBlob object.
{
StructuredCloneBlob* holder = nullptr;
if (NS_SUCCEEDED(UNWRAP_OBJECT(StructuredCloneHolder, aObj, holder))) {
return holder->WriteStructuredClone(aCx, aWriter);
}
}
// Handle URLSearchParams cloning
{
URLSearchParams* usp = nullptr;

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

@ -62,6 +62,8 @@ enum StructuredCloneTags {
SCTAG_DOM_INPUTSTREAM,
SCTAG_DOM_STRUCTURED_CLONE_HOLDER,
// When adding a new tag for IDB, please don't add it to the end of the list!
// Tags that are supported by IDB must not ever change. See the static assert
// in IDBObjectStore.cpp, method CommonStructuredCloneReadCallback.

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

@ -196,6 +196,7 @@ EXPORTS.mozilla.dom += [
'SameProcessMessageQueue.h',
'ScreenOrientation.h',
'ShadowRoot.h',
'StructuredCloneBlob.h',
'StructuredCloneHolder.h',
'StructuredCloneTags.h',
'StyleSheetList.h',
@ -338,6 +339,7 @@ UNIFIED_SOURCES += [
'SameProcessMessageQueue.cpp',
'ScreenOrientation.cpp',
'ShadowRoot.cpp',
'StructuredCloneBlob.cpp',
'StructuredCloneHolder.cpp',
'StyleSheetList.cpp',
'SubtleCrypto.cpp',

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

@ -0,0 +1,98 @@
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/Services.jsm");
const global = this;
add_task(async function test_structuredCloneHolder() {
let principal = Services.scriptSecurityManager.createCodebasePrincipal(
Services.io.newURI("http://example.com/"), {});
let sandbox = Cu.Sandbox(principal);
const obj = {foo: [{bar: "baz"}]};
let holder = new StructuredCloneHolder(obj);
// Test same-compartment deserialization
let res = holder.deserialize(global);
notEqual(res, obj, "Deserialized result is a different object from the original");
deepEqual(res, obj, "Deserialized result is deeply equivalent to the original");
equal(Cu.getObjectPrincipal(res), Cu.getObjectPrincipal(global),
"Deserialized result has the correct principal");
// Test non-object-value round-trip.
equal(new StructuredCloneHolder("foo").deserialize(global), "foo",
"Round-tripping non-object values works as expected");
// Test cross-compartment deserialization
res = holder.deserialize(sandbox);
notEqual(res, obj, "Cross-compartment-deserialized result is a different object from the original");
deepEqual(res, obj, "Cross-compartment-deserialized result is deeply equivalent to the original");
equal(Cu.getObjectPrincipal(res), principal,
"Cross-compartment-deserialized result has the correct principal");
// Test message manager transportability
const MSG = "StructuredCloneHolder";
let resultPromise = new Promise(resolve => {
Services.ppmm.addMessageListener(MSG, resolve);
});
Services.cpmm.sendAsyncMessage(MSG, holder);
res = await resultPromise;
ok(res.data instanceof StructuredCloneHolder,
"Sending structured clone holders through message managers works as expected");
deepEqual(res.data.deserialize(global), obj,
"Sending structured clone holders through message managers works as expected");
});
// Test that X-rays passed to an exported function are serialized
// through their exported wrappers.
add_task(async function test_structuredCloneHolder_xray() {
let principal = Services.scriptSecurityManager.createCodebasePrincipal(
Services.io.newURI("http://example.com/"), {});
let sandbox1 = Cu.Sandbox(principal, {wantXrays: true});
let sandbox2 = Cu.Sandbox(principal, {wantXrays: true});
Cu.evalInSandbox(`this.x = {y: "z", get z() { return "q" }}`, sandbox2);
sandbox1.x = sandbox2.x;
let holder;
Cu.exportFunction(function serialize(val) {
holder = new StructuredCloneHolder(val, sandbox1);
}, sandbox1, {defineAs: "serialize"});
Cu.evalInSandbox(`serialize(x)`, sandbox1);
const obj = {y: "z"};
let res = holder.deserialize(global);
deepEqual(res, obj, "Deserialized result is deeply equivalent to the expected object");
deepEqual(res, sandbox2.x, "Deserialized result is deeply equivalent to the X-ray-wrapped object");
equal(Cu.getObjectPrincipal(res), Cu.getObjectPrincipal(global),
"Deserialized result has the correct principal");
});

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

@ -38,6 +38,7 @@ head = head_xml.js
head = head_xml.js
[test_range.js]
head = head_xml.js
[test_structuredcloneholder.js]
[test_thirdpartyutil.js]
[test_treewalker.js]
head = head_xml.js

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

@ -798,6 +798,11 @@ DOMInterfaces = {
'implicitJSContext': [ 'close' ],
},
'StructuredCloneHolder': {
'nativeType': 'mozilla::dom::StructuredCloneBlob',
'wrapperCache': False,
},
'StyleSheet': {
'nativeType': 'mozilla::StyleSheet',
'headerFile': 'mozilla/StyleSheetInlines.h',

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

@ -0,0 +1,27 @@
/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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/.
*/
/**
* A holder for structured-clonable data which can itself be cloned with
* little overhead, and deserialized into an arbitrary global.
*/
[ChromeOnly, Exposed=(Window,System,Worker),
/**
* Serializes the given value to an opaque structured clone blob, and
* returns the result.
*
* The serialization happens in the compartment of the given global or, if no
* global is provided, the compartment of the data value.
*/
Constructor(any data, optional object? global = null)]
interface StructuredCloneHolder {
/**
* Deserializes the structured clone data in the scope of the given global,
* and returns the result.
*/
[Throws]
any deserialize(object global);
};

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

@ -790,6 +790,7 @@ WEBIDL_FILES = [
'StorageEvent.webidl',
'StorageManager.webidl',
'StorageType.webidl',
'StructuredCloneHolder.webidl',
'StyleSheet.webidl',
'StyleSheetList.webidl',
'SubtleCrypto.webidl',