Bug 861925 - Steal and neuter ArrayBuffers at end of structured clone, r=jorendorff
authorSteve Fink <sfink@mozilla.com>
Tue, 15 Oct 2013 23:48:20 -0700
changeset 165538 b12f63beba2cbfd92dbc4e9a820ea6c9b2bbc8fa
parent 165537 0efef923cc179420f5897c19e5e9c0164f1a7693
child 165539 b87bbbb0c612eb58efa8f1a789d319dd135d10ae
push id3066
push userakeybl@mozilla.com
push dateMon, 09 Dec 2013 19:58:46 +0000
treeherdermozilla-beta@a31a0dce83aa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjorendorff
bugs861925
milestone27.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 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
@@ -64,59 +64,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:
@@ -132,16 +159,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);
@@ -202,18 +235,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(); }
 
@@ -224,16 +256,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
@@ -256,17 +289,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);
@@ -293,64 +326,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
@@ -495,16 +539,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)
@@ -622,17 +672,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;
     }
@@ -658,31 +708,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()
@@ -890,35 +940,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;
@@ -955,18 +1047,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)) {
@@ -1282,17 +1373,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))
@@ -1368,47 +1459,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;
@@ -1473,17 +1584,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))