/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- * vim: set ts=8 sts=4 et sw=4 tw=99: * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* * This file implements the structured clone algorithm of * http://www.whatwg.org/specs/web-apps/current-work/multipage/common-dom-interfaces.html#safe-passing-of-structured-data * * The implementation differs slightly in that it uses an explicit stack, and * the "memory" maps source objects to sequential integer indexes rather than * directly pointing to destination objects. As a result, the order in which * things are added to the memory must exactly match the order in which they * are placed into 'allObjs', an analogous array of back-referenceable * destination objects constructed while reading. * * For the most part, this is easy: simply add objects to the memory when first * encountering them. But reading in a typed array requires an ArrayBuffer for * construction, so objects cannot just be added to 'allObjs' in the order they * are created. If they were, ArrayBuffers would come before typed arrays when * in fact the typed array was added to 'memory' first. * * So during writing, we add objects to the memory when first encountering * them. When reading a typed array, a placeholder is pushed onto allObjs until * the ArrayBuffer has been read, then it is updated with the actual typed * array object. */ #include "js/StructuredClone.h" #include "mozilla/CheckedInt.h" #include "mozilla/EndianUtils.h" #include "mozilla/FloatingPoint.h" #include #include "jsapi.h" #include "jscntxt.h" #include "jsdate.h" #include "jswrapper.h" #include "builtin/MapObject.h" #include "js/Date.h" #include "js/GCHashTable.h" #include "vm/SavedFrame.h" #include "vm/SharedArrayObject.h" #include "vm/TypedArrayObject.h" #include "vm/WrapperObject.h" #include "jscntxtinlines.h" #include "jsobjinlines.h" using namespace js; using mozilla::BitwiseCast; using mozilla::IsNaN; using mozilla::LittleEndian; using mozilla::NativeEndian; using mozilla::NumbersAreIdentical; using JS::CanonicalizeNaN; // When you make updates here, make sure you consider whether you need to bump the // value of JS_STRUCTURED_CLONE_VERSION in js/public/StructuredClone.h. You will // likely need to increment the version if anything at all changes in the serialization // format. // // Note that SCTAG_END_OF_KEYS is written into the serialized form and should have // a stable ID, it need not be at the end of the list and should not be used for // sizing data structures. enum StructuredDataType : uint32_t { /* Structured data types provided by the engine */ SCTAG_FLOAT_MAX = 0xFFF00000, SCTAG_HEADER = 0xFFF10000, SCTAG_NULL = 0xFFFF0000, SCTAG_UNDEFINED, SCTAG_BOOLEAN, SCTAG_INT32, SCTAG_STRING, SCTAG_DATE_OBJECT, 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_DO_NOT_USE_1, // Required for backwards compatibility SCTAG_DO_NOT_USE_2, // Required for backwards compatibility SCTAG_TYPED_ARRAY_OBJECT, SCTAG_MAP_OBJECT, SCTAG_SET_OBJECT, SCTAG_END_OF_KEYS, SCTAG_DO_NOT_USE_3, // Required for backwards compatibility SCTAG_DATA_VIEW_OBJECT, SCTAG_SAVED_FRAME_OBJECT, // No new tags before principals. SCTAG_JSPRINCIPALS, SCTAG_NULL_JSPRINCIPALS, SCTAG_RECONSTRUCTED_SAVED_FRAME_PRINCIPALS_IS_SYSTEM, SCTAG_RECONSTRUCTED_SAVED_FRAME_PRINCIPALS_IS_NOT_SYSTEM, SCTAG_SHARED_ARRAY_BUFFER_OBJECT, SCTAG_TYPED_ARRAY_V1_MIN = 0xFFFF0100, SCTAG_TYPED_ARRAY_V1_INT8 = SCTAG_TYPED_ARRAY_V1_MIN + Scalar::Int8, SCTAG_TYPED_ARRAY_V1_UINT8 = SCTAG_TYPED_ARRAY_V1_MIN + Scalar::Uint8, SCTAG_TYPED_ARRAY_V1_INT16 = SCTAG_TYPED_ARRAY_V1_MIN + Scalar::Int16, SCTAG_TYPED_ARRAY_V1_UINT16 = SCTAG_TYPED_ARRAY_V1_MIN + Scalar::Uint16, SCTAG_TYPED_ARRAY_V1_INT32 = SCTAG_TYPED_ARRAY_V1_MIN + Scalar::Int32, SCTAG_TYPED_ARRAY_V1_UINT32 = SCTAG_TYPED_ARRAY_V1_MIN + Scalar::Uint32, SCTAG_TYPED_ARRAY_V1_FLOAT32 = SCTAG_TYPED_ARRAY_V1_MIN + Scalar::Float32, SCTAG_TYPED_ARRAY_V1_FLOAT64 = SCTAG_TYPED_ARRAY_V1_MIN + Scalar::Float64, SCTAG_TYPED_ARRAY_V1_UINT8_CLAMPED = SCTAG_TYPED_ARRAY_V1_MIN + Scalar::Uint8Clamped, SCTAG_TYPED_ARRAY_V1_MAX = SCTAG_TYPED_ARRAY_V1_MIN + Scalar::MaxTypedArrayViewType - 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_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: * * numTransferables (64 bits) * array of: * * pointer (64 bits) * extraData (64 bits), eg byte length for ArrayBuffers */ // Data associated with an SCTAG_TRANSFER_MAP_HEADER that tells whether the // contents have been read out yet or not. enum TransferableMapHeader { SCTAG_TM_UNREAD = 0, SCTAG_TM_TRANSFERRED }; static inline uint64_t PairToUInt64(uint32_t tag, uint32_t data) { return uint64_t(data) | (uint64_t(tag) << 32); } namespace js { template struct BufferIterator { typedef mozilla::BufferList BufferList; 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(); } bool readBytes(char* outData, size_t size) { return mBuffer.ReadBytes(mIter, outData, size); } void write(const T& data) { MOZ_ASSERT(mIter.HasRoomFor(sizeof(T))); *reinterpret_cast(mIter.Data()) = data; } T peek() const { MOZ_ASSERT(mIter.HasRoomFor(sizeof(T))); return *reinterpret_cast(mIter.Data()); } bool canPeek() const { return mIter.HasRoomFor(sizeof(T)); } BufferList& mBuffer; typename BufferList::IterImpl mIter; }; struct SCOutput { public: using Iter = BufferIterator; 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); bool writeChars(const Latin1Char* p, size_t nchars); bool writeChars(const char16_t* p, size_t nchars); bool writePtr(const void*); template 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); } Iter iter() { return BufferIterator(buf); } size_t offset(Iter dest) { return dest - iter(); } private: JSContext* cx; mozilla::BufferList buf; }; class SCInput { typedef js::BufferIterator BufferIterator; public: SCInput(JSContext* cx, JSStructuredCloneData& data); JSContext* context() const { return cx; } static void getPtr(uint64_t data, void** ptr); static void getPair(uint64_t data, uint32_t* tagp, uint32_t* datap); bool read(uint64_t* p); bool readNativeEndian(uint64_t* p); bool readPair(uint32_t* tagp, uint32_t* datap); bool readDouble(double* p); 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); const BufferIterator& tell() const { return point; } void seekTo(const BufferIterator& pos) { point = pos; } void seekBy(size_t pos) { point += pos; } template bool readArray(T* p, size_t nelems); bool reportTruncated() { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "truncated"); return false; } private: void staticAssertions() { JS_STATIC_ASSERT(sizeof(char16_t) == 2); JS_STATIC_ASSERT(sizeof(uint32_t) == 4); } JSContext* cx; BufferIterator point; }; } /* namespace js */ struct JSStructuredCloneReader { public: explicit JSStructuredCloneReader(SCInput& in, JS::StructuredCloneScope scope, const JSStructuredCloneCallbacks* cb, void* cbClosure) : 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(); } bool readHeader(); bool readTransferMap(); template JSString* readStringImpl(uint32_t nchars); JSString* readString(uint32_t data); bool checkDouble(double d); bool readTypedArray(uint32_t arrayType, uint32_t nelems, MutableHandleValue vp, bool v1Read = false); bool readDataView(uint32_t byteLength, MutableHandleValue vp); 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; // 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. const JSStructuredCloneCallbacks* callbacks; // Any value passed to JS_ReadStructuredClone. void* closure; friend bool JS_ReadTypedArray(JSStructuredCloneReader* r, MutableHandleValue vp); }; struct JSStructuredCloneWriter { public: explicit JSStructuredCloneWriter(JSContext* cx, JS::StructuredCloneScope scope, JS::CloneDataPolicy cloneDataPolicy, const JSStructuredCloneCallbacks* cb, void* cbClosure, const Value& tVal) : out(cx), scope(scope), objs(out.context()), counts(out.context()), entries(out.context()), memory(out.context()), callbacks(cb), closure(cbClosure), transferable(out.context(), tVal), transferableObjects(out.context(), GCHashSet(cx)), cloneDataPolicy(cloneDataPolicy) {} ~JSStructuredCloneWriter(); bool init() { if (!memory.init()) { ReportOutOfMemory(context()); return false; } return parseTransferable() && writeHeader() && writeTransferMap(); } bool write(HandleValue v); SCOutput& output() { return out; } bool extractBuffer(JSStructuredCloneData* data) { bool success = out.extractBuffer(data); if (success) { data->setOptionalCallbacks(callbacks, closure, OwnTransferablePolicy::OwnsTransferablesIfAny); } return success; } JS::StructuredCloneScope cloneScope() const { return scope; } private: JSStructuredCloneWriter() = delete; JSStructuredCloneWriter(const JSStructuredCloneWriter&) = delete; JSContext* context() { return out.context(); } bool writeHeader(); bool writeTransferMap(); bool writeString(uint32_t tag, JSString* str); bool writeArrayBuffer(HandleObject obj); bool writeTypedArray(HandleObject obj); bool writeDataView(HandleObject obj); bool writeSharedArrayBuffer(HandleObject obj); bool startObject(HandleObject obj, bool* backref); bool startWrite(HandleValue v); bool traverseObject(HandleObject obj); bool traverseMap(HandleObject obj); bool traverseSet(HandleObject obj); bool traverseSavedFrame(HandleObject obj); bool reportDataCloneError(uint32_t errorId); bool parseTransferable(); bool transferOwnership(); inline void checkStack(); SCOutput out; // The (address space, thread) scope within which this clone is valid. JS::StructuredCloneScope scope; // Vector of objects with properties remaining to be written. // // NB: These can span multiple compartments, so the compartment must be // entered before any manipulation is performed. AutoValueVector objs; // counts[i] is the number of entries of objs[i] remaining to be written. // counts.length() == objs.length() and sum(counts) == entries.length(). Vector counts; // For JSObject: Property IDs as value // For Map: Key followed by value // For Set: Key // For SavedFrame: parent SavedFrame AutoValueVector entries; // The "memory" list described in the HTML5 internal structured cloning // algorithm. memory is a superset of objs; items are never removed from // Memory until a serialization operation is finished using CloneMemory = GCHashMap, SystemAllocPolicy>; Rooted memory; // The user defined callbacks that will be used for cloning. const JSStructuredCloneCallbacks* callbacks; // Any value passed to JS_WriteStructuredClone. void* closure; // Set of transferable objects RootedValue transferable; Rooted> transferableObjects; const JS::CloneDataPolicy cloneDataPolicy; friend bool JS_WriteString(JSStructuredCloneWriter* w, HandleString str); friend bool JS_WriteTypedArray(JSStructuredCloneWriter* w, HandleValue v); friend bool JS_ObjectNotWritten(JSStructuredCloneWriter* w, HandleObject obj); }; JS_FRIEND_API(uint64_t) js::GetSCOffset(JSStructuredCloneWriter* writer) { MOZ_ASSERT(writer); return writer->output().count() * sizeof(uint64_t); } JS_STATIC_ASSERT(SCTAG_END_OF_BUILTIN_TYPES <= JS_SCTAG_USER_MIN); JS_STATIC_ASSERT(JS_SCTAG_USER_MIN <= JS_SCTAG_USER_MAX); JS_STATIC_ASSERT(Scalar::Int8 == 0); static void ReportDataCloneError(JSContext* cx, const JSStructuredCloneCallbacks* callbacks, uint32_t errorId) { if (callbacks && callbacks->reportError) { callbacks->reportError(cx, errorId); return; } switch (errorId) { case JS_SCERR_DUP_TRANSFERABLE: JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SC_DUP_TRANSFERABLE); break; case JS_SCERR_TRANSFERABLE: JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SC_NOT_TRANSFERABLE); break; case JS_SCERR_UNSUPPORTED_TYPE: JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SC_UNSUPPORTED_TYPE); break; default: MOZ_CRASH("Unkown errorId"); break; } } bool WriteStructuredClone(JSContext* cx, HandleValue v, JSStructuredCloneData* bufp, JS::StructuredCloneScope scope, JS::CloneDataPolicy cloneDataPolicy, const JSStructuredCloneCallbacks* cb, void* cbClosure, const Value& transferable) { JSStructuredCloneWriter w(cx, scope, cloneDataPolicy, cb, cbClosure, transferable); return w.init() && w.write(v) && w.extractBuffer(bufp); } bool ReadStructuredClone(JSContext* cx, JSStructuredCloneData& data, JS::StructuredCloneScope scope, MutableHandleValue vp, const JSStructuredCloneCallbacks* cb, void* cbClosure) { SCInput in(cx, data); JSStructuredCloneReader r(in, scope, cb, cbClosure); return r.read(vp); } // If the given buffer contains Transferables, free them. Note that custom // Transferables will use the JSStructuredCloneCallbacks::freeTransfer() to // delete their transferables. template static void DiscardTransferables(mozilla::BufferList& buffer, const JSStructuredCloneCallbacks* cb, void* cbClosure) { auto point = BufferIterator(buffer); if (point.done()) return; // Empty buffer uint32_t tag, data; MOZ_RELEASE_ASSERT(point.canPeek()); SCInput::getPair(point.peek(), &tag, &data); point.next(); if (tag == SCTAG_HEADER) { if (point.done()) return; MOZ_RELEASE_ASSERT(point.canPeek()); SCInput::getPair(point.peek(), &tag, &data); point.next(); } if (tag != SCTAG_TRANSFER_MAP_HEADER) return; if (TransferableMapHeader(data) == SCTAG_TM_TRANSFERRED) return; // freeTransfer should not GC JS::AutoSuppressGCAnalysis nogc; if (point.done()) return; uint64_t numTransferables = NativeEndian::swapFromLittleEndian(point.peek()); point.next(); while (numTransferables--) { if (!point.canPeek()) return; uint32_t ownership; SCInput::getPair(point.peek(), &tag, &ownership); point.next(); MOZ_ASSERT(tag >= SCTAG_TRANSFER_MAP_PENDING_ENTRY); if (!point.canPeek()) return; void* content; SCInput::getPtr(point.peek(), &content); point.next(); if (!point.canPeek()) return; uint64_t extraData = NativeEndian::swapFromLittleEndian(point.peek()); point.next(); if (ownership < JS::SCTAG_TMO_FIRST_OWNED) continue; if (ownership == JS::SCTAG_TMO_ALLOC_DATA) { js_free(content); } else if (ownership == JS::SCTAG_TMO_MAPPED_DATA) { JS_ReleaseMappedArrayBufferContents(content, extraData); } else if (cb && cb->freeTransfer) { cb->freeTransfer(tag, JS::TransferableOwnership(ownership), content, extraData, cbClosure); } else { MOZ_ASSERT(false, "unknown ownership"); } } } static bool StructuredCloneHasTransferObjects(const JSStructuredCloneData& data) { auto iter = data.Iter(); if (data.Size() < sizeof(uint64_t)) return false; uint64_t u; data.ReadBytes(iter, reinterpret_cast(&u), sizeof(u)); uint32_t tag = uint32_t(u >> 32); return (tag == SCTAG_TRANSFER_MAP_HEADER); } namespace js { SCInput::SCInput(JSContext* cx, JSStructuredCloneData& data) : cx(cx), point(data) { static_assert(JSStructuredCloneData::kSegmentAlignment % 8 == 0, "structured clone buffer reads should be aligned"); MOZ_ASSERT(data.Size() % 8 == 0); } bool SCInput::read(uint64_t* p) { if (!point.canPeek()) { *p = 0; /* initialize to shut GCC up */ return reportTruncated(); } *p = NativeEndian::swapFromLittleEndian(point.peek()); point.next(); return true; } bool SCInput::readNativeEndian(uint64_t* p) { if (!point.canPeek()) { *p = 0; /* initialize to shut GCC up */ return reportTruncated(); } *p = point.peek(); point.next(); return true; } bool SCInput::readPair(uint32_t* tagp, uint32_t* datap) { uint64_t u; bool ok = read(&u); if (ok) { *tagp = uint32_t(u >> 32); *datap = uint32_t(u); } return ok; } bool SCInput::get(uint64_t* p) { if (!point.canPeek()) return reportTruncated(); *p = NativeEndian::swapFromLittleEndian(point.peek()); return true; } bool SCInput::getPair(uint32_t* tagp, uint32_t* datap) { uint64_t u = 0; if (!get(&u)) return false; *tagp = uint32_t(u >> 32); *datap = uint32_t(u); return true; } void SCInput::getPair(uint64_t data, uint32_t* tagp, uint32_t* datap) { uint64_t u = NativeEndian::swapFromLittleEndian(data); *tagp = uint32_t(u >> 32); *datap = uint32_t(u); } bool SCInput::readDouble(double* p) { union { uint64_t u; double d; } pun; if (!read(&pun.u)) return false; *p = CanonicalizeNaN(pun.d); return true; } template static void swapFromLittleEndianInPlace(T* ptr, size_t nelems) { if (nelems > 0) NativeEndian::swapFromLittleEndianInPlace(ptr, nelems); } template <> void swapFromLittleEndianInPlace(uint8_t* ptr, size_t nelems) {} template bool SCInput::readArray(T* p, size_t nelems) { if (!nelems) return true; JS_STATIC_ASSERT(sizeof(uint64_t) % sizeof(T) == 0); /* * Fail if nelems is so huge as to make JS_HOWMANY overflow or if nwords is * larger than the remaining data. */ size_t nwords = JS_HOWMANY(nelems, sizeof(uint64_t) / sizeof(T)); if (nelems + sizeof(uint64_t) / sizeof(T) - 1 < nelems) return reportTruncated(); size_t size = sizeof(T) * nelems; if (!point.readBytes(reinterpret_cast(p), size)) return false; swapFromLittleEndianInPlace(p, nelems); point += sizeof(uint64_t) * nwords - size; return true; } bool SCInput::readBytes(void* p, size_t nbytes) { return readArray((uint8_t*) p, nbytes); } bool SCInput::readChars(Latin1Char* p, size_t nchars) { static_assert(sizeof(Latin1Char) == sizeof(uint8_t), "Latin1Char must fit in 1 byte"); return readBytes(p, nchars); } bool SCInput::readChars(char16_t* p, size_t nchars) { MOZ_ASSERT(sizeof(char16_t) == sizeof(uint16_t)); return readArray((uint16_t*) p, nchars); } void SCInput::getPtr(uint64_t data, void** ptr) { // No endianness conversion is used for pointers, since they are not sent // across address spaces anyway. *ptr = reinterpret_cast(data); } bool SCInput::readPtr(void** p) { uint64_t u; if (!readNativeEndian(&u)) return false; *p = reinterpret_cast(NativeEndian::swapFromLittleEndian(u)); return true; } SCOutput::SCOutput(JSContext* cx) : cx(cx) , buf(0, 0, 4096, cx) { } bool SCOutput::write(uint64_t u) { uint64_t v = NativeEndian::swapToLittleEndian(u); return buf.WriteBytes(reinterpret_cast(&v), sizeof(u)); } bool SCOutput::writePair(uint32_t tag, uint32_t data) { /* * As it happens, the tag word appears after the data word in the output. * This is because exponents occupy the last 2 bytes of doubles on the * little-endian platforms we care most about. * * For example, TrueValue() is written using writePair(SCTAG_BOOLEAN, 1). * PairToUInt64 produces the number 0xFFFF000200000001. * That is written out as the bytes 01 00 00 00 02 00 FF FF. */ return write(PairToUInt64(tag, data)); } static inline double ReinterpretPairAsDouble(uint32_t tag, uint32_t data) { return BitwiseCast(PairToUInt64(tag, data)); } bool SCOutput::writeDouble(double d) { return write(BitwiseCast(CanonicalizeNaN(d))); } template static T swapToLittleEndian(T value) { return NativeEndian::swapToLittleEndian(value); } template <> uint8_t swapToLittleEndian(uint8_t value) { return value; } template bool SCOutput::writeArray(const T* p, size_t nelems) { JS_STATIC_ASSERT(8 % sizeof(T) == 0); JS_STATIC_ASSERT(sizeof(uint64_t) % sizeof(T) == 0); if (nelems == 0) return true; if (nelems + sizeof(uint64_t) / sizeof(T) - 1 < nelems) { ReportAllocationOverflow(context()); return false; } for (size_t i = 0; i < nelems; i++) { T value = swapToLittleEndian(p[i]); if (!buf.WriteBytes(reinterpret_cast(&value), sizeof(value))) return false; } // zero-pad to 8 bytes boundary size_t nwords = JS_HOWMANY(nelems, sizeof(uint64_t) / sizeof(T)); size_t padbytes = sizeof(uint64_t) * nwords - sizeof(T) * nelems; char zero = 0; for (size_t i = 0; i < padbytes; i++) { if (!buf.WriteBytes(&zero, sizeof(zero))) return false; } return true; } bool SCOutput::writeBytes(const void* p, size_t nbytes) { return writeArray((const uint8_t*) p, nbytes); } bool SCOutput::writeChars(const char16_t* p, size_t nchars) { static_assert(sizeof(char16_t) == sizeof(uint16_t), "required so that treating char16_t[] memory as uint16_t[] " "memory is permissible"); return writeArray((const uint16_t*) p, nchars); } bool SCOutput::writeChars(const Latin1Char* p, size_t nchars) { static_assert(sizeof(Latin1Char) == sizeof(uint8_t), "Latin1Char must fit in 1 byte"); return writeBytes(p, nchars); } bool SCOutput::writePtr(const void* p) { return write(reinterpret_cast(p)); } bool SCOutput::extractBuffer(JSStructuredCloneData* data) { bool success; mozilla::BufferList out = buf.MoveFallible(&success); if (!success) { ReportOutOfMemory(cx); return false; } *data = JSStructuredCloneData(Move(out)); return true; } void SCOutput::discardTransferables(const JSStructuredCloneCallbacks* cb, void* cbClosure) { DiscardTransferables(buf, cb, cbClosure); } } /* namespace js */ JSStructuredCloneData::~JSStructuredCloneData() { if (!Size()) return; if (ownTransferables_ == OwnTransferablePolicy::OwnsTransferablesIfAny) DiscardTransferables(*this, callbacks_, closure_); } JS_STATIC_ASSERT(JSString::MAX_LENGTH < UINT32_MAX); JSStructuredCloneWriter::~JSStructuredCloneWriter() { // Free any transferable data left lying around in the buffer if (out.count()) { out.discardTransferables(callbacks, closure); } } bool JSStructuredCloneWriter::parseTransferable() { // NOTE: The transferables set is tested for non-emptiness at various // junctures in structured cloning, so this set must be initialized // by this method in all non-error cases. MOZ_ASSERT(!transferableObjects.initialized(), "parseTransferable called with stale data"); if (transferable.isNull() || transferable.isUndefined()) return transferableObjects.init(0); if (!transferable.isObject()) return reportDataCloneError(JS_SCERR_TRANSFERABLE); JSContext* cx = context(); RootedObject array(cx, &transferable.toObject()); bool isArray; if (!JS_IsArrayObject(cx, array, &isArray)) return false; if (!isArray) return reportDataCloneError(JS_SCERR_TRANSFERABLE); uint32_t length; if (!JS_GetArrayLength(cx, array, &length)) return false; // Initialize the set for the provided array's length. if (!transferableObjects.init(length)) return false; if (length == 0) return true; RootedValue v(context()); RootedObject tObj(context()); for (uint32_t i = 0; i < length; ++i) { if (!CheckForInterrupt(cx)) return false; if (!JS_GetElement(cx, array, i, &v)) return false; if (!v.isObject()) return reportDataCloneError(JS_SCERR_TRANSFERABLE); tObj = &v.toObject(); // Backward compatibility, see bug 1302036 and bug 1302037. if (tObj->is()) { if (!JS_ReportErrorFlagsAndNumberASCII(cx, JSREPORT_WARNING, GetErrorMessage, nullptr, JSMSG_SC_SAB_TRANSFER)) return false; continue; } // No duplicates allowed auto p = transferableObjects.lookupForAdd(tObj); if (p) return reportDataCloneError(JS_SCERR_DUP_TRANSFERABLE); if (!transferableObjects.add(p, tObj)) return false; } return true; } bool JSStructuredCloneWriter::reportDataCloneError(uint32_t errorId) { ReportDataCloneError(context(), callbacks, errorId); return false; } bool JSStructuredCloneWriter::writeString(uint32_t tag, JSString* str) { JSLinearString* linear = str->ensureLinear(context()); if (!linear) return false; static_assert(JSString::MAX_LENGTH <= INT32_MAX, "String length must fit in 31 bits"); uint32_t length = linear->length(); uint32_t lengthAndEncoding = length | (uint32_t(linear->hasLatin1Chars()) << 31); if (!out.writePair(tag, lengthAndEncoding)) return false; JS::AutoCheckCannotGC nogc; return linear->hasLatin1Chars() ? out.writeChars(linear->latin1Chars(nogc), length) : out.writeChars(linear->twoByteChars(nogc), length); } inline void JSStructuredCloneWriter::checkStack() { #ifdef DEBUG /* To avoid making serialization O(n^2), limit stack-checking at 10. */ const size_t MAX = 10; size_t limit = Min(counts.length(), MAX); MOZ_ASSERT(objs.length() == counts.length()); size_t total = 0; for (size_t i = 0; i < limit; i++) { MOZ_ASSERT(total + counts[i] >= total); total += counts[i]; } if (counts.length() <= MAX) MOZ_ASSERT(total == entries.length()); else MOZ_ASSERT(total <= entries.length()); size_t j = objs.length(); for (size_t i = 0; i < limit; i++) { --j; MOZ_ASSERT(memory.has(&objs[j].toObject())); } #endif } /* * Write out a typed array. Note that post-v1 structured clone buffers do not * perform endianness conversion on stored data, so multibyte typed arrays * cannot be deserialized into a different endianness machine. Endianness * conversion would prevent sharing ArrayBuffers: if you have Int8Array and * Int16Array views of the same ArrayBuffer, should the data bytes be * byte-swapped when writing or not? The Int8Array requires them to not be * swapped; the Int16Array requires that they are. */ bool JSStructuredCloneWriter::writeTypedArray(HandleObject obj) { Rooted tarr(context(), &CheckedUnwrap(obj)->as()); JSAutoCompartment ac(context(), tarr); if (!TypedArrayObject::ensureHasBuffer(context(), tarr)) return false; if (!out.writePair(SCTAG_TYPED_ARRAY_OBJECT, tarr->length())) return false; uint64_t type = tarr->type(); if (!out.write(type)) return false; // Write out the ArrayBuffer tag and contents RootedValue val(context(), TypedArrayObject::bufferValue(tarr)); if (!startWrite(val)) return false; return out.write(tarr->byteOffset()); } bool JSStructuredCloneWriter::writeDataView(HandleObject obj) { Rooted view(context(), &CheckedUnwrap(obj)->as()); JSAutoCompartment ac(context(), view); if (!out.writePair(SCTAG_DATA_VIEW_OBJECT, view->byteLength())) return false; // Write out the ArrayBuffer tag and contents RootedValue val(context(), DataViewObject::bufferValue(view)); if (!startWrite(val)) return false; return out.write(view->byteOffset()); } bool JSStructuredCloneWriter::writeArrayBuffer(HandleObject obj) { ArrayBufferObject& buffer = CheckedUnwrap(obj)->as(); JSAutoCompartment ac(context(), &buffer); return out.writePair(SCTAG_ARRAY_BUFFER_OBJECT, buffer.byteLength()) && out.writeBytes(buffer.dataPointer(), buffer.byteLength()); } bool JSStructuredCloneWriter::writeSharedArrayBuffer(HandleObject obj) { if (!cloneDataPolicy.isSharedArrayBufferAllowed()) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_NOT_CLONABLE, "SharedArrayBuffer"); return false; } Rooted sharedArrayBuffer(context(), &CheckedUnwrap(obj)->as()); SharedArrayRawBuffer* rawbuf = sharedArrayBuffer->rawBufferObject(); // Avoids a race condition where the parent thread frees the buffer // before the child has accepted the transferable. rawbuf->addReference(); intptr_t p = reinterpret_cast(rawbuf); return out.writePair(SCTAG_SHARED_ARRAY_BUFFER_OBJECT, static_cast(sizeof(p))) && out.writeBytes(&p, sizeof(p)); } bool JSStructuredCloneWriter::startObject(HandleObject obj, bool* backref) { /* Handle cycles in the object graph. */ CloneMemory::AddPtr p = memory.lookupForAdd(obj); if ((*backref = p.found())) return out.writePair(SCTAG_BACK_REFERENCE_OBJECT, p->value()); if (!memory.add(p, obj, memory.count())) { ReportOutOfMemory(context()); return false; } if (memory.count() == UINT32_MAX) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_NEED_DIET, "object graph to serialize"); return false; } return true; } bool JSStructuredCloneWriter::traverseObject(HandleObject obj) { /* * Get enumerable property ids and put them in reverse order so that they * will come off the stack in forward order. */ AutoIdVector properties(context()); if (!GetPropertyKeys(context(), obj, JSITER_OWNONLY, &properties)) return false; for (size_t i = properties.length(); i > 0; --i) { MOZ_ASSERT(JSID_IS_STRING(properties[i - 1]) || JSID_IS_INT(properties[i - 1])); RootedValue val(context(), IdToValue(properties[i - 1])); if (!entries.append(val)) return false; } /* Push obj and count to the stack. */ if (!objs.append(ObjectValue(*obj)) || !counts.append(properties.length())) return false; checkStack(); /* Write the header for obj. */ ESClass cls; if (!GetBuiltinClass(context(), obj, &cls)) return false; return out.writePair(cls == ESClass::Array ? SCTAG_ARRAY_OBJECT : SCTAG_OBJECT_OBJECT, 0); } bool JSStructuredCloneWriter::traverseMap(HandleObject obj) { Rooted> newEntries(context(), GCVector(context())); { // If there is no wrapper, the compartment munging is a no-op. RootedObject unwrapped(context(), CheckedUnwrap(obj)); MOZ_ASSERT(unwrapped); JSAutoCompartment ac(context(), unwrapped); if (!MapObject::getKeysAndValuesInterleaved(context(), unwrapped, &newEntries)) return false; } if (!context()->compartment()->wrap(context(), &newEntries)) return false; for (size_t i = newEntries.length(); i > 0; --i) { if (!entries.append(newEntries[i - 1])) return false; } /* Push obj and count to the stack. */ if (!objs.append(ObjectValue(*obj)) || !counts.append(newEntries.length())) return false; checkStack(); /* Write the header for obj. */ return out.writePair(SCTAG_MAP_OBJECT, 0); } bool JSStructuredCloneWriter::traverseSet(HandleObject obj) { Rooted> keys(context(), GCVector(context())); { // If there is no wrapper, the compartment munging is a no-op. RootedObject unwrapped(context(), CheckedUnwrap(obj)); MOZ_ASSERT(unwrapped); JSAutoCompartment ac(context(), unwrapped); if (!SetObject::keys(context(), unwrapped, &keys)) return false; } if (!context()->compartment()->wrap(context(), &keys)) return false; for (size_t i = keys.length(); i > 0; --i) { if (!entries.append(keys[i - 1])) return false; } /* Push obj and count to the stack. */ if (!objs.append(ObjectValue(*obj)) || !counts.append(keys.length())) return false; checkStack(); /* Write the header for obj. */ return out.writePair(SCTAG_SET_OBJECT, 0); } // Objects are written as a "preorder" traversal of the object graph: object // "headers" (the class tag and any data needed for initial construction) are // visited first, then the children are recursed through (where children are // properties, Set or Map entries, etc.). So for example // // m = new Map(); // m.set(key1 = {}, value1 = {}) // // would be stored as // // // // // // // // // Notice how the end-of-children marker for key1 is sandwiched between the // value1 beginning and end. bool JSStructuredCloneWriter::traverseSavedFrame(HandleObject obj) { RootedObject unwrapped(context(), js::CheckedUnwrap(obj)); MOZ_ASSERT(unwrapped && unwrapped->is()); RootedSavedFrame savedFrame(context(), &unwrapped->as()); RootedObject parent(context(), savedFrame->getParent()); if (!context()->compartment()->wrap(context(), &parent)) return false; if (!objs.append(ObjectValue(*obj)) || !entries.append(parent ? ObjectValue(*parent) : NullValue()) || !counts.append(1)) { return false; } checkStack(); // Write the SavedFrame tag and the SavedFrame's principals. if (savedFrame->getPrincipals() == &ReconstructedSavedFramePrincipals::IsSystem) { if (!out.writePair(SCTAG_SAVED_FRAME_OBJECT, SCTAG_RECONSTRUCTED_SAVED_FRAME_PRINCIPALS_IS_SYSTEM)) { return false; }; } else if (savedFrame->getPrincipals() == &ReconstructedSavedFramePrincipals::IsNotSystem) { if (!out.writePair(SCTAG_SAVED_FRAME_OBJECT, SCTAG_RECONSTRUCTED_SAVED_FRAME_PRINCIPALS_IS_NOT_SYSTEM)) { return false; } } else { if (auto principals = savedFrame->getPrincipals()) { if (!out.writePair(SCTAG_SAVED_FRAME_OBJECT, SCTAG_JSPRINCIPALS) || !principals->write(context(), this)) { return false; } } else { if (!out.writePair(SCTAG_SAVED_FRAME_OBJECT, SCTAG_NULL_JSPRINCIPALS)) return false; } } // Write the SavedFrame's reserved slots, except for the parent, which is // queued on objs for further traversal. RootedValue val(context()); val = StringValue(savedFrame->getSource()); if (!startWrite(val)) return false; val = NumberValue(savedFrame->getLine()); if (!startWrite(val)) return false; val = NumberValue(savedFrame->getColumn()); if (!startWrite(val)) return false; auto name = savedFrame->getFunctionDisplayName(); val = name ? StringValue(name) : NullValue(); if (!startWrite(val)) return false; auto cause = savedFrame->getAsyncCause(); val = cause ? StringValue(cause) : NullValue(); if (!startWrite(val)) return false; return true; } bool JSStructuredCloneWriter::startWrite(HandleValue v) { assertSameCompartment(context(), v); if (v.isString()) { return writeString(SCTAG_STRING, v.toString()); } else if (v.isInt32()) { return out.writePair(SCTAG_INT32, v.toInt32()); } else if (v.isDouble()) { return out.writeDouble(v.toDouble()); } else if (v.isBoolean()) { return out.writePair(SCTAG_BOOLEAN, v.toBoolean()); } else if (v.isNull()) { return out.writePair(SCTAG_NULL, 0); } else if (v.isUndefined()) { return out.writePair(SCTAG_UNDEFINED, 0); } else if (v.isObject()) { RootedObject obj(context(), &v.toObject()); bool backref; if (!startObject(obj, &backref)) return false; if (backref) return true; ESClass cls; if (!GetBuiltinClass(context(), obj, &cls)) return false; if (cls == ESClass::RegExp) { RegExpGuard re(context()); if (!RegExpToShared(context(), obj, &re)) return false; return out.writePair(SCTAG_REGEXP_OBJECT, re->getFlags()) && writeString(SCTAG_STRING, re->getSource()); } else if (cls == ESClass::Date) { RootedValue unboxed(context()); if (!Unbox(context(), obj, &unboxed)) return false; return out.writePair(SCTAG_DATE_OBJECT, 0) && out.writeDouble(unboxed.toNumber()); } else if (JS_IsTypedArrayObject(obj)) { return writeTypedArray(obj); } else if (JS_IsDataViewObject(obj)) { return writeDataView(obj); } else if (JS_IsArrayBufferObject(obj) && JS_ArrayBufferHasData(obj)) { return writeArrayBuffer(obj); } else if (JS_IsSharedArrayBufferObject(obj)) { return writeSharedArrayBuffer(obj); } else if (cls == ESClass::Object) { return traverseObject(obj); } else if (cls == ESClass::Array) { return traverseObject(obj); } else if (cls == ESClass::Boolean) { RootedValue unboxed(context()); if (!Unbox(context(), obj, &unboxed)) return false; return out.writePair(SCTAG_BOOLEAN_OBJECT, unboxed.toBoolean()); } else if (cls == ESClass::Number) { RootedValue unboxed(context()); if (!Unbox(context(), obj, &unboxed)) return false; return out.writePair(SCTAG_NUMBER_OBJECT, 0) && out.writeDouble(unboxed.toNumber()); } else if (cls == ESClass::String) { RootedValue unboxed(context()); if (!Unbox(context(), obj, &unboxed)) return false; return writeString(SCTAG_STRING_OBJECT, unboxed.toString()); } else if (cls == ESClass::Map) { return traverseMap(obj); } else if (cls == ESClass::Set) { return traverseSet(obj); } else if (SavedFrame::isSavedFrameOrWrapperAndNotProto(*obj)) { return traverseSavedFrame(obj); } if (callbacks && callbacks->write) return callbacks->write(context(), this, obj, closure); /* else fall through */ } return reportDataCloneError(JS_SCERR_UNSUPPORTED_TYPE); } bool JSStructuredCloneWriter::writeHeader() { return out.writePair(SCTAG_HEADER, (uint32_t)scope); } bool JSStructuredCloneWriter::writeTransferMap() { if (transferableObjects.empty()) return true; if (!out.writePair(SCTAG_TRANSFER_MAP_HEADER, (uint32_t)SCTAG_TM_UNREAD)) 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)) return false; if (!out.writePtr(nullptr)) // Pointer to ArrayBuffer contents. return false; if (!out.write(0)) // extraData 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. auto point = out.iter(); MOZ_RELEASE_ASSERT(point.canPeek()); MOZ_ASSERT(uint32_t(NativeEndian::swapFromLittleEndian(point.peek()) >> 32) == SCTAG_HEADER); point++; MOZ_RELEASE_ASSERT(point.canPeek()); MOZ_ASSERT(uint32_t(NativeEndian::swapFromLittleEndian(point.peek()) >> 32) == SCTAG_TRANSFER_MAP_HEADER); point++; MOZ_RELEASE_ASSERT(point.canPeek()); MOZ_ASSERT(NativeEndian::swapFromLittleEndian(point.peek()) == transferableObjects.count()); point++; 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(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 arrayBuffer(cx, &CheckedUnwrap(obj)->as()); JSAutoCompartment ac(cx, arrayBuffer); size_t nbytes = arrayBuffer->byteLength(); if (arrayBuffer->isWasm() || arrayBuffer->isPreparedForAsmJS()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_WASM_NO_TRANSFER); return false; } 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(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(content))); point.next(); point.write(NativeEndian::swapToLittleEndian(extraData)); point.next(); } #if DEBUG // Make sure there aren't any more transfer map entries after the expected // number we read out. if (!point.done()) { uint32_t tag, data; SCInput::getPair(point.peek(), &tag, &data); MOZ_ASSERT(tag < SCTAG_TRANSFER_MAP_HEADER || tag >= SCTAG_TRANSFER_MAP_END_OF_BUILTIN_TYPES); } #endif return true; } bool JSStructuredCloneWriter::write(HandleValue v) { if (!startWrite(v)) return false; while (!counts.empty()) { RootedObject obj(context(), &objs.back().toObject()); AutoCompartment ac(context(), obj); if (counts.back()) { counts.back()--; RootedValue key(context(), entries.back()); entries.popBack(); checkStack(); ESClass cls; if (!GetBuiltinClass(context(), obj, &cls)) return false; if (cls == ESClass::Map) { counts.back()--; RootedValue val(context(), entries.back()); entries.popBack(); checkStack(); if (!startWrite(key) || !startWrite(val)) return false; } else if (cls == ESClass::Set || SavedFrame::isSavedFrameOrWrapperAndNotProto(*obj)) { if (!startWrite(key)) return false; } else { RootedId id(context()); if (!ValueToId(context(), key, &id)) return false; MOZ_ASSERT(JSID_IS_STRING(id) || JSID_IS_INT(id)); /* * If obj still has an own property named id, write it out. * The cost of re-checking could be avoided by using * NativeIterators. */ bool found; if (!HasOwnProperty(context(), obj, id, &found)) return false; if (found) { RootedValue val(context()); if (!startWrite(key) || !GetProperty(context(), obj, obj, id, &val) || !startWrite(val)) { return false; } } } } else { if (!out.writePair(SCTAG_END_OF_KEYS, 0)) return false; objs.popBack(); counts.popBack(); } } memory.clear(); return transferOwnership(); } bool JSStructuredCloneReader::checkDouble(double d) { if (!JS::IsCanonicalized(d)) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "unrecognized NaN"); return false; } return true; } namespace { template class Chars { JSContext* cx; CharT* p; public: explicit Chars(JSContext* cx) : cx(cx), p(nullptr) {} ~Chars() { js_free(p); } bool allocate(size_t len) { MOZ_ASSERT(!p); // We're going to null-terminate! p = cx->pod_malloc(len + 1); if (p) { p[len] = CharT(0); return true; } return false; } CharT* get() { return p; } void forget() { p = nullptr; } }; } /* anonymous namespace */ template JSString* JSStructuredCloneReader::readStringImpl(uint32_t nchars) { if (nchars > JSString::MAX_LENGTH) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "string length"); return nullptr; } Chars chars(context()); if (!chars.allocate(nchars) || !in.readChars(chars.get(), nchars)) return nullptr; JSString* str = NewString(context(), chars.get(), nchars); if (str) chars.forget(); return str; } JSString* JSStructuredCloneReader::readString(uint32_t data) { uint32_t nchars = data & JS_BITMASK(31); bool latin1 = data & (1 << 31); return latin1 ? readStringImpl(nchars) : readStringImpl(nchars); } static uint32_t TagToV1ArrayType(uint32_t tag) { MOZ_ASSERT(tag >= SCTAG_TYPED_ARRAY_V1_MIN && tag <= SCTAG_TYPED_ARRAY_V1_MAX); return tag - SCTAG_TYPED_ARRAY_V1_MIN; } bool JSStructuredCloneReader::readTypedArray(uint32_t arrayType, uint32_t nelems, MutableHandleValue vp, bool v1Read) { if (arrayType > Scalar::Uint8Clamped) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "unhandled typed array element type"); return false; } // Push a placeholder onto the allObjs list to stand in for the typed array uint32_t placeholderIndex = allObjs.length(); Value dummy = UndefinedValue(); if (!allObjs.append(dummy)) return false; // Read the ArrayBuffer object and its contents (but no properties) RootedValue v(context()); uint32_t byteOffset; if (v1Read) { if (!readV1ArrayBuffer(arrayType, nelems, &v)) return false; byteOffset = 0; } else { if (!startRead(&v)) return false; uint64_t n; if (!in.read(&n)) return false; byteOffset = n; } if (!v.isObject() || !v.toObject().is()) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "typed array must be backed by an ArrayBuffer"); return false; } RootedObject buffer(context(), &v.toObject()); RootedObject obj(context(), nullptr); switch (arrayType) { case Scalar::Int8: obj = JS_NewInt8ArrayWithBuffer(context(), buffer, byteOffset, nelems); break; case Scalar::Uint8: obj = JS_NewUint8ArrayWithBuffer(context(), buffer, byteOffset, nelems); break; case Scalar::Int16: obj = JS_NewInt16ArrayWithBuffer(context(), buffer, byteOffset, nelems); break; case Scalar::Uint16: obj = JS_NewUint16ArrayWithBuffer(context(), buffer, byteOffset, nelems); break; case Scalar::Int32: obj = JS_NewInt32ArrayWithBuffer(context(), buffer, byteOffset, nelems); break; case Scalar::Uint32: obj = JS_NewUint32ArrayWithBuffer(context(), buffer, byteOffset, nelems); break; case Scalar::Float32: obj = JS_NewFloat32ArrayWithBuffer(context(), buffer, byteOffset, nelems); break; case Scalar::Float64: obj = JS_NewFloat64ArrayWithBuffer(context(), buffer, byteOffset, nelems); break; case Scalar::Uint8Clamped: obj = JS_NewUint8ClampedArrayWithBuffer(context(), buffer, byteOffset, nelems); break; default: MOZ_CRASH("Can't happen: arrayType range checked above"); } if (!obj) return false; vp.setObject(*obj); allObjs[placeholderIndex].set(vp); return true; } bool JSStructuredCloneReader::readDataView(uint32_t byteLength, MutableHandleValue vp) { // Push a placeholder onto the allObjs list to stand in for the DataView. uint32_t placeholderIndex = allObjs.length(); Value dummy = UndefinedValue(); if (!allObjs.append(dummy)) return false; // Read the ArrayBuffer object and its contents (but no properties). RootedValue v(context()); if (!startRead(&v)) return false; if (!v.isObject() || !v.toObject().is()) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "DataView must be backed by an ArrayBuffer"); return false; } // Read byteOffset. uint64_t n; if (!in.read(&n)) return false; uint32_t byteOffset = n; RootedObject buffer(context(), &v.toObject()); RootedObject obj(context(), JS_NewDataView(context(), buffer, byteOffset, byteLength)); if (!obj) return false; vp.setObject(*obj); allObjs[placeholderIndex].set(vp); return true; } bool JSStructuredCloneReader::readArrayBuffer(uint32_t nbytes, MutableHandleValue vp) { JSObject* obj = ArrayBufferObject::create(context(), nbytes); if (!obj) return false; vp.setObject(*obj); ArrayBufferObject& buffer = obj->as(); MOZ_ASSERT(buffer.byteLength() == nbytes); return in.readArray(buffer.dataPointer(), nbytes); } bool JSStructuredCloneReader::readSharedArrayBuffer(uint32_t nbytes, MutableHandleValue vp) { intptr_t p; in.readBytes(&p, sizeof(p)); SharedArrayRawBuffer* rawbuf = reinterpret_cast(p); // There's no guarantee that the receiving agent has enabled shared memory // even if the transmitting agent has done so. Ideally we'd check at the // transmission point, but that's tricky, and it will be a very rare problem // in any case. Just fail at the receiving end if we can't handle it. if (!context()->compartment()->creationOptions().getSharedMemoryAndAtomicsEnabled()) { // The sending side performed a reference increment before sending. // Account for that here before leaving. if (rawbuf) rawbuf->dropReference(); JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_SAB_DISABLED); return false; } // The constructor absorbs the reference count increment performed by the sender. JSObject* obj = SharedArrayBufferObject::New(context(), rawbuf); vp.setObject(*obj); return true; } /* * Read in the data for a structured clone version 1 ArrayBuffer, performing * endianness-conversion while reading. */ bool JSStructuredCloneReader::readV1ArrayBuffer(uint32_t arrayType, uint32_t nelems, MutableHandleValue vp) { if (arrayType > Scalar::Uint8Clamped) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "invalid TypedArray type"); return false; } mozilla::CheckedInt nbytes = mozilla::CheckedInt(nelems) * TypedArrayElemSize(static_cast(arrayType)); if (!nbytes.isValid() || nbytes.value() > UINT32_MAX) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "invalid typed array size"); return false; } JSObject* obj = ArrayBufferObject::create(context(), nbytes.value()); if (!obj) return false; vp.setObject(*obj); ArrayBufferObject& buffer = obj->as(); MOZ_ASSERT(buffer.byteLength() == nbytes); switch (arrayType) { case Scalar::Int8: case Scalar::Uint8: case Scalar::Uint8Clamped: return in.readArray((uint8_t*) buffer.dataPointer(), nelems); case Scalar::Int16: case Scalar::Uint16: return in.readArray((uint16_t*) buffer.dataPointer(), nelems); case Scalar::Int32: case Scalar::Uint32: case Scalar::Float32: return in.readArray((uint32_t*) buffer.dataPointer(), nelems); case Scalar::Float64: return in.readArray((uint64_t*) buffer.dataPointer(), nelems); default: MOZ_CRASH("Can't happen: arrayType range checked by caller"); } } static bool PrimitiveToObject(JSContext* cx, MutableHandleValue vp) { JSObject* obj = js::PrimitiveToObject(cx, vp); if (!obj) return false; vp.setObject(*obj); return true; } bool JSStructuredCloneReader::startRead(MutableHandleValue vp) { uint32_t tag, data; if (!in.readPair(&tag, &data)) return false; switch (tag) { case SCTAG_NULL: vp.setNull(); break; case SCTAG_UNDEFINED: vp.setUndefined(); break; case SCTAG_INT32: vp.setInt32(data); break; case SCTAG_BOOLEAN: case SCTAG_BOOLEAN_OBJECT: vp.setBoolean(!!data); if (tag == SCTAG_BOOLEAN_OBJECT && !PrimitiveToObject(context(), vp)) return false; break; case SCTAG_STRING: case SCTAG_STRING_OBJECT: { JSString* str = readString(data); if (!str) return false; vp.setString(str); if (tag == SCTAG_STRING_OBJECT && !PrimitiveToObject(context(), vp)) return false; break; } case SCTAG_NUMBER_OBJECT: { double d; if (!in.readDouble(&d) || !checkDouble(d)) return false; vp.setDouble(d); if (!PrimitiveToObject(context(), vp)) return false; break; } case SCTAG_DATE_OBJECT: { double d; if (!in.readDouble(&d) || !checkDouble(d)) return false; JS::ClippedTime t = JS::TimeClip(d); if (!NumbersAreIdentical(d, t.toDouble())) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "date"); return false; } JSObject* obj = NewDateObjectMsec(context(), t); if (!obj) return false; vp.setObject(*obj); break; } case SCTAG_REGEXP_OBJECT: { RegExpFlag flags = RegExpFlag(data); uint32_t tag2, stringData; if (!in.readPair(&tag2, &stringData)) return false; if (tag2 != SCTAG_STRING) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "regexp"); return false; } JSString* str = readString(stringData); if (!str) return false; RootedAtom atom(context(), AtomizeString(context(), str)); if (!atom) return false; RegExpObject* reobj = RegExpObject::create(context(), atom, flags, nullptr, context()->tempLifoAlloc()); if (!reobj) return false; vp.setObject(*reobj); break; } case SCTAG_ARRAY_OBJECT: case SCTAG_OBJECT_OBJECT: { JSObject* obj = (tag == SCTAG_ARRAY_OBJECT) ? (JSObject*) NewDenseEmptyArray(context()) : (JSObject*) NewBuiltinClassInstance(context()); if (!obj || !objs.append(ObjectValue(*obj))) return false; vp.setObject(*obj); break; } case SCTAG_BACK_REFERENCE_OBJECT: { if (data >= allObjs.length() || !allObjs[data].isObject()) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "invalid back reference in input"); return false; } vp.set(allObjs[data]); return true; } case SCTAG_TRANSFER_MAP_HEADER: case SCTAG_TRANSFER_MAP_PENDING_ENTRY: // We should be past all the transfer map tags. JS_ReportErrorNumberASCII(context(), GetErrorMessage, NULL, JSMSG_SC_BAD_SERIALIZED_DATA, "invalid input"); return false; case SCTAG_ARRAY_BUFFER_OBJECT: if (!readArrayBuffer(data, vp)) return false; break; case SCTAG_SHARED_ARRAY_BUFFER_OBJECT: if (!readSharedArrayBuffer(data, vp)) return false; break; case SCTAG_TYPED_ARRAY_OBJECT: { // readTypedArray adds the array to allObjs. uint64_t arrayType; if (!in.read(&arrayType)) return false; return readTypedArray(arrayType, data, vp); } case SCTAG_DATA_VIEW_OBJECT: { // readDataView adds the array to allObjs. return readDataView(data, vp); } case SCTAG_MAP_OBJECT: { JSObject* obj = MapObject::create(context()); if (!obj || !objs.append(ObjectValue(*obj))) return false; vp.setObject(*obj); break; } case SCTAG_SET_OBJECT: { JSObject* obj = SetObject::create(context()); if (!obj || !objs.append(ObjectValue(*obj))) return false; vp.setObject(*obj); break; } case SCTAG_SAVED_FRAME_OBJECT: { auto obj = readSavedFrame(data); if (!obj || !objs.append(ObjectValue(*obj))) return false; vp.setObject(*obj); break; } default: { if (tag <= SCTAG_FLOAT_MAX) { double d = ReinterpretPairAsDouble(tag, data); if (!checkDouble(d)) return false; vp.setNumber(d); break; } if (SCTAG_TYPED_ARRAY_V1_MIN <= tag && tag <= SCTAG_TYPED_ARRAY_V1_MAX) { // A v1-format typed array // readTypedArray adds the array to allObjs return readTypedArray(TagToV1ArrayType(tag), data, vp, true); } if (!callbacks || !callbacks->read) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "unsupported type"); return false; } JSObject* obj = callbacks->read(context(), this, tag, data, closure); if (!obj) return false; vp.setObject(*obj); } } if (vp.isObject() && !allObjs.append(vp)) return false; return true; } bool JSStructuredCloneReader::readHeader() { uint32_t tag, data; if (!in.getPair(&tag, &data)) return in.reportTruncated(); if (tag != SCTAG_HEADER) { // Old structured clone buffer. We must have read it from disk. storedScope = JS::StructuredCloneScope::DifferentProcess; return true; } MOZ_ALWAYS_TRUE(in.readPair(&tag, &data)); storedScope = JS::StructuredCloneScope(data); if (data != uint32_t(JS::StructuredCloneScope::SameProcessSameThread) && data != uint32_t(JS::StructuredCloneScope::SameProcessDifferentThread) && data != uint32_t(JS::StructuredCloneScope::DifferentProcess)) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "invalid structured clone scope"); return false; } if (storedScope < allowedScope) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "incompatible structured clone scope"); return false; } return true; } bool JSStructuredCloneReader::readTransferMap() { JSContext* cx = context(); auto headerPos = in.tell(); uint32_t tag, data; if (!in.getPair(&tag, &data)) return in.reportTruncated(); if (tag != SCTAG_TRANSFER_MAP_HEADER || TransferableMapHeader(data) == SCTAG_TM_TRANSFERRED) return true; uint64_t numTransferables; MOZ_ALWAYS_TRUE(in.readPair(&tag, &data)); if (!in.read(&numTransferables)) return false; for (uint64_t i = 0; i < numTransferables; i++) { auto pos = in.tell(); if (!in.readPair(&tag, &data)) return false; MOZ_ASSERT(tag != SCTAG_TRANSFER_MAP_PENDING_ENTRY); RootedObject obj(cx); void* content; 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(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); MOZ_ASSERT(!cx->isExceptionPending()); } // On failure, the buffer will still own the data (since its ownership // will not get set to SCTAG_TMO_UNOWNED), so the data will be freed by // DiscardTransferables. if (!obj) return false; // Mark the SCTAG_TRANSFER_MAP_* entry as no longer owned by the input // buffer. pos.write(PairToUInt64(tag, JS::SCTAG_TMO_UNOWNED)); MOZ_ASSERT(!pos.done()); if (!allObjs.append(ObjectValue(*obj))) return false; } // Mark the whole transfer map as consumed. #ifdef DEBUG SCInput::getPair(headerPos.peek(), &tag, &data); MOZ_ASSERT(tag == SCTAG_TRANSFER_MAP_HEADER); MOZ_ASSERT(TransferableMapHeader(data) != SCTAG_TM_TRANSFERRED); #endif headerPos.write(PairToUInt64(SCTAG_TRANSFER_MAP_HEADER, SCTAG_TM_TRANSFERRED)); return true; } JSObject* JSStructuredCloneReader::readSavedFrame(uint32_t principalsTag) { RootedSavedFrame savedFrame(context(), SavedFrame::create(context())); if (!savedFrame) return nullptr; JSPrincipals* principals; if (principalsTag == SCTAG_JSPRINCIPALS) { if (!context()->runtime()->readPrincipals) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_UNSUPPORTED_TYPE); return nullptr; } if (!context()->runtime()->readPrincipals(context(), this, &principals)) return nullptr; } else if (principalsTag == SCTAG_RECONSTRUCTED_SAVED_FRAME_PRINCIPALS_IS_SYSTEM) { principals = &ReconstructedSavedFramePrincipals::IsSystem; principals->refcount++; } else if (principalsTag == SCTAG_RECONSTRUCTED_SAVED_FRAME_PRINCIPALS_IS_NOT_SYSTEM) { principals = &ReconstructedSavedFramePrincipals::IsNotSystem; principals->refcount++; } else if (principalsTag == SCTAG_NULL_JSPRINCIPALS) { principals = nullptr; } else { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "bad SavedFrame principals"); return nullptr; } savedFrame->initPrincipalsAlreadyHeld(principals); RootedValue source(context()); if (!startRead(&source) || !source.isString()) return nullptr; auto atomSource = AtomizeString(context(), source.toString()); if (!atomSource) return nullptr; savedFrame->initSource(atomSource); RootedValue lineVal(context()); uint32_t line; if (!startRead(&lineVal) || !lineVal.isNumber() || !ToUint32(context(), lineVal, &line)) return nullptr; savedFrame->initLine(line); RootedValue columnVal(context()); uint32_t column; if (!startRead(&columnVal) || !columnVal.isNumber() || !ToUint32(context(), columnVal, &column)) return nullptr; savedFrame->initColumn(column); RootedValue name(context()); if (!startRead(&name) || !(name.isString() || name.isNull())) return nullptr; JSAtom* atomName = nullptr; if (name.isString()) { atomName = AtomizeString(context(), name.toString()); if (!atomName) return nullptr; } savedFrame->initFunctionDisplayName(atomName); RootedValue cause(context()); if (!startRead(&cause) || !(cause.isString() || cause.isNull())) return nullptr; JSAtom* atomCause = nullptr; if (cause.isString()) { atomCause = AtomizeString(context(), cause.toString()); if (!atomCause) return nullptr; } savedFrame->initAsyncCause(atomCause); return savedFrame; } // Perform the whole recursive reading procedure. bool JSStructuredCloneReader::read(MutableHandleValue vp) { if (!readHeader()) return false; if (!readTransferMap()) return false; // Start out by reading in the main object and pushing it onto the 'objs' // stack. The data related to this object and its descendants extends from // here to the SCTAG_END_OF_KEYS at the end of the stream. if (!startRead(vp)) return false; // Stop when the stack shows that all objects have been read. while (objs.length() != 0) { // What happens depends on the top obj on the objs stack. RootedObject obj(context(), &objs.back().toObject()); uint32_t tag, data; if (!in.getPair(&tag, &data)) return false; if (tag == SCTAG_END_OF_KEYS) { // Pop the current obj off the stack, since we are done with it and // its children. MOZ_ALWAYS_TRUE(in.readPair(&tag, &data)); objs.popBack(); continue; } // The input stream contains a sequence of "child" values, whose // interpretation depends on the type of obj. These values can be // anything, and startRead() will push onto 'objs' for any non-leaf // value (i.e., anything that may contain children). // // startRead() will allocate the (empty) object, but note that when // startRead() returns, 'key' is not yet initialized with any of its // properties. Those will be filled in by returning to the head of this // loop, processing the first child obj, and continuing until all // children have been fully created. // // Note that this means the ordering in the stream is a little funky // for things like Map. See the comment above startWrite() for an // example. RootedValue key(context()); if (!startRead(&key)) return false; if (key.isNull() && !(obj->is() || obj->is() || obj->is())) { // Backwards compatibility: Null formerly indicated the end of // object properties. objs.popBack(); continue; } // Set object: the values between obj header (from startRead()) and // SCTAG_END_OF_KEYS are all interpreted as values to add to the set. if (obj->is()) { if (!SetObject::add(context(), obj, key)) return false; continue; } // SavedFrame object: there is one following value, the parent // SavedFrame, which is either null or another SavedFrame object. if (obj->is()) { SavedFrame* parentFrame; if (key.isNull()) parentFrame = nullptr; else if (key.isObject() && key.toObject().is()) parentFrame = &key.toObject().as(); else return false; obj->as().initParent(parentFrame); continue; } // Everything else uses a series of key,value,key,value,... Value // objects. RootedValue val(context()); if (!startRead(&val)) return false; if (obj->is()) { // For a Map, store those pairs in the contained map // data structure. if (!MapObject::set(context(), obj, key, val)) return false; } else { // For any other Object, interpret them as plain properties. RootedId id(context()); if (!ValueToId(context(), key, &id)) return false; if (!DefineProperty(context(), obj, id, val)) return false; } } allObjs.clear(); return true; } using namespace js; JS_PUBLIC_API(bool) JS_ReadStructuredClone(JSContext* cx, JSStructuredCloneData& buf, uint32_t version, JS::StructuredCloneScope scope, MutableHandleValue vp, const JSStructuredCloneCallbacks* optionalCallbacks, void* closure) { AssertHeapIsIdle(cx); CHECK_REQUEST(cx); if (version > JS_STRUCTURED_CLONE_VERSION) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SC_BAD_CLONE_VERSION); return false; } const JSStructuredCloneCallbacks* callbacks = optionalCallbacks; return ReadStructuredClone(cx, buf, scope, vp, callbacks, closure); } JS_PUBLIC_API(bool) JS_WriteStructuredClone(JSContext* cx, HandleValue value, JSStructuredCloneData* bufp, JS::StructuredCloneScope scope, JS::CloneDataPolicy cloneDataPolicy, const JSStructuredCloneCallbacks* optionalCallbacks, void* closure, HandleValue transferable) { AssertHeapIsIdle(cx); CHECK_REQUEST(cx); assertSameCompartment(cx, value); const JSStructuredCloneCallbacks* callbacks = optionalCallbacks; return WriteStructuredClone(cx, value, bufp, scope, cloneDataPolicy, callbacks, closure, transferable); } JS_PUBLIC_API(bool) JS_StructuredCloneHasTransferables(JSStructuredCloneData& data, bool* hasTransferable) { *hasTransferable = StructuredCloneHasTransferObjects(data); return true; } JS_PUBLIC_API(bool) JS_StructuredClone(JSContext* cx, HandleValue value, MutableHandleValue vp, const JSStructuredCloneCallbacks* optionalCallbacks, void* closure) { AssertHeapIsIdle(cx); CHECK_REQUEST(cx); // Strings are associated with zones, not compartments, // so we copy the string by wrapping it. if (value.isString()) { RootedString strValue(cx, value.toString()); if (!cx->compartment()->wrap(cx, &strValue)) { return false; } vp.setString(strValue); return true; } const JSStructuredCloneCallbacks* callbacks = optionalCallbacks; JSAutoStructuredCloneBuffer buf(JS::StructuredCloneScope::SameProcessSameThread, callbacks, closure); { // If we use Maybe here, G++ can't tell that the // destructor is only called when Maybe::construct was called, and // we get warnings about using uninitialized variables. if (value.isObject()) { AutoCompartment ac(cx, &value.toObject()); if (!buf.write(cx, value, callbacks, closure)) return false; } else { if (!buf.write(cx, value, callbacks, closure)) return false; } } return buf.read(cx, vp, callbacks, closure); } JSAutoStructuredCloneBuffer::JSAutoStructuredCloneBuffer(JSAutoStructuredCloneBuffer&& other) : scope_(other.scope_) { data_.ownTransferables_ = other.data_.ownTransferables_; other.steal(&data_, &version_, &data_.callbacks_, &data_.closure_); } JSAutoStructuredCloneBuffer& JSAutoStructuredCloneBuffer::operator=(JSAutoStructuredCloneBuffer&& other) { MOZ_ASSERT(&other != this); MOZ_ASSERT(scope_ == other.scope_); clear(); data_.ownTransferables_ = other.data_.ownTransferables_; other.steal(&data_, &version_, &data_.callbacks_, &data_.closure_); return *this; } void JSAutoStructuredCloneBuffer::clear(const JSStructuredCloneCallbacks* optionalCallbacks, void* optionalClosure) { if (!data_.Size()) return; const JSStructuredCloneCallbacks* callbacks = optionalCallbacks ? optionalCallbacks : data_.callbacks_; void* closure = optionalClosure ? optionalClosure : data_.closure_; if (data_.ownTransferables_ == OwnTransferablePolicy::OwnsTransferablesIfAny) DiscardTransferables(data_, callbacks, closure); data_.ownTransferables_ = OwnTransferablePolicy::NoTransferables; data_.Clear(); version_ = 0; } bool JSAutoStructuredCloneBuffer::copy(const JSStructuredCloneData& srcData, uint32_t version, const JSStructuredCloneCallbacks* callbacks, void* closure) { // transferable objects cannot be copied if (StructuredCloneHasTransferObjects(srcData)) return false; clear(); auto iter = srcData.Iter(); while (!iter.Done()) { data_.WriteBytes(iter.Data(), iter.RemainingInSegment()); iter.Advance(srcData, iter.RemainingInSegment()); } version_ = version; data_.setOptionalCallbacks(callbacks, closure, OwnTransferablePolicy::NoTransferables); return true; } void JSAutoStructuredCloneBuffer::adopt(JSStructuredCloneData&& data, uint32_t version, const JSStructuredCloneCallbacks* callbacks, void* closure) { clear(); data_ = Move(data); version_ = version; data_.setOptionalCallbacks(callbacks, closure, OwnTransferablePolicy::OwnsTransferablesIfAny); } void JSAutoStructuredCloneBuffer::steal(JSStructuredCloneData* data, uint32_t* versionp, const JSStructuredCloneCallbacks** callbacks, void** closure) { if (versionp) *versionp = version_; if (callbacks) *callbacks = data_.callbacks_; if (closure) *closure = data_.closure_; *data = Move(data_); version_ = 0; data_.setOptionalCallbacks(nullptr, nullptr, OwnTransferablePolicy::NoTransferables); } bool JSAutoStructuredCloneBuffer::read(JSContext* cx, MutableHandleValue vp, const JSStructuredCloneCallbacks* optionalCallbacks, void* closure) { MOZ_ASSERT(cx); return !!JS_ReadStructuredClone(cx, data_, version_, scope_, vp, optionalCallbacks, closure); } bool JSAutoStructuredCloneBuffer::write(JSContext* cx, HandleValue value, const JSStructuredCloneCallbacks* optionalCallbacks, void* closure) { HandleValue transferable = UndefinedHandleValue; return write(cx, value, transferable, JS::CloneDataPolicy().denySharedArrayBuffer(), optionalCallbacks, closure); } bool JSAutoStructuredCloneBuffer::write(JSContext* cx, HandleValue value, HandleValue transferable, JS::CloneDataPolicy cloneDataPolicy, const JSStructuredCloneCallbacks* optionalCallbacks, void* closure) { clear(); bool ok = JS_WriteStructuredClone(cx, value, &data_, scope_, cloneDataPolicy, optionalCallbacks, closure, transferable); if (ok) { data_.ownTransferables_ = OwnTransferablePolicy::OwnsTransferablesIfAny; } else { version_ = JS_STRUCTURED_CLONE_VERSION; data_.ownTransferables_ = OwnTransferablePolicy::NoTransferables; } return ok; } JS_PUBLIC_API(bool) JS_ReadUint32Pair(JSStructuredCloneReader* r, uint32_t* p1, uint32_t* p2) { return r->input().readPair((uint32_t*) p1, (uint32_t*) p2); } JS_PUBLIC_API(bool) JS_ReadBytes(JSStructuredCloneReader* r, void* p, size_t len) { return r->input().readBytes(p, len); } JS_PUBLIC_API(bool) JS_ReadTypedArray(JSStructuredCloneReader* r, MutableHandleValue vp) { uint32_t tag, nelems; if (!r->input().readPair(&tag, &nelems)) return false; if (tag >= SCTAG_TYPED_ARRAY_V1_MIN && tag <= SCTAG_TYPED_ARRAY_V1_MAX) { return r->readTypedArray(TagToV1ArrayType(tag), nelems, vp, true); } else if (tag == SCTAG_TYPED_ARRAY_OBJECT) { uint64_t arrayType; if (!r->input().read(&arrayType)) return false; return r->readTypedArray(arrayType, nelems, vp); } else { JS_ReportErrorNumberASCII(r->context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "expected type array"); return false; } } JS_PUBLIC_API(bool) JS_WriteUint32Pair(JSStructuredCloneWriter* w, uint32_t tag, uint32_t data) { return w->output().writePair(tag, data); } JS_PUBLIC_API(bool) JS_WriteBytes(JSStructuredCloneWriter* w, const void* p, size_t len) { return w->output().writeBytes(p, len); } JS_PUBLIC_API(bool) JS_WriteString(JSStructuredCloneWriter* w, HandleString str) { return w->writeString(SCTAG_STRING, str); } JS_PUBLIC_API(bool) JS_WriteTypedArray(JSStructuredCloneWriter* w, HandleValue v) { MOZ_ASSERT(v.isObject()); assertSameCompartment(w->context(), v); RootedObject obj(w->context(), &v.toObject()); return w->writeTypedArray(obj); } JS_PUBLIC_API(bool) JS_ObjectNotWritten(JSStructuredCloneWriter* w, HandleObject obj) { w->memory.remove(w->memory.lookup(obj)); return true; } JS_PUBLIC_API(JS::StructuredCloneScope) JS_GetStructuredCloneScope(JSStructuredCloneWriter* w) { return w->cloneScope(); }