Bug 1264053 - Transfer DifferentProcess ArrayBuffers by copying, r=jorendorff
authorSteve Fink <sfink@mozilla.com>
Thu, 19 Jan 2017 14:02:40 -0800
changeset 331707 468f75ba2034e33ae82dcd349425e51b6bbf7a48
parent 331706 30293a6c6e4ae95a743a2dee67eba031a6e28700
child 331708 bf223949220cb242645dfff27a7fe2fb8330c320
push id31281
push userkwierso@gmail.com
push dateMon, 30 Jan 2017 23:45:09 +0000
treeherdermozilla-central@1fe66bd0efba [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjorendorff
bugs1264053
milestone54.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1264053 - Transfer DifferentProcess ArrayBuffers by copying, r=jorendorff
js/src/builtin/TestingFunctions.cpp
js/src/tests/js1_8_5/extensions/clone-errors.js
js/src/tests/js1_8_5/extensions/clone-transferables.js
js/src/vm/StructuredClone.cpp
mfbt/BufferList.h
--- a/js/src/builtin/TestingFunctions.cpp
+++ b/js/src/builtin/TestingFunctions.cpp
@@ -2368,22 +2368,41 @@ const Class CloneBufferObject::class_ = 
     &CloneBufferObjectClassOps
 };
 
 const JSPropertySpec CloneBufferObject::props_[] = {
     JS_PSGS("clonebuffer", getCloneBuffer, setCloneBuffer, 0),
     JS_PS_END
 };
 
+static mozilla::Maybe<JS::StructuredCloneScope>
+ParseCloneScope(JSContext* cx, HandleString str)
+{
+    mozilla::Maybe<JS::StructuredCloneScope> scope;
+
+    JSAutoByteString scopeStr(cx, str);
+    if (!scopeStr)
+        return scope;
+
+    if (strcmp(scopeStr.ptr(), "SameProcessSameThread") == 0)
+        scope.emplace(JS::StructuredCloneScope::SameProcessSameThread);
+    else if (strcmp(scopeStr.ptr(), "SameProcessDifferentThread") == 0)
+        scope.emplace(JS::StructuredCloneScope::SameProcessDifferentThread);
+    else if (strcmp(scopeStr.ptr(), "DifferentProcess") == 0)
+        scope.emplace(JS::StructuredCloneScope::DifferentProcess);
+
+    return scope;
+}
+
 static bool
 Serialize(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
-    JSAutoStructuredCloneBuffer clonebuf(JS::StructuredCloneScope::SameProcessSameThread, nullptr, nullptr);
+    mozilla::Maybe<JSAutoStructuredCloneBuffer> clonebuf;
     JS::CloneDataPolicy policy;
 
     if (!args.get(2).isUndefined()) {
         RootedObject opts(cx, ToObject(cx, args.get(2)));
         if (!opts)
             return false;
 
         RootedValue v(cx);
@@ -2402,42 +2421,79 @@ Serialize(JSContext* cx, unsigned argc, 
                 // default
             } else if (strcmp(poli.ptr(), "deny") == 0) {
                 policy.denySharedArrayBuffer();
             } else {
                 JS_ReportErrorASCII(cx, "Invalid policy value for 'SharedArrayBuffer'");
                 return false;
             }
         }
+
+        if (!JS_GetProperty(cx, opts, "scope", &v))
+            return false;
+
+        if (!v.isUndefined()) {
+            RootedString str(cx, JS::ToString(cx, v));
+            if (!str)
+                return false;
+            auto scope = ParseCloneScope(cx, str);
+            if (!scope) {
+                JS_ReportErrorASCII(cx, "Invalid structured clone scope");
+                return false;
+            }
+            clonebuf.emplace(*scope, nullptr, nullptr);
+        }
     }
 
