Bug 1264053 - Transfer DifferentProcess ArrayBuffers by copying, r=jorendorff a=jcristau
authorSteve Fink <sfink@mozilla.com>
Thu, 19 Jan 2017 14:02:40 -0800
changeset 358777 4aa072b1110c8c2f2ccdd387d270d5115f488853
parent 358776 c10934134a854d324262f48190982071b073c69c
child 358778 26f98ab7307248364557292295979f1452199ba3
push id10655
push usercbook@mozilla.com
push dateTue, 31 Jan 2017 15:17:41 +0000
treeherdermozilla-aurora@927ab26019d8 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjorendorff, jcristau
bugs1264053
milestone53.0a2
Bug 1264053 - Transfer DifferentProcess ArrayBuffers by copying, r=jorendorff a=jcristau
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
@@ -2311,22 +2311,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);
@@ -2345,42 +2364,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?)");
@@ -2389,18 +2445,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;
@@ -4410,25 +4467,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
@@ -119,16 +119,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)>
@@ -160,31 +161,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();
@@ -205,16 +224,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);
@@ -223,21 +244,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;
 
@@ -256,17 +282,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;
@@ -284,17 +312,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(); }
 
@@ -312,17 +340,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.
@@ -1432,17 +1471,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))
@@ -1468,67 +1506,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();
@@ -2094,17 +2155,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;
 }
 
@@ -2139,23 +2201,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)
   {
   }