Bug 861925 - Steal and neuter ArrayBuffers at end of structured clone, r=jorendorff
☠☠ backed out by d0fa5c45cabf ☠ ☠
authorSteve Fink <sfink@mozilla.com>
Tue, 15 Oct 2013 17:26:19 -0700
changeset 150850 8febf2f0e35dcc341b8acea6ae882a338144cc72
parent 150849 e646195f32aea58681eb93eef3fe4dcc99579354
child 150851 e7d649603075c8142770527b383bd2ede5541491
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersjorendorff
bugs861925
milestone27.0a1
Bug 861925 - Steal and neuter ArrayBuffers at end of structured clone, r=jorendorff
js/public/StructuredClone.h
js/src/js.msg
js/src/jsapi.h
js/src/tests/js1_8_5/extensions/clone-transferables.js
js/src/tests/js1_8_5/extensions/shell.js
js/src/vm/StructuredClone.cpp
--- a/js/public/StructuredClone.h
+++ b/js/public/StructuredClone.h
@@ -43,17 +43,19 @@ typedef JSObject *(*ReadStructuredCloneO
 typedef bool (*WriteStructuredCloneOp)(JSContext *cx, JSStructuredCloneWriter *w,
                                          JS::Handle<JSObject*> obj, void *closure);
 
 // This is called when JS_WriteStructuredClone is given an invalid transferable.
 // To follow HTML5, the application must throw a DATA_CLONE_ERR DOMException
 // with error set to one of the JS_SCERR_* values.
 typedef void (*StructuredCloneErrorOp)(JSContext *cx, uint32_t errorid);
 
-// The maximum supported structured-clone serialization format version.
+// The maximum supported structured-clone serialization format version. Note
+// that this does not need to be bumped for Transferable-only changes, since
+// they are never saved to persistent storage.
 #define JS_STRUCTURED_CLONE_VERSION 2
 
 struct JSStructuredCloneCallbacks {
     ReadStructuredCloneOp read;
     WriteStructuredCloneOp write;
     StructuredCloneErrorOp reportError;
 };
 
--- a/js/src/js.msg
+++ b/js/src/js.msg
@@ -229,17 +229,17 @@ MSG_DEF(JSMSG_NESTING_GENERATOR,      17
 MSG_DEF(JSMSG_UNUSED176,              176, 0, JSEXN_NONE, "")
 MSG_DEF(JSMSG_UNUSED177,              177, 0, JSEXN_NONE, "")
 MSG_DEF(JSMSG_UNUSED178,              178, 0, JSEXN_NONE, "")
 MSG_DEF(JSMSG_UNUSED179,              179, 0, JSEXN_NONE, "")
 MSG_DEF(JSMSG_UNUSED180,              180, 0, JSEXN_NONE, "")
 MSG_DEF(JSMSG_UNUSED181,              181, 0, JSEXN_NONE, "")
 MSG_DEF(JSMSG_BAD_GENERATOR_SEND,     182, 1, JSEXN_TYPEERR, "attempt to send {0} to newborn generator")
 MSG_DEF(JSMSG_SC_NOT_TRANSFERABLE,    183, 0, JSEXN_TYPEERR, "invalid transferable array for structured clone")
-MSG_DEF(JSMSG_UNUSED184,              184, 0, JSEXN_NONE, "")
+MSG_DEF(JSMSG_SC_DUP_TRANSFERABLE,    184, 0, JSEXN_TYPEERR, "duplicate transferable for structured clone")
 MSG_DEF(JSMSG_CANT_REPORT_AS_NON_EXTENSIBLE, 185, 0, JSEXN_TYPEERR, "proxy can't report an extensible object as non-extensible")
 MSG_DEF(JSMSG_UNUSED186,              186, 0, JSEXN_NONE, "")
 MSG_DEF(JSMSG_UNUSED187,              187, 0, JSEXN_NONE, "")
 MSG_DEF(JSMSG_INCOMPATIBLE_METHOD,    188, 3, JSEXN_TYPEERR, "{0} {1} called on incompatible {2}")
 MSG_DEF(JSMSG_UNUSED189,              189, 0, JSEXN_NONE, "")
 MSG_DEF(JSMSG_UNUSED190,              190, 0, JSEXN_NONE, "")
 MSG_DEF(JSMSG_UNUSED191,              191, 0, JSEXN_NONE, "")
 MSG_DEF(JSMSG_UNUSED192,              192, 0, JSEXN_NONE, "")
--- a/js/src/jsapi.h
+++ b/js/src/jsapi.h
@@ -219,32 +219,39 @@ class AutoArrayRooter : private AutoGCRo
     MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER
 
     js::SkipRoot skip;
 };
 
 template<class T>
 class AutoVectorRooter : protected AutoGCRooter
 {
+    typedef js::Vector<T, 8> VectorImpl;
+    VectorImpl vector;
+
+    /* Prevent overwriting of inline elements in vector. */
+    js::SkipRoot vectorRoot;
+
   public:
     explicit AutoVectorRooter(JSContext *cx, ptrdiff_t tag
                               MOZ_GUARD_OBJECT_NOTIFIER_PARAM)
       : AutoGCRooter(cx, tag), vector(cx), vectorRoot(cx, &vector)
     {
         MOZ_GUARD_OBJECT_NOTIFIER_INIT;
     }
 
     explicit AutoVectorRooter(js::ContextFriendFields *cx, ptrdiff_t tag
                               MOZ_GUARD_OBJECT_NOTIFIER_PARAM)
       : AutoGCRooter(cx, tag), vector(cx), vectorRoot(cx, &vector)
     {
         MOZ_GUARD_OBJECT_NOTIFIER_INIT;
     }
 
     typedef T ElementType;
+    typedef typename VectorImpl::Range Range;
 
     size_t length() const { return vector.length(); }
     bool empty() const { return vector.empty(); }
 
     bool append(const T &v) { return vector.append(v); }
     bool appendAll(const AutoVectorRooter<T> &other) {
         return vector.appendAll(other.vector);
     }
@@ -294,33 +301,29 @@ class AutoVectorRooter : protected AutoG
     }
 
     const T *begin() const { return vector.begin(); }
     T *begin() { return vector.begin(); }
 
     const T *end() const { return vector.end(); }
     T *end() { return vector.end(); }
 
+    Range all() { return vector.all(); }
+
     const T &back() const { return vector.back(); }
 
     friend void AutoGCRooter::trace(JSTracer *trc);
 
   private:
     void makeRangeGCSafe(size_t oldLength) {
         T *t = vector.begin() + oldLength;
         for (size_t i = oldLength; i < vector.length(); ++i, ++t)
             memset(t, 0, sizeof(T));
     }
 
-    typedef js::Vector<T, 8> VectorImpl;
-    VectorImpl vector;
-
-    /* Prevent overwriting of inline elements in vector. */
-    js::SkipRoot vectorRoot;
-
     MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER
 };
 
 template<class Key, class Value>
 class AutoHashMapRooter : protected AutoGCRooter
 {
   private:
     typedef js::HashMap<Key, Value> HashMapImpl;
@@ -330,16 +333,17 @@ class AutoHashMapRooter : protected Auto
                                MOZ_GUARD_OBJECT_NOTIFIER_PARAM)
       : AutoGCRooter(cx, tag), map(cx)
     {
         MOZ_GUARD_OBJECT_NOTIFIER_INIT;
     }
 
     typedef Key KeyType;
     typedef Value ValueType;