-    if (!clonebuf.write(cx, args.get(0), args.get(1), policy))
-        return false;
-
-    RootedObject obj(cx, CloneBufferObject::Create(cx, &clonebuf));
+    if (!clonebuf)
+        clonebuf.emplace(JS::StructuredCloneScope::SameProcessSameThread, nullptr, nullptr);
+
+    if (!clonebuf->write(cx, args.get(0), args.get(1), policy))
+        return false;
+
+    RootedObject obj(cx, CloneBufferObject::Create(cx, clonebuf.ptr()));
     if (!obj)
         return false;
 
     args.rval().setObject(*obj);
     return true;
 }
 
 static bool
 Deserialize(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
-    if (args.length() != 1 || !args[0].isObject()) {
-        JS_ReportErrorASCII(cx, "deserialize requires a single clonebuffer argument");
+    if (!args.get(0).isObject() || !args[0].toObject().is<CloneBufferObject>()) {
+        JS_ReportErrorASCII(cx, "deserialize requires a clonebuffer argument");
         return false;
     }
 
-    if (!args[0].toObject().is<CloneBufferObject>()) {
-        JS_ReportErrorASCII(cx, "deserialize requires a clonebuffer");
-        return false;
+    JS::StructuredCloneScope scope = JS::StructuredCloneScope::SameProcessSameThread;
+    if (args.get(1).isObject()) {
+        RootedObject opts(cx, &args[1].toObject());
+        if (!opts)
+            return false;
+
+        RootedValue v(cx);
+        if (!JS_GetProperty(cx, opts, "scope", &v))
+            return false;
+
+        if (!v.isUndefined()) {
+            RootedString str(cx, JS::ToString(cx, v));
+            if (!str)
+                return false;
+            auto maybeScope = ParseCloneScope(cx, str);
+            if (!maybeScope) {
+                JS_ReportErrorASCII(cx, "Invalid structured clone scope");
+                return false;
+            }
+
+            scope = *maybeScope;
+        }
     }
 
     Rooted<CloneBufferObject*> obj(cx, &args[0].toObject().as<CloneBufferObject>());
 
     // Clone buffer was already consumed?
     if (!obj->data()) {
         JS_ReportErrorASCII(cx, "deserialize given invalid clone buffer "
                             "(transferables already consumed?)");
@@ -2446,18 +2502,19 @@ Deserialize(JSContext* cx, unsigned argc
 
     bool hasTransferable;
     if (!JS_StructuredCloneHasTransferables(*obj->data(), &hasTransferable))
         return false;
 
     RootedValue deserialized(cx);
     if (!JS_ReadStructuredClone(cx, *obj->data(),
                                 JS_STRUCTURED_CLONE_VERSION,
-                                JS::StructuredCloneScope::SameProcessSameThread,
-                                &deserialized, nullptr, nullptr)) {
+                                scope,
+                                &deserialized, nullptr, nullptr))
+    {
         return false;
     }
     args.rval().set(deserialized);
 
     if (hasTransferable)
         obj->discard();
 
     return true;
@@ -4490,25 +4547,32 @@ gc::ZealModeHelpText),
 "setIonCheckGraphCoherency(bool)",
 "  Set whether Ion should perform graph consistency (DEBUG-only) assertions. These assertions\n"
 "  are valuable and should be generally enabled, however they can be very expensive for large\n"
 "  (wasm) programs."),
 
     JS_FN_HELP("serialize", Serialize, 1, 0,
 "serialize(data, [transferables, [policy]])",
 "  Serialize 'data' using JS_WriteStructuredClone. Returns a structured\n"
-"  clone buffer object. 'policy' must be an object. The following keys'\n"
-"  string values will be used to determine whether the corresponding types\n"
-"  may be serialized (value 'allow', the default) or not (value 'deny').\n"
-"  If denied types are encountered a TypeError will be thrown during cloning.\n"
-"  Valid keys: 'SharedArrayBuffer'."),
+"  clone buffer object. 'policy' may be an options hash. Valid keys:\n"
+"    'SharedArrayBuffer' - either 'allow' (the default) or 'deny'\n"
+"    to specify whether SharedArrayBuffers may be serialized.\n"
+"\n"
+"    'scope' - SameProcessSameThread, SameProcessDifferentThread, or\n"
+"    DifferentProcess. Determines how some values will be serialized.\n"
+"    Clone buffers may only be deserialized with a compatible scope."),
 
     JS_FN_HELP("deserialize", Deserialize, 1, 0,
-"deserialize(clonebuffer)",
-"  Deserialize data generated by serialize."),
+"deserialize(clonebuffer[, opts])",
+"  Deserialize data generated by serialize. 'opts' is an options hash with one\n"
+"  recognized key 'scope', which limits the clone buffers that are considered\n"
+"  valid. Allowed values: 'SameProcessSameThread', 'SameProcessDifferentThread',\n"
+"  and 'DifferentProcess'. So for example, a DifferentProcess clone buffer\n"
+"  may be deserialized in any scope, but a SameProcessSameThread clone buffer\n"
+"  cannot be deserialized in a DifferentProcess scope."),
 
     JS_FN_HELP("detachArrayBuffer", DetachArrayBuffer, 1, 0,
 "detachArrayBuffer(buffer)",
 "  Detach the given ArrayBuffer object from its memory, i.e. as if it\n"
 "  had been transferred to a WebWorker."),
 
     JS_FN_HELP("helperThreadCount", HelperThreadCount, 0, 0,
 "helperThreadCount()",
--- a/js/src/tests/js1_8_5/extensions/clone-errors.js
+++ b/js/src/tests/js1_8_5/extensions/clone-errors.js
@@ -17,9 +17,25 @@ check(new Error("oops"));
 check(this);
 check(Math);
 check(function () {});
 check(new Proxy({}, {}));
 
 // A failing getter.
 check({get x() { throw new Error("fail"); }});
 
+// Mismatched scopes.
+for (let [write_scope, read_scope] of [['SameProcessSameThread', 'SameProcessDifferentThread'],
+                                       ['SameProcessSameThread', 'DifferentProcess'],
+                                       ['SameProcessDifferentThread', 'DifferentProcess']])
+{
+  var ab = new ArrayBuffer(12);
+  var buffer = serialize(ab, [ab], { scope: write_scope });
+  var caught = false;
+  try {
+    deserialize(buffer, { scope: read_scope });
+  } catch (exc) {
+    caught = true;
+  }
+  assertEq(caught, true, `${write_scope} clone buffer should not be deserializable as ${read_scope}`);
+}
+
 reportCompare(0, 0, "ok");
--- a/js/src/tests/js1_8_5/extensions/clone-transferables.js
+++ b/js/src/tests/js1_8_5/extensions/clone-transferables.js
@@ -1,19 +1,28 @@
 // |reftest| skip-if(!xulRuntime.shell)
 // Any copyright is dedicated to the Public Domain.
 // http://creativecommons.org/licenses/publicdomain/
 
-function test() {
-    for (var size of [0, 8, 16, 200, 1000, 4096, -8, -200, -8192, -65536]) {
-        size = Math.abs(size);
+function* buffer_options() {
+  for (var scope of ["SameProcessSameThread", "SameProcessDifferentThread", "DifferentProcess"]) {
+    for (var size of [0, 8, 16, 200, 1000, 4096, 8192, 65536]) {
+      yield { scope, size };
+    }
+  }
+}
 
+
+function test() {
+    for (var {scope, size} of buffer_options()) {
         var old = new ArrayBuffer(size);
-        var copy = deserialize(serialize(old, [old]));
+        var copy = deserialize(serialize([old, old], [old], { scope }), { scope });
         assertEq(old.byteLength, 0);
+        assertEq(copy[0] === copy[1], true);
+        copy = copy[0];
         assertEq(copy.byteLength, size);
 
         var constructors = [ Int8Array,
                              Uint8Array,
                              Int16Array,
                              Uint16Array,
                              Int32Array,
                              Uint32Array,
@@ -27,17 +36,17 @@ function test() {
 
             var buf = new ArrayBuffer(size);
             var old_arr = new ctor(buf);
             assertEq(buf.byteLength, size);
             assertEq(buf, old_arr.buffer);
             if (!dataview)
                 assertEq(old_arr.length, size / old_arr.BYTES_PER_ELEMENT);
 
-            var copy_arr = deserialize(serialize(old_arr, [ buf ]));
+            var copy_arr = deserialize(serialize(old_arr, [ buf ], { scope }), { scope });
             assertEq(buf.byteLength, 0,
                      "donor array buffer should be detached");
             if (!dataview) {
                 assertEq(old_arr.length, 0,
                          "donor typed array should be detached");
             }
             assertEq(copy_arr.buffer.byteLength == size, true);
             if (!dataview)
@@ -49,17 +58,17 @@ function test() {
         }
 
         for (var ctor of constructors) {
             var dataview = (ctor === DataView);
 
             var buf = new ArrayBuffer(size);
             var old_arr = new ctor(buf);
             var dv = new DataView(buf); // Second view
-            var copy_arr = deserialize(serialize(old_arr, [ buf ]));
+            var copy_arr = deserialize(serialize(old_arr, [ buf ], { scope }), { scope });
             assertEq(buf.byteLength, 0,
                      "donor array buffer should be detached");
             assertEq(old_arr.byteLength, 0,
                      "donor typed array should be detached");
             if (!dataview) {
                 assertEq(old_arr.length, 0,
                          "donor typed array should be detached");
             }
@@ -73,29 +82,29 @@ function test() {
         }
 
         // Mutate the buffer during the clone operation. The modifications should be visible.
         if (size >= 4) {
             old = new ArrayBuffer(size);
             var view = new Int32Array(old);
             view[0] = 1;
             var mutator = { get foo() { view[0] = 2; } };
-            var copy = deserialize(serialize([ old, mutator ], [old]));
+            var copy = deserialize(serialize([ old, mutator ], [ old ], { scope }), { scope });
             var viewCopy = new Int32Array(copy[0]);
             assertEq(view.length, 0); // Underlying buffer now detached.
             assertEq(viewCopy[0], 2);
         }
 
         // Detach the buffer during the clone operation. Should throw an
         // exception.
         if (size >= 4) {
             old = new ArrayBuffer(size);
             var mutator = {
                 get foo() {
-                    deserialize(serialize(old, [old]));
+                    deserialize(serialize(old, [old], { scope }), { scope });
                 }
             };
             // The throw is not yet implemented, bug 919259.
             //var copy = deserialize(serialize([ old, mutator ], [old]));
         }
     }
 }
 
--- a/js/src/vm/StructuredClone.cpp
+++ b/js/src/vm/StructuredClone.cpp
@@ -120,16 +120,17 @@ enum StructuredDataType : uint32_t {
     /*
      * Define a separate range of numbers for Transferable-only tags, since
      * they are not used for persistent clone buffers and therefore do not
      * require bumping JS_STRUCTURED_CLONE_VERSION.
      */
     SCTAG_TRANSFER_MAP_HEADER = 0xFFFF0200,
     SCTAG_TRANSFER_MAP_PENDING_ENTRY,
     SCTAG_TRANSFER_MAP_ARRAY_BUFFER,
+    SCTAG_TRANSFER_MAP_STORED_ARRAY_BUFFER,
     SCTAG_TRANSFER_MAP_END_OF_BUILTIN_TYPES,
 
     SCTAG_END_OF_BUILTIN_TYPES
 };
 
 /*
  * Format of transfer map:
  *   <SCTAG_TRANSFER_MAP_HEADER, TransferableMapHeader(UNREAD|TRANSFERRED)>
@@ -161,31 +162,49 @@ struct BufferIterator {
 
     explicit BufferIterator(BufferList& buffer)
         : mBuffer(buffer)
         , mIter(buffer.Iter())
     {
         JS_STATIC_ASSERT(8 % sizeof(T) == 0);
     }
 
+    BufferIterator(const BufferIterator& other)
+        : mBuffer(other.mBuffer)
+        , mIter(other.mIter)
+    {
+    }
+
+    BufferIterator& operator=(const BufferIterator& other)
+    {
+        MOZ_ASSERT(&mBuffer == &other.mBuffer);
+        mIter = other.mIter;
+        return *this;
+    }
+
     BufferIterator operator++(int) {
         BufferIterator ret = *this;
         if (!mIter.AdvanceAcrossSegments(mBuffer, sizeof(T))) {
             MOZ_ASSERT(false, "Failed to read StructuredCloneData. Data incomplete");
         }
         return ret;
     }
 
     BufferIterator& operator+=(size_t size) {
         if (!mIter.AdvanceAcrossSegments(mBuffer, size)) {
             MOZ_ASSERT(false, "Failed to read StructuredCloneData. Data incomplete");
         }
         return *this;
     }
 
+    size_t operator-(const BufferIterator& other) {
+        MOZ_ASSERT(&mBuffer == &other.mBuffer);
+        return mBuffer.RangeLength(other.mIter, mIter);
+    }
+
     void next() {
         if (!mIter.AdvanceAcrossSegments(mBuffer, sizeof(T))) {
             MOZ_ASSERT(false, "Failed to read StructuredCloneData. Data incomplete");
         }
     }
 
     bool done() const {
         return mIter.Done();
@@ -206,16 +225,18 @@ struct BufferIterator {
     }
 
     BufferList& mBuffer;
     typename BufferList::IterImpl mIter;
 };
 
 struct SCOutput {
   public:
+    using Iter = BufferIterator<uint64_t, TempAllocPolicy>;
+
     explicit SCOutput(JSContext* cx);
 
     JSContext* context() const { return cx; }
 
     bool write(uint64_t u);
     bool writePair(uint32_t tag, uint32_t data);
     bool writeDouble(double d);
     bool writeBytes(const void* p, size_t nbytes);
@@ -224,21 +245,26 @@ struct SCOutput {
     bool writePtr(const void*);
 
     template <class T>
     bool writeArray(const T* p, size_t nbytes);
 
     bool extractBuffer(JSStructuredCloneData* data);
     void discardTransferables(const JSStructuredCloneCallbacks* cb, void* cbClosure);
 
+    uint64_t tell() const { return buf.Size(); }
     uint64_t count() const { return buf.Size() / sizeof(uint64_t); }
-    BufferIterator<uint64_t, TempAllocPolicy> iter() {
+    Iter iter() {
         return BufferIterator<uint64_t, TempAllocPolicy>(buf);
     }
 
+    size_t offset(Iter dest) {
+        return dest - iter();
+    }
+
   private:
     JSContext* cx;
     mozilla::BufferList<TempAllocPolicy> buf;
 };
 
 class SCInput {
     typedef js::BufferIterator<uint64_t, SystemAllocPolicy> BufferIterator;
 
@@ -257,17 +283,19 @@ class SCInput {
     bool readBytes(void* p, size_t nbytes);
     bool readChars(Latin1Char* p, size_t nchars);
     bool readChars(char16_t* p, size_t nchars);
     bool readPtr(void**);
 
     bool get(uint64_t* p);
     bool getPair(uint32_t* tagp, uint32_t* datap);
 
-    BufferIterator tell() const { return point; }
+    const BufferIterator& tell() const { return point; }
+    void seekTo(const BufferIterator& pos) { point = pos; }
+    void seekBy(size_t pos) { point += pos; }
 
     template <class T>
     bool readArray(T* p, size_t nelems);
 
     bool reportTruncated() {
          JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA,
                                    "truncated");
          return false;
@@ -285,17 +313,17 @@ class SCInput {
 
 } /* namespace js */
 
 struct JSStructuredCloneReader {
   public:
     explicit JSStructuredCloneReader(SCInput& in, JS::StructuredCloneScope scope,
                                      const JSStructuredCloneCallbacks* cb,
                                      void* cbClosure)
-        : in(in), scope(scope), objs(in.context()), allObjs(in.context()),
+        : in(in), allowedScope(scope), objs(in.context()), allObjs(in.context()),
           callbacks(cb), closure(cbClosure) { }
 
     SCInput& input() { return in; }
     bool read(MutableHandleValue vp);
 
   private:
     JSContext* context() { return in.context(); }
 
@@ -313,17 +341,28 @@ struct JSStructuredCloneReader {
     bool readArrayBuffer(uint32_t nbytes, MutableHandleValue vp);
     bool readSharedArrayBuffer(uint32_t nbytes, MutableHandleValue vp);
     bool readV1ArrayBuffer(uint32_t arrayType, uint32_t nelems, MutableHandleValue vp);
     JSObject* readSavedFrame(uint32_t principalsTag);
     bool startRead(MutableHandleValue vp);
 
     SCInput& in;
 
-    JS::StructuredCloneScope scope;
+    // The widest scope that the caller will accept, where
+    // SameProcessSameThread is the widest (it can store anything it wants) and
+    // DifferentProcess is the narrowest (it cannot contain pointers and must
+    // be valid cross-process.)
+    JS::StructuredCloneScope allowedScope;
+
+    // The scope the buffer was generated for (what sort of buffer it is.) The
+    // scope is not just a permissions thing; it also affects the storage
+    // format (eg a Transferred ArrayBuffer can be stored as a pointer for
+    // SameProcessSameThread but must have its contents in the clone buffer for
+    // DifferentProcess.)
+    JS::StructuredCloneScope storedScope;
 
     // Stack of objects with properties remaining to be read.
     AutoValueVector objs;
 
     // Stack of all objects read during this deserialization
     AutoValueVector allObjs;
 
     // The user defined callbacks that will be used for cloning.
@@ -1433,17 +1472,16 @@ JSStructuredCloneWriter::writeTransferMa
         return false;
 
     if (!out.write(transferableObjects.count()))
         return false;
 
     RootedObject obj(context());
     for (auto tr = transferableObjects.all(); !tr.empty(); tr.popFront()) {
         obj = tr.front();
-
         if (!memory.put(obj, memory.count())) {
             ReportOutOfMemory(context());
             return false;
         }
 
         // Emit a placeholder pointer.  We defer stealing the data until later
         // (and, if necessary, detaching this object if it's an ArrayBuffer).
         if (!out.writePair(SCTAG_TRANSFER_MAP_PENDING_ENTRY, JS::SCTAG_TMO_UNFILLED))
@@ -1469,67 +1507,90 @@ JSStructuredCloneWriter::transferOwnersh
     auto point = out.iter();
     MOZ_ASSERT(uint32_t(NativeEndian::swapFromLittleEndian(point.peek()) >> 32) == SCTAG_HEADER);
     point++;
     MOZ_ASSERT(uint32_t(NativeEndian::swapFromLittleEndian(point.peek()) >> 32) == SCTAG_TRANSFER_MAP_HEADER);
     point++;
     MOZ_ASSERT(NativeEndian::swapFromLittleEndian(point.peek()) == transferableObjects.count());
     point++;
 
-    RootedObject obj(context());
+    JSContext* cx = context();
+    RootedObject obj(cx);
     for (auto tr = transferableObjects.all(); !tr.empty(); tr.popFront()) {
         obj = tr.front();
 
         uint32_t tag;
         JS::TransferableOwnership ownership;
         void* content;
         uint64_t extraData;
 
 #if DEBUG
         SCInput::getPair(point.peek(), &tag, (uint32_t*) &ownership);
         MOZ_ASSERT(tag == SCTAG_TRANSFER_MAP_PENDING_ENTRY);
         MOZ_ASSERT(ownership == JS::SCTAG_TMO_UNFILLED);
 #endif
 
         ESClass cls;
-        if (!GetBuiltinClass(context(), obj, &cls))
+        if (!GetBuiltinClass(cx, obj, &cls))
             return false;
 
         if (cls == ESClass::ArrayBuffer) {
+            tag = SCTAG_TRANSFER_MAP_ARRAY_BUFFER;
+
             // The current setup of the array buffer inheritance hierarchy doesn't
             // lend itself well to generic manipulation via proxies.
-            Rooted<ArrayBufferObject*> arrayBuffer(context(), &CheckedUnwrap(obj)->as<ArrayBufferObject>());
-            JSAutoCompartment ac(context(), arrayBuffer);
+            Rooted<ArrayBufferObject*> arrayBuffer(cx, &CheckedUnwrap(obj)->as<ArrayBufferObject>());
+            JSAutoCompartment ac(cx, arrayBuffer);
             size_t nbytes = arrayBuffer->byteLength();
 
             if (arrayBuffer->isWasm() || arrayBuffer->isPreparedForAsmJS()) {
-                JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr,
-                                          JSMSG_WASM_NO_TRANSFER);
+                JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_WASM_NO_TRANSFER);
                 return false;
             }
 
-            bool hasStealableContents = arrayBuffer->hasStealableContents() &&
-                                        (scope != JS::StructuredCloneScope::DifferentProcess);
-
-            ArrayBufferObject::BufferContents bufContents =
-                ArrayBufferObject::stealContents(context(), arrayBuffer, hasStealableContents);
-            if (!bufContents)
-                return false; // already transferred data
-
-            content = bufContents.data();
-            tag = SCTAG_TRANSFER_MAP_ARRAY_BUFFER;
-            if (bufContents.kind() == ArrayBufferObject::MAPPED)
-                ownership = JS::SCTAG_TMO_MAPPED_DATA;
-            else
-                ownership = JS::SCTAG_TMO_ALLOC_DATA;
-            extraData = nbytes;
+            if (scope == JS::StructuredCloneScope::DifferentProcess) {
+                // Write Transferred ArrayBuffers in DifferentProcess scope at
+                // the end of the clone buffer, and store the offset within the
+                // buffer to where the ArrayBuffer was written. Note that this
+                // will invalidate the current position iterator.
+
+                size_t pointOffset = out.offset(point);
+                tag = SCTAG_TRANSFER_MAP_STORED_ARRAY_BUFFER;
+                ownership = JS::SCTAG_TMO_UNOWNED;
+                content = nullptr;
+                extraData = out.tell() - pointOffset; // Offset from tag to current end of buffer
+                if (!writeArrayBuffer(arrayBuffer))
+                    return false;
+
+                // Must refresh the point iterator after its collection has
+                // been modified.
+                point = out.iter();
+                point += pointOffset;
+
+                if (!JS_DetachArrayBuffer(cx, arrayBuffer))
+                    return false;
+            } else {
+                bool hasStealableContents = arrayBuffer->hasStealableContents();
+
+                ArrayBufferObject::BufferContents bufContents =
+                    ArrayBufferObject::stealContents(cx, arrayBuffer, hasStealableContents);
+                if (!bufContents)
+                    return false; // already transferred data
+
+                content = bufContents.data();
+                if (bufContents.kind() == ArrayBufferObject::MAPPED)
+                    ownership = JS::SCTAG_TMO_MAPPED_DATA;
+                else
+                    ownership = JS::SCTAG_TMO_ALLOC_DATA;
+                extraData = nbytes;
+            }
         } else {
             if (!callbacks || !callbacks->writeTransfer)
                 return reportDataCloneError(JS_SCERR_TRANSFERABLE);
-            if (!callbacks->writeTransfer(context(), obj, closure, &tag, &ownership, &content, &extraData))
+            if (!callbacks->writeTransfer(cx, obj, closure, &tag, &ownership, &content, &extraData))
                 return false;
             MOZ_ASSERT(tag > SCTAG_TRANSFER_MAP_PENDING_ENTRY);
         }
 
         point.write(NativeEndian::swapToLittleEndian(PairToUInt64(tag, ownership)));
         point.next();
         point.write(NativeEndian::swapToLittleEndian(reinterpret_cast<uint64_t>(content)));
         point.next();
@@ -2095,17 +2156,18 @@ JSStructuredCloneReader::readHeader()
 
     if (tag != SCTAG_HEADER) {
         // Old structured clone buffer. We must have read it from disk or
         // somewhere, so we can assume it's scope-compatible.
         return true;
     }
 
     MOZ_ALWAYS_TRUE(in.readPair(&tag, &data));
-    if (data < uint32_t(scope)) {
+    storedScope = JS::StructuredCloneScope(data);
+    if (storedScope < allowedScope) {
         JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA,
                                   "incompatible structured clone scope");
         return false;
     }
 
     return true;
 }
 
@@ -2140,23 +2202,45 @@ JSStructuredCloneReader::readTransferMap
         if (!in.readPtr(&content))
             return false;
 
         uint64_t extraData;
         if (!in.read(&extraData))
             return false;
 
         if (tag == SCTAG_TRANSFER_MAP_ARRAY_BUFFER) {
+            if (storedScope == JS::StructuredCloneScope::DifferentProcess) {
+                // Transferred ArrayBuffers in a DifferentProcess clone buffer
+                // are treated as if they weren't Transferred at all.
+                continue;
+            }
+
             size_t nbytes = extraData;
             MOZ_ASSERT(data == JS::SCTAG_TMO_ALLOC_DATA ||
                        data == JS::SCTAG_TMO_MAPPED_DATA);
             if (data == JS::SCTAG_TMO_ALLOC_DATA)
                 obj = JS_NewArrayBufferWithContents(cx, nbytes, content);
             else if (data == JS::SCTAG_TMO_MAPPED_DATA)
                 obj = JS_NewMappedArrayBufferWithContents(cx, nbytes, content);
+        } else if (tag == SCTAG_TRANSFER_MAP_STORED_ARRAY_BUFFER) {
+            auto savedPos = in.tell();
+            auto guard = mozilla::MakeScopeExit([&] {
+                in.seekTo(savedPos);
+            });
+            in.seekTo(pos);
+            in.seekBy(static_cast<size_t>(extraData));
+
+            uint32_t tag, data;
+            if (!in.readPair(&tag, &data))
+                return false;
+            MOZ_ASSERT(tag == SCTAG_ARRAY_BUFFER_OBJECT);
+            RootedValue val(cx);
+            if (!readArrayBuffer(data, &val))
+                return false;
+            obj = &val.toObject();
         } else {
             if (!callbacks || !callbacks->readTransfer) {
                 ReportDataCloneError(cx, callbacks, JS_SCERR_TRANSFERABLE);
                 return false;
             }
             if (!callbacks->readTransfer(cx, this, tag, content, extraData, closure, &obj))
                 return false;
             MOZ_ASSERT(obj);
--- a/mfbt/BufferList.h
+++ b/mfbt/BufferList.h
@@ -219,16 +219,43 @@ class BufferList : private AllocPolicy
       return true;
     }
 
     // Returns true when the iterator reaches the end of the BufferList.
     bool Done() const
     {
       return mData == mDataEnd;
     }
+
+   private:
+
+    // Count the bytes we would need to advance in order to reach aTarget.
+    size_t BytesUntil(const BufferList& aBuffers, const IterImpl& aTarget) const {
+      size_t offset = 0;
+
+      MOZ_ASSERT(aTarget.IsIn(aBuffers));
+
+      char* data = mData;
+      for (uintptr_t segment = mSegment; segment < aTarget.mSegment; segment++) {
+        offset += aBuffers.mSegments[segment].End() - data;
+        data = aBuffers.mSegments[segment].mData;
+      }
+
+      MOZ_RELEASE_ASSERT(IsIn(aBuffers));
+      MOZ_RELEASE_ASSERT(aTarget.mData >= data);
+
+      offset += aTarget.mData - data;
+      return offset;
+    }
+
+    bool IsIn(const BufferList& aBuffers) const {
+      return mSegment < aBuffers.mSegments.length() &&
+             mData >= aBuffers.mSegments[mSegment].mData &&
+             mData < aBuffers.mSegments[mSegment].End();
+    }
   };
 
   // Special convenience method that returns Iter().Data().
   char* Start() { return mSegments[0].mData; }
   const char* Start() const { return mSegments[0].mData; }
 
   IterImpl Iter() const { return IterImpl(*this); }
 
@@ -265,16 +292,23 @@ class BufferList : private AllocPolicy
   // Contents of the buffer before aIter + aSize is left undefined.
   // Extract can fail, in which case *aSuccess will be false upon return. The
   // moved buffers are erased from the original BufferList. In case of extract
   // fails, the original BufferList is intact.  All other iterators except aIter
   // are invalidated.
   // This method requires aIter and aSize to be 8-byte aligned.
   BufferList Extract(IterImpl& aIter, size_t aSize, bool* aSuccess);
 
+  // Return the number of bytes from 'start' to 'end', two iterators within
+  // this BufferList.
+  size_t RangeLength(const IterImpl& start, const IterImpl& end) const {
+    MOZ_ASSERT(start.IsIn(*this) && end.IsIn(*this));
+    return start.BytesUntil(*this, end);
+  }
+
 private:
   explicit BufferList(AllocPolicy aAP)
    : AllocPolicy(aAP),
      mOwning(false),
      mSize(0),
      mStandardCapacity(0)
   {
   }