+    typedef typename HashMapImpl::Entry Entry;
     typedef typename HashMapImpl::Lookup Lookup;
     typedef typename HashMapImpl::Ptr Ptr;
     typedef typename HashMapImpl::AddPtr AddPtr;
 
     bool init(uint32_t len = 16) {
         return map.init(len);
     }
     bool initialized() const {
new file mode 100644
--- /dev/null
+++ b/js/src/tests/js1_8_5/extensions/clone-transferables.js
@@ -0,0 +1,87 @@
+// |reftest| skip-if(!xulRuntime.shell)
+// Any copyright is dedicated to the Public Domain.
+// http://creativecommons.org/licenses/publicdomain/
+
+function test() {
+    // Note: -8 and -200 will trigger asm.js link failures because 8 and 200
+    // bytes are below the minimum allowed size, and the buffer will not
+    // actually be converted to an asm.js buffer.
+    for (var size of [0, 8, 16, 200, 1000, 4096, -8, -200, -8192, -65536]) {
+        var buffer_ctor = (size < 0) ? AsmJSArrayBuffer : ArrayBuffer;
+        size = Math.abs(size);
+
+        var old = buffer_ctor(size);
+        var copy = deserialize(serialize(old, [old]));
+        assertEq(old.byteLength, 0);
+        assertEq(copy.byteLength, size);
+
+        var constructors = [ Int8Array,
+                             Uint8Array,
+                             Int16Array,
+                             Uint16Array,
+                             Int32Array,
+                             Uint32Array,
+                             Float32Array,
+                             Float64Array,
+                             Uint8ClampedArray ];
+
+        for (var ctor of constructors) {
+            var buf = buffer_ctor(size);
+            var old_arr = ctor(buf);
+            assertEq(buf.byteLength, size);
+            assertEq(buf, old_arr.buffer);
+            assertEq(old_arr.length, size / old_arr.BYTES_PER_ELEMENT);
+
+            var copy_arr = deserialize(serialize(old_arr, [ buf ]));
+            assertEq(buf.byteLength, 0, "donor array buffer should be neutered");
+            assertEq(old_arr.length, 0, "donor typed array should be neutered");
+            assertEq(copy_arr.buffer.byteLength == size, true);
+            assertEq(copy_arr.length, size / old_arr.BYTES_PER_ELEMENT);
+
+            buf = null;
+            old_arr = null;
+            gc(); // Tickle the ArrayBuffer -> view management
+        }
+
+        for (var ctor of constructors) {
+            var buf = buffer_ctor(size);
+            var old_arr = ctor(buf);
+            var dv = DataView(buf); // Second view
+            var copy_arr = deserialize(serialize(old_arr, [ buf ]));
+            assertEq(buf.byteLength, 0, "donor array buffer should be neutered");
+            assertEq(old_arr.length, 0, "donor typed array should be neutered");
+            assertEq(dv.byteLength, 0, "all views of donor array buffer should be neutered");
+
+            buf = null;
+            old_arr = null;
+            gc(); // Tickle the ArrayBuffer -> view management
+        }
+
+        // Mutate the buffer during the clone operation. The modifications should be visible.
+        if (size >= 4) {
+            old = buffer_ctor(size);
+            var view = Int32Array(old);
+            view[0] = 1;
+            var mutator = { get foo() { view[0] = 2; } };
+            var copy = deserialize(serialize([ old, mutator ], [old]));
+            var viewCopy = Int32Array(copy[0]);
+            assertEq(view.length, 0); // Neutered
+            assertEq(viewCopy[0], 2);
+        }
+
+        // Neuter the buffer during the clone operation. Should throw an exception.
+        if (size >= 4) {
+            old = buffer_ctor(size);
+            var mutator = {
+                get foo() {
+                    deserialize(serialize(old, [old]));
+                }
+            };
+            // The throw is not yet implemented, bug 919259.
+            //var copy = deserialize(serialize([ old, mutator ], [old]));
+        }
+    }
+}
+
+test();
+reportCompare(0, 0, 'ok');
--- a/js/src/tests/js1_8_5/extensions/shell.js
+++ b/js/src/tests/js1_8_5/extensions/shell.js
@@ -192,8 +192,21 @@ function referencesVia(from, edge, to) {
         print("referent is not referenced via: " + uneval(edge));
         print("but it is referenced via:       " + uneval(alternatives));
     }
     print("all incoming edges, from any object:");
     for (var e in edges)
         print(e);
     return false;
 }
+
+// Note that AsmJS ArrayBuffers have a minimum size, currently 4096 bytes. If a
+// smaller size is given, a regular ArrayBuffer will be returned instead.
+function AsmJSArrayBuffer(size) {
+    var ab = new ArrayBuffer(size);
+    (new Function('global', 'foreign', 'buffer', '' +
+'        "use asm";' +
+'        var i32 = new global.Int32Array(buffer);' +
+'        function g() {};' +
+'        return g;' +
+''))(this,null,ab);
+    return ab;
+}
--- a/js/src/vm/StructuredClone.cpp
+++ b/js/src/vm/StructuredClone.cpp
@@ -63,59 +63,86 @@ enum StructuredDataType {
     SCTAG_REGEXP_OBJECT,
     SCTAG_ARRAY_OBJECT,
     SCTAG_OBJECT_OBJECT,
     SCTAG_ARRAY_BUFFER_OBJECT,
     SCTAG_BOOLEAN_OBJECT,
     SCTAG_STRING_OBJECT,
     SCTAG_NUMBER_OBJECT,
     SCTAG_BACK_REFERENCE_OBJECT,
-    SCTAG_TRANSFER_MAP_HEADER,
-    SCTAG_TRANSFER_MAP,
+    SCTAG_DO_NOT_USE_1,
+    SCTAG_DO_NOT_USE_2,
     SCTAG_TYPED_ARRAY_OBJECT,
     SCTAG_TYPED_ARRAY_V1_MIN = 0xFFFF0100,
     SCTAG_TYPED_ARRAY_V1_INT8 = SCTAG_TYPED_ARRAY_V1_MIN + ScalarTypeRepresentation::TYPE_INT8,
     SCTAG_TYPED_ARRAY_V1_UINT8 = SCTAG_TYPED_ARRAY_V1_MIN + ScalarTypeRepresentation::TYPE_UINT8,
     SCTAG_TYPED_ARRAY_V1_INT16 = SCTAG_TYPED_ARRAY_V1_MIN + ScalarTypeRepresentation::TYPE_INT16,
     SCTAG_TYPED_ARRAY_V1_UINT16 = SCTAG_TYPED_ARRAY_V1_MIN + ScalarTypeRepresentation::TYPE_UINT16,
     SCTAG_TYPED_ARRAY_V1_INT32 = SCTAG_TYPED_ARRAY_V1_MIN + ScalarTypeRepresentation::TYPE_INT32,
     SCTAG_TYPED_ARRAY_V1_UINT32 = SCTAG_TYPED_ARRAY_V1_MIN + ScalarTypeRepresentation::TYPE_UINT32,
     SCTAG_TYPED_ARRAY_V1_FLOAT32 = SCTAG_TYPED_ARRAY_V1_MIN + ScalarTypeRepresentation::TYPE_FLOAT32,
     SCTAG_TYPED_ARRAY_V1_FLOAT64 = SCTAG_TYPED_ARRAY_V1_MIN + ScalarTypeRepresentation::TYPE_FLOAT64,
     SCTAG_TYPED_ARRAY_V1_UINT8_CLAMPED = SCTAG_TYPED_ARRAY_V1_MIN + ScalarTypeRepresentation::TYPE_UINT8_CLAMPED,
     SCTAG_TYPED_ARRAY_V1_MAX = SCTAG_TYPED_ARRAY_V1_MIN + ScalarTypeRepresentation::TYPE_MAX - 1,
+
+    /*
+     * 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_ENTRY,
+
     SCTAG_END_OF_BUILTIN_TYPES
 };
 
+// Data associated with an SCTAG_TRANSFER_MAP_HEADER that tells whether the
+// contents have been read out yet or not.
 enum TransferableMapHeader {
-    SCTAG_TM_NOT_MARKED = 0,
-    SCTAG_TM_MARKED
+    SCTAG_TM_UNREAD = 0,
+    SCTAG_TM_TRANSFERRED
+};
+
+enum TransferableObjectType {
+    // Transferable data has not been filled in yet
+    SCTAG_TM_UNFILLED = 0,
+
+    // Structured clone buffer does not yet own the data
+    SCTAG_TM_UNOWNED = 1,
+
+    // All values at least this large are owned by the clone buffer
+    SCTAG_TM_FIRST_OWNED = 2,
+
+    // Data is a pointer that can be freed
+    SCTAG_TM_ALLOC_DATA = 2,
 };
 
 namespace js {
 
 struct SCOutput {
   public:
     explicit SCOutput(JSContext *cx);
+    ~SCOutput();
 
     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);
     bool writeChars(const jschar *p, size_t nchars);
     bool writePtr(const void *);
 
     template <class T>
     bool writeArray(const T *p, size_t nbytes);
 
     bool extractBuffer(uint64_t **datap, size_t *sizep);
 
-    uint64_t count() { return buf.length(); }
+    uint64_t count() const { return buf.length(); }
+    uint64_t *rawBuffer() { return buf.begin(); }
 
   private:
     JSContext *cx;
     js::Vector<uint64_t> buf;
 };
 
 struct SCInput {
   public:
@@ -131,16 +158,22 @@ struct SCInput {
     bool readPtr(void **);
 
     bool get(uint64_t *p);
     bool getPair(uint32_t *tagp, uint32_t *datap);
 
     bool replace(uint64_t u);
     bool replacePair(uint32_t tag, uint32_t data);
 
+    uint64_t *tell() const { return point; }
+    void seek(uint64_t *pos) {
+        JS_ASSERT(pos <= end);
+        point = pos;
+    }
+
     template <class T>
     bool readArray(T *p, size_t nelems);
 
   private:
     bool eof();
 
     void staticAssertions() {
         JS_STATIC_ASSERT(sizeof(jschar) == 2);
@@ -201,18 +234,17 @@ struct JSStructuredCloneWriter {
                                      const JSStructuredCloneCallbacks *cb,
                                      void *cbClosure,
                                      jsval tVal)
         : out(out), objs(out.context()),
           counts(out.context()), ids(out.context()),
           memory(out.context()), callbacks(cb), closure(cbClosure),
           transferable(out.context(), tVal), transferableObjects(out.context()) { }
 
-    bool init() { return transferableObjects.init() && parseTransferable() &&
-                         memory.init() && writeTransferMap(); }
+    bool init() { return parseTransferable() && memory.init() && writeTransferMap(); }
 
     bool write(const js::Value &v);
 
     js::SCOutput &output() { return out; }
 
   private:
     JSContext *context() { return out.context(); }
 
@@ -223,16 +255,17 @@ struct JSStructuredCloneWriter {
     bool writeArrayBuffer(JS::HandleObject obj);
     bool writeTypedArray(JS::HandleObject obj);
     bool startObject(JS::HandleObject obj, bool *backref);
     bool startWrite(const js::Value &v);
     bool traverseObject(JS::HandleObject obj);
 
     bool parseTransferable();
     void reportErrorTransferable();
+    bool transferOwnership();
 
     inline void checkStack();
 
     js::SCOutput &out;
 
     // Vector of objects with properties remaining to be written.
     //
     // NB: These can span multiple compartments, so the compartment must be
@@ -255,17 +288,17 @@ struct JSStructuredCloneWriter {
     // The user defined callbacks that will be used for cloning.
     const JSStructuredCloneCallbacks *callbacks;
 
     // Any value passed to JS_WriteStructuredClone.
     void *closure;
 
     // List of transferable objects
     JS::RootedValue transferable;
-    js::AutoObjectHashSet transferableObjects;
+    JS::AutoObjectVector transferableObjects;
 
     friend bool JS_WriteTypedArray(JSStructuredCloneWriter *w, JS::Value v);
 };
 
 JS_FRIEND_API(uint64_t)
 js_GetSCOffset(JSStructuredCloneWriter* writer)
 {
     JS_ASSERT(writer);
@@ -292,64 +325,75 @@ bool
 ReadStructuredClone(JSContext *cx, uint64_t *data, size_t nbytes, Value *vp,
                     const JSStructuredCloneCallbacks *cb, void *cbClosure)
 {
     SCInput in(cx, data, nbytes);
     JSStructuredCloneReader r(in, cb, cbClosure);
     return r.read(vp);
 }
 
-bool
-ClearStructuredClone(const uint64_t *data, size_t nbytes)
+// This may acquire new ways of discarding transfer map entries as new
+// Transferables are implemented.
+static void
+DiscardEntry(uint32_t mapEntryDescriptor, const uint64_t *ptr)
 {
-    const uint64_t *point = data;
-    const uint64_t *end = data + nbytes / 8;
+    JS_ASSERT(mapEntryDescriptor == SCTAG_TM_ALLOC_DATA);
+    uint64_t u = LittleEndian::readUint64(ptr);
+    js_free(reinterpret_cast<void*>(u));
+}
+
+static void
+Discard(const uint64_t *begin, const uint64_t *end)
+{
+    const uint64_t *point = begin;
 
     uint64_t u = LittleEndian::readUint64(point++);
     uint32_t tag = uint32_t(u >> 32);
-    if (tag == SCTAG_TRANSFER_MAP_HEADER) {
-        if ((TransferableMapHeader)uint32_t(u) == SCTAG_TM_NOT_MARKED) {
-            while (point != end) {
-                uint64_t u = LittleEndian::readUint64(point++);
-                uint32_t tag = uint32_t(u >> 32);
-                if (tag == SCTAG_TRANSFER_MAP) {
-                    u = LittleEndian::readUint64(point++);
-                    js_free(reinterpret_cast<void*>(u));
-                } else {
-                    // The only things in the transfer map should be
-                    // SCTAG_TRANSFER_MAP tags paired with pointers. If we find
-                    // any other tag, we've walked off the end of the transfer
-                    // map.
-                    break;
-                }
-            }
+    if (tag != SCTAG_TRANSFER_MAP_HEADER)
+        return;
+
+    if (TransferableMapHeader(u) == SCTAG_TM_TRANSFERRED)
+        return;
+
+    uint64_t numTransferables = LittleEndian::readUint64(point++);
+    while (numTransferables--) {
+        uint64_t u = LittleEndian::readUint64(point++);
+        JS_ASSERT(uint32_t(u >> 32) == SCTAG_TRANSFER_MAP_ENTRY);
+        uint32_t mapEntryDescriptor = uint32_t(u);
+        if (mapEntryDescriptor >= SCTAG_TM_FIRST_OWNED) {
+            DiscardEntry(mapEntryDescriptor, point);
+            point += 2; // Pointer and userdata
         }
     }
+}
 
-    js_free((void *)data);
-    return true;
+static void
+ClearStructuredClone(const uint64_t *data, size_t nbytes)
+{
+    JS_ASSERT(nbytes % 8 == 0);
+    Discard(data, data + nbytes / 8);
+    js_free(const_cast<uint64_t*>(data));
 }
 
 bool
 StructuredCloneHasTransferObjects(const uint64_t *data, size_t nbytes, bool *hasTransferable)
 {
     *hasTransferable = false;
 
     if (data) {
         uint64_t u = LittleEndian::readUint64(data);
         uint32_t tag = uint32_t(u >> 32);
-        if (tag == SCTAG_TRANSFER_MAP_HEADER) {
+        if (tag == SCTAG_TRANSFER_MAP_HEADER)
             *hasTransferable = true;
-        }
     }
 
     return true;
 }
 
-}
+} /* namespace js */
 
 static inline uint64_t
 PairToUInt64(uint32_t tag, uint32_t data)
 {
     return uint64_t(data) | (uint64_t(tag) << 32);
 }
 
 bool
@@ -504,16 +548,22 @@ SCInput::readPtr(void **p)
     uint64_t tmp;
     bool ret = read(&tmp);
     *p = reinterpret_cast<void*>(tmp);
     return ret;
 }
 
 SCOutput::SCOutput(JSContext *cx) : cx(cx), buf(cx) {}
 
+SCOutput::~SCOutput()
+{
+    // Free any transferable data left lying around in the buffer
+    Discard(rawBuffer(), rawBuffer() + count());
+}
+
 bool
 SCOutput::write(uint64_t u)
 {
     return buf.append(NativeEndian::swapToLittleEndian(u));
 }
 
 bool
 SCOutput::writePair(uint32_t tag, uint32_t data)
@@ -631,17 +681,17 @@ SCOutput::extractBuffer(uint64_t **datap
     return (*datap = buf.extractRawBuffer()) != nullptr;
 }
 
 JS_STATIC_ASSERT(JSString::MAX_LENGTH < UINT32_MAX);
 
 bool
 JSStructuredCloneWriter::parseTransferable()
 {
-    transferableObjects.clear();
+    MOZ_ASSERT(transferableObjects.empty(), "parseTransferable called with stale data");
 
     if (JSVAL_IS_NULL(transferable) || JSVAL_IS_VOID(transferable))
         return true;
 
     if (!transferable.isObject()) {
         reportErrorTransferable();
         return false;
     }
@@ -667,31 +717,31 @@ JSStructuredCloneWriter::parseTransferab
 
         if (!v.isObject()) {
             reportErrorTransferable();
             return false;
         }
 
         JSObject* tObj = CheckedUnwrap(&v.toObject());
         if (!tObj) {
-            JS_ReportError(context(), "Permission denied to access object");
+            JS_ReportErrorNumber(context(), js_GetErrorMessage, NULL, JSMSG_UNWRAP_DENIED);
             return false;
         }
         if (!tObj->is<ArrayBufferObject>()) {
             reportErrorTransferable();
             return false;
         }
 
-        // No duplicate:
-        if (transferableObjects.has(tObj)) {
-            reportErrorTransferable();
+        // No duplicates allowed
+        if (std::find(transferableObjects.begin(), transferableObjects.end(), tObj) != transferableObjects.end()) {
+            JS_ReportErrorNumber(context(), js_GetErrorMessage, NULL, JSMSG_SC_DUP_TRANSFERABLE);
             return false;
         }
 
-        if (!transferableObjects.putNew(tObj))
+        if (!transferableObjects.append(tObj))
             return false;
     }
 
     return true;
 }
 
 void
 JSStructuredCloneWriter::reportErrorTransferable()
@@ -899,35 +949,77 @@ JSStructuredCloneWriter::startWrite(cons
 }
 
 bool
 JSStructuredCloneWriter::writeTransferMap()
 {
     if (transferableObjects.empty())
         return true;
 
-    if (!out.writePair(SCTAG_TRANSFER_MAP_HEADER, (uint32_t)SCTAG_TM_NOT_MARKED))
+    if (!out.writePair(SCTAG_TRANSFER_MAP_HEADER, (uint32_t)SCTAG_TM_UNREAD))
         return false;
 
-    for (HashSet<JSObject*>::Range r = transferableObjects.all();
-         !r.empty(); r.popFront()) {
-        JSObject *obj = r.front();
+    if (!out.write(transferableObjects.length()))
+        return false;
+
+    for (JS::AutoObjectVector::Range tr = transferableObjects.all();
+         !tr.empty(); tr.popFront())
+    {
+        JSObject *obj = tr.front();
 
         if (!memory.put(obj, memory.count()))
             return false;
 
+        // Emit a placeholder pointer. We will steal the data and neuter the
+        // transferable later.
+        if (!out.writePair(SCTAG_TRANSFER_MAP_ENTRY, SCTAG_TM_UNFILLED) ||
+            !out.writePtr(NULL) ||
+            !out.write(0))
+        {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+bool
+JSStructuredCloneWriter::transferOwnership()
+{
+    if (transferableObjects.empty())
+        return true;
+
+    // Walk along the transferables and the transfer map at the same time,
+    // grabbing out pointers from the transferables and stuffing them into the
+    // transfer map.
+    uint64_t *point = out.rawBuffer();
+    JS_ASSERT(uint32_t(LittleEndian::readUint64(point) >> 32) == SCTAG_TRANSFER_MAP_HEADER);
+    point++;
+    JS_ASSERT(LittleEndian::readUint64(point) == transferableObjects.length());
+    point++;
+
+    for (JS::AutoObjectVector::Range tr = transferableObjects.all();
+         !tr.empty();
+         tr.popFront())
+    {
         void *content;
         uint8_t *data;
-        if (!JS_StealArrayBufferContents(context(), obj, &content, &data))
-            return false;
+        if (!JS_StealArrayBufferContents(context(), tr.front(), &content, &data))
+            return false; // Destructor will clean up the already-transferred data
 
-        if (!out.writePair(SCTAG_TRANSFER_MAP, 0) || !out.writePtr(content))
-            return false;
+        MOZ_ASSERT(uint32_t(LittleEndian::readUint64(point) >> 32) == SCTAG_TRANSFER_MAP_ENTRY);
+        LittleEndian::writeUint64(point++, PairToUInt64(SCTAG_TRANSFER_MAP_ENTRY, SCTAG_TM_ALLOC_DATA));
+        LittleEndian::writeUint64(point++, reinterpret_cast<uint64_t>(content));
+        LittleEndian::writeUint64(point++, 0);
     }
 
+    JS_ASSERT(point <= out.rawBuffer() + out.count());
+    JS_ASSERT_IF(point < out.rawBuffer() + out.count(),
+                 uint32_t(LittleEndian::readUint64(point) >> 32) != SCTAG_TRANSFER_MAP_ENTRY);
+
     return true;
 }
 
 bool
 JSStructuredCloneWriter::write(const Value &v)
 {
     if (!startWrite(v))
         return false;
@@ -964,18 +1056,17 @@ JSStructuredCloneWriter::write(const Val
         } else {
             out.writePair(SCTAG_NULL, 0);
             objs.popBack();
             counts.popBack();
         }
     }
 
     memory.clear();
-
-    return true;
+    return transferOwnership();
 }
 
 bool
 JSStructuredCloneReader::checkDouble(double d)
 {
     jsval_layout l;
     l.asDouble = d;
     if (!JSVAL_IS_DOUBLE_IMPL(l)) {
@@ -1291,17 +1382,17 @@ JSStructuredCloneReader::startRead(Value
 
       case SCTAG_TRANSFER_MAP_HEADER:
         // A map header cannot be here but just at the beginning of the buffer.
         JS_ReportErrorNumber(context(), js_GetErrorMessage, nullptr,
                              JSMSG_SC_BAD_SERIALIZED_DATA,
                              "invalid input");
         return false;
 
-      case SCTAG_TRANSFER_MAP:
+      case SCTAG_TRANSFER_MAP_ENTRY:
         // A map cannot be here but just at the beginning of the buffer.
         JS_ReportErrorNumber(context(), js_GetErrorMessage, nullptr,
                              JSMSG_SC_BAD_SERIALIZED_DATA,
                              "invalid input");
         return false;
 
       case SCTAG_ARRAY_BUFFER_OBJECT:
         if (!readArrayBuffer(data, vp))
@@ -1377,47 +1468,67 @@ JSStructuredCloneReader::readId(jsid *id
     JS_ReportErrorNumber(context(), js_GetErrorMessage, nullptr,
                          JSMSG_SC_BAD_SERIALIZED_DATA, "id");
     return false;
 }
 
 bool
 JSStructuredCloneReader::readTransferMap()
 {
+    uint64_t *headerPos = in.tell();
+
     uint32_t tag, data;
     if (!in.getPair(&tag, &data))
         return false;
 
-    if (tag != SCTAG_TRANSFER_MAP_HEADER ||
-        (TransferableMapHeader)data == SCTAG_TM_MARKED)
+    if (tag != SCTAG_TRANSFER_MAP_HEADER || TransferableMapHeader(data) == SCTAG_TM_TRANSFERRED)
         return true;
 
-    if (!in.replacePair(SCTAG_TRANSFER_MAP_HEADER, SCTAG_TM_MARKED))
-        return false;
-
-    if (!in.readPair(&tag, &data))
+    uint64_t numTransferables;
+    MOZ_ALWAYS_TRUE(in.readPair(&tag, &data));
+    if (!in.read(&numTransferables))
         return false;
 
-    while (1) {
-        if (!in.getPair(&tag, &data))
+    for (uint64_t i = 0; i < numTransferables; i++) {
+        uint64_t *pos = in.tell();
+
+        if (!in.readPair(&tag, &data))
+            return false;
+        JS_ASSERT(tag == SCTAG_TRANSFER_MAP_ENTRY);
+        JS_ASSERT(data == SCTAG_TM_ALLOC_DATA);
+
+        void *content;
+        if (!in.readPtr(&content))
             return false;
 
-        if (tag != SCTAG_TRANSFER_MAP)
-            break;
-
-        void *content;
-
-        if (!in.readPair(&tag, &data) || !in.readPtr(&content))
+        uint64_t userdata;
+        if (!in.read(&userdata))
             return false;
 
         JSObject *obj = JS_NewArrayBufferWithContents(context(), content);
-        if (!obj || !allObjs.append(ObjectValue(*obj)))
+        if (!obj)
+            return false;
+
+        // Rewind to the SCTAG_TRANSFER_MAP_ENTRY and mark this entry as unowned by
+        // the input buffer.
+        uint64_t *next = in.tell();
+        in.seek(pos);
+        MOZ_ALWAYS_TRUE(in.replacePair(SCTAG_TRANSFER_MAP_ENTRY, SCTAG_TM_UNOWNED));
+        in.seek(next);
+
+        if (!allObjs.append(ObjectValue(*obj)))
             return false;
     }
 
+    // Mark the whole transfer map as consumed
+    uint64_t *endPos = in.tell();
+    in.seek(headerPos);
+    MOZ_ALWAYS_TRUE(in.replacePair(SCTAG_TRANSFER_MAP_HEADER, SCTAG_TM_TRANSFERRED));
+    in.seek(endPos);
+
     return true;
 }
 
 bool
 JSStructuredCloneReader::read(Value *vp)
 {
     if (!readTransferMap())
         return false;
@@ -1482,17 +1593,18 @@ JS_WriteStructuredClone(JSContext *cx, J
         optionalCallbacks :
         cx->runtime()->structuredCloneCallbacks;
     return WriteStructuredClone(cx, value, bufp, nbytesp, callbacks, closure, transferable);
 }
 
 JS_PUBLIC_API(bool)
 JS_ClearStructuredClone(const uint64_t *data, size_t nbytes)
 {
-    return ClearStructuredClone(data, nbytes);
+    ClearStructuredClone(data, nbytes);
+    return true;
 }
 
 JS_PUBLIC_API(bool)
 JS_StructuredCloneHasTransferables(const uint64_t *data, size_t nbytes,
                                    bool *hasTransferable)
 {
     bool transferable;
     if (!StructuredCloneHasTransferObjects(data, nbytes, &transferable))