summaryrefslogtreecommitdiffstats
path: root/js/src/vm/StructuredClone.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'js/src/vm/StructuredClone.cpp')
-rw-r--r--js/src/vm/StructuredClone.cpp4267
1 files changed, 4267 insertions, 0 deletions
diff --git a/js/src/vm/StructuredClone.cpp b/js/src/vm/StructuredClone.cpp
new file mode 100644
index 0000000000..8f1e131021
--- /dev/null
+++ b/js/src/vm/StructuredClone.cpp
@@ -0,0 +1,4267 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: set ts=8 sts=2 et sw=2 tw=80:
+ * 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 data algorithms of
+ * https://html.spec.whatwg.org/multipage/structured-data.html
+ *
+ * The spec is in two parts:
+ *
+ * - StructuredSerialize examines a JS value and produces a graph of Records.
+ * - StructuredDeserialize walks the Records and produces a new JS value.
+ *
+ * The differences between our implementation and the spec are minor:
+ *
+ * - We call the two phases "write" and "read".
+ * - Our algorithms use an explicit work stack, rather than recursion.
+ * - Serialized data is a flat array of bytes, not a (possibly cyclic) graph
+ * of "Records".
+ * - As a consequence, we handle non-treelike object graphs differently.
+ * We serialize objects that appear in multiple places in the input as
+ * backreferences, using sequential integer indexes.
+ * See `JSStructuredCloneReader::allObjs`, our take on the "memory" map
+ * in the spec's StructuredDeserialize.
+ */
+
+#include "js/StructuredClone.h"
+
+#include "mozilla/Casting.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/EndianUtils.h"
+#include "mozilla/FloatingPoint.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/ScopeExit.h"
+
+#include <algorithm>
+#include <memory>
+#include <utility>
+
+#include "jsdate.h"
+
+#include "builtin/DataViewObject.h"
+#include "builtin/MapObject.h"
+#include "gc/GC.h" // AutoSelectGCHeap
+#include "js/Array.h" // JS::GetArrayLength, JS::IsArrayObject
+#include "js/ArrayBuffer.h" // JS::{ArrayBufferHasData,DetachArrayBuffer,IsArrayBufferObject,New{,Mapped}ArrayBufferWithContents,ReleaseMappedArrayBufferContents}
+#include "js/ColumnNumber.h" // JS::ColumnNumberOneOrigin, JS::TaggedColumnNumberOneOrigin
+#include "js/Date.h"
+#include "js/experimental/TypedData.h" // JS_NewDataView, JS_New{{Ui,I}nt{8,16,32},Float{32,64},Uint8Clamped,Big{Ui,I}nt64}ArrayWithBuffer
+#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_*
+#include "js/GCAPI.h"
+#include "js/GCHashTable.h"
+#include "js/Object.h" // JS::GetBuiltinClass
+#include "js/PropertyAndElement.h" // JS_GetElement
+#include "js/RegExpFlags.h" // JS::RegExpFlag, JS::RegExpFlags
+#include "js/ScalarType.h" // js::Scalar::Type
+#include "js/SharedArrayBuffer.h" // JS::IsSharedArrayBufferObject
+#include "js/Wrapper.h"
+#include "util/DifferentialTesting.h"
+#include "vm/BigIntType.h"
+#include "vm/ErrorObject.h"
+#include "vm/JSContext.h"
+#include "vm/PlainObject.h" // js::PlainObject
+#include "vm/RegExpObject.h"
+#include "vm/SavedFrame.h"
+#include "vm/SharedArrayObject.h"
+#include "vm/TypedArrayObject.h"
+#include "wasm/WasmJS.h"
+
+#include "vm/ArrayObject-inl.h"
+#include "vm/Compartment-inl.h"
+#include "vm/ErrorObject-inl.h"
+#include "vm/InlineCharBuffer-inl.h"
+#include "vm/JSContext-inl.h"
+#include "vm/JSObject-inl.h"
+#include "vm/NativeObject-inl.h"
+#include "vm/ObjectOperations-inl.h"
+#include "vm/Realm-inl.h"
+
+using namespace js;
+
+using JS::CanonicalizeNaN;
+using JS::GetBuiltinClass;
+using JS::RegExpFlag;
+using JS::RegExpFlags;
+using JS::RootedValueVector;
+using mozilla::AssertedCast;
+using mozilla::BitwiseCast;
+using mozilla::Maybe;
+using mozilla::NativeEndian;
+using mozilla::NumbersAreIdentical;
+
+// 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_V2, // Old version, for backwards compatibility.
+ 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_V2, // Old version, for backwards compatibility.
+ SCTAG_MAP_OBJECT,
+ SCTAG_SET_OBJECT,
+ SCTAG_END_OF_KEYS,
+ SCTAG_DO_NOT_USE_3, // Required for backwards compatibility
+ SCTAG_DATA_VIEW_OBJECT_V2, // Old version, for backwards compatibility.
+ 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_SHARED_WASM_MEMORY_OBJECT,
+
+ SCTAG_BIGINT,
+ SCTAG_BIGINT_OBJECT,
+
+ SCTAG_ARRAY_BUFFER_OBJECT,
+ SCTAG_TYPED_ARRAY_OBJECT,
+ SCTAG_DATA_VIEW_OBJECT,
+
+ SCTAG_ERROR_OBJECT,
+
+ SCTAG_RESIZABLE_ARRAY_BUFFER_OBJECT,
+ SCTAG_GROWABLE_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,
+ // BigInt64 and BigUint64 are not supported in the v1 format.
+ SCTAG_TYPED_ARRAY_V1_MAX = SCTAG_TYPED_ARRAY_V1_UINT8_CLAMPED,
+
+ // Define a separate range of numbers for Transferable-only tags, since
+ // they are not used for persistent clone buffers and therefore do not
+ // require bumping JS_STRUCTURED_CLONE_VERSION.
+ SCTAG_TRANSFER_MAP_HEADER = 0xFFFF0200,
+ SCTAG_TRANSFER_MAP_PENDING_ENTRY,
+ SCTAG_TRANSFER_MAP_ARRAY_BUFFER,
+ SCTAG_TRANSFER_MAP_STORED_ARRAY_BUFFER,
+ SCTAG_TRANSFER_MAP_END_OF_BUILTIN_TYPES,
+
+ SCTAG_END_OF_BUILTIN_TYPES
+};
+
+/*
+ * Format of transfer map:
+ * <SCTAG_TRANSFER_MAP_HEADER, TransferableMapHeader(UNREAD|TRANSFERRED)>
+ * numTransferables (64 bits)
+ * array of:
+ * <SCTAG_TRANSFER_MAP_*, TransferableOwnership>
+ * 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 <typename T, typename AllocPolicy>
+struct BufferIterator {
+ using BufferList = mozilla::BufferList<AllocPolicy>;
+
+ explicit BufferIterator(const BufferList& buffer)
+ : mBuffer(buffer), mIter(buffer.Iter()) {
+ static_assert(8 % sizeof(T) == 0);
+ }
+
+ explicit BufferIterator(const JSStructuredCloneData& data)
+ : mBuffer(data.bufList_), mIter(data.Start()) {}
+
+ BufferIterator& operator=(const BufferIterator& other) {
+ MOZ_ASSERT(&mBuffer == &other.mBuffer);
+ mIter = other.mIter;
+ return *this;
+ }
+
+ [[nodiscard]] bool advance(size_t size = sizeof(T)) {
+ return mIter.AdvanceAcrossSegments(mBuffer, size);
+ }
+
+ BufferIterator operator++(int) {
+ BufferIterator ret = *this;
+ if (!advance(sizeof(T))) {
+ MOZ_ASSERT(false, "Failed to read StructuredCloneData. Data incomplete");
+ }
+ return ret;
+ }
+
+ BufferIterator& operator+=(size_t size) {
+ if (!advance(size)) {
+ MOZ_ASSERT(false, "Failed to read StructuredCloneData. Data incomplete");
+ }
+ return *this;
+ }
+
+ size_t operator-(const BufferIterator& other) const {
+ MOZ_ASSERT(&mBuffer == &other.mBuffer);
+ return mBuffer.RangeLength(other.mIter, mIter);
+ }
+
+ bool operator==(const BufferIterator& other) const {
+ return mBuffer.Start() == other.mBuffer.Start() && mIter == other.mIter;
+ }
+ bool operator!=(const BufferIterator& other) const {
+ return !(*this == other);
+ }
+
+ bool done() const { return mIter.Done(); }
+
+ [[nodiscard]] 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<T*>(mIter.Data()) = data;
+ }
+
+ T peek() const {
+ MOZ_ASSERT(mIter.HasRoomFor(sizeof(T)));
+ return *reinterpret_cast<T*>(mIter.Data());
+ }
+
+ bool canPeek() const { return mIter.HasRoomFor(sizeof(T)); }
+
+ const BufferList& mBuffer;
+ typename BufferList::IterImpl mIter;
+};
+
+SharedArrayRawBufferRefs& SharedArrayRawBufferRefs::operator=(
+ SharedArrayRawBufferRefs&& other) {
+ takeOwnership(std::move(other));
+ return *this;
+}
+
+SharedArrayRawBufferRefs::~SharedArrayRawBufferRefs() { releaseAll(); }
+
+bool SharedArrayRawBufferRefs::acquire(JSContext* cx,
+ SharedArrayRawBuffer* rawbuf) {
+ if (!refs_.append(rawbuf)) {
+ ReportOutOfMemory(cx);
+ return false;
+ }
+
+ if (!rawbuf->addReference()) {
+ refs_.popBack();
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_SC_SAB_REFCNT_OFLO);
+ return false;
+ }
+
+ return true;
+}
+
+bool SharedArrayRawBufferRefs::acquireAll(
+ JSContext* cx, const SharedArrayRawBufferRefs& that) {
+ if (!refs_.reserve(refs_.length() + that.refs_.length())) {
+ ReportOutOfMemory(cx);
+ return false;
+ }
+
+ for (auto ref : that.refs_) {
+ if (!ref->addReference()) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_SC_SAB_REFCNT_OFLO);
+ return false;
+ }
+ MOZ_ALWAYS_TRUE(refs_.append(ref));
+ }
+
+ return true;
+}
+
+void SharedArrayRawBufferRefs::takeOwnership(SharedArrayRawBufferRefs&& other) {
+ MOZ_ASSERT(refs_.empty());
+ refs_ = std::move(other.refs_);
+}
+
+void SharedArrayRawBufferRefs::releaseAll() {
+ for (auto ref : refs_) {
+ ref->dropReference();
+ }
+ refs_.clear();
+}
+
+// SCOutput provides an interface to write raw data -- eg uint64_ts, doubles,
+// arrays of bytes -- into a structured clone data output stream. It also knows
+// how to free any transferable data within that stream.
+//
+// Note that it contains a full JSStructuredCloneData object, which holds the
+// callbacks necessary to read/write/transfer/free the data. For the purpose of
+// this class, only the freeTransfer callback is relevant; the rest of the
+// callbacks are used by the higher-level JSStructuredCloneWriter interface.
+struct SCOutput {
+ public:
+ using Iter = BufferIterator<uint64_t, SystemAllocPolicy>;
+
+ SCOutput(JSContext* cx, JS::StructuredCloneScope scope);
+
+ JSContext* context() const { return cx; }
+ JS::StructuredCloneScope scope() const { return buf.scope(); }
+ void sameProcessScopeRequired() { buf.sameProcessScopeRequired(); }
+
+ [[nodiscard]] bool write(uint64_t u);
+ [[nodiscard]] bool writePair(uint32_t tag, uint32_t data);
+ [[nodiscard]] bool writeDouble(double d);
+ [[nodiscard]] bool writeBytes(const void* p, size_t nbytes);
+ [[nodiscard]] bool writeChars(const Latin1Char* p, size_t nchars);
+ [[nodiscard]] bool writeChars(const char16_t* p, size_t nchars);
+
+ template <class T>
+ [[nodiscard]] bool writeArray(const T* p, size_t nelems);
+
+ void setCallbacks(const JSStructuredCloneCallbacks* callbacks, void* closure,
+ OwnTransferablePolicy policy) {
+ buf.setCallbacks(callbacks, closure, policy);
+ }
+ void extractBuffer(JSStructuredCloneData* data) { *data = std::move(buf); }
+
+ uint64_t tell() const { return buf.Size(); }
+ uint64_t count() const { return buf.Size() / sizeof(uint64_t); }
+ Iter iter() { return Iter(buf); }
+
+ size_t offset(Iter dest) { return dest - iter(); }
+
+ JSContext* cx;
+ JSStructuredCloneData buf;
+};
+
+class SCInput {
+ public:
+ using BufferIterator = js::BufferIterator<uint64_t, SystemAllocPolicy>;
+
+ SCInput(JSContext* cx, const 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);
+
+ [[nodiscard]] bool read(uint64_t* p);
+ [[nodiscard]] bool readPair(uint32_t* tagp, uint32_t* datap);
+ [[nodiscard]] bool readDouble(double* p);
+ [[nodiscard]] bool readBytes(void* p, size_t nbytes);
+ [[nodiscard]] bool readChars(Latin1Char* p, size_t nchars);
+ [[nodiscard]] bool readChars(char16_t* p, size_t nchars);
+ [[nodiscard]] bool readPtr(void**);
+
+ [[nodiscard]] bool get(uint64_t* p);
+ [[nodiscard]] bool getPair(uint32_t* tagp, uint32_t* datap);
+
+ const BufferIterator& tell() const { return point; }
+ void seekTo(const BufferIterator& pos) { point = pos; }
+ [[nodiscard]] bool seekBy(size_t pos) {
+ if (!point.advance(pos)) {
+ reportTruncated();
+ return false;
+ }
+ return true;
+ }
+
+ template <class T>
+ [[nodiscard]] 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() {
+ static_assert(sizeof(char16_t) == 2);
+ static_assert(sizeof(uint32_t) == 4);
+ }
+
+ JSContext* cx;
+ BufferIterator point;
+};
+
+} // namespace js
+
+struct JSStructuredCloneReader {
+ public:
+ explicit JSStructuredCloneReader(SCInput& in, JS::StructuredCloneScope scope,
+ const JS::CloneDataPolicy& cloneDataPolicy,
+ const JSStructuredCloneCallbacks* cb,
+ void* cbClosure);
+
+ SCInput& input() { return in; }
+ bool read(MutableHandleValue vp, size_t nbytes);
+
+ private:
+ JSContext* context() { return in.context(); }
+
+ bool readHeader();
+ bool readTransferMap();
+
+ [[nodiscard]] bool readUint32(uint32_t* num);
+
+ enum ShouldAtomizeStrings : bool {
+ DontAtomizeStrings = false,
+ AtomizeStrings = true
+ };
+
+ template <typename CharT>
+ JSString* readStringImpl(uint32_t nchars, ShouldAtomizeStrings atomize);
+ JSString* readString(uint32_t data, ShouldAtomizeStrings atomize);
+
+ BigInt* readBigInt(uint32_t data);
+
+ [[nodiscard]] bool readTypedArray(uint32_t arrayType, uint64_t nelems,
+ MutableHandleValue vp, bool v1Read = false);
+
+ [[nodiscard]] bool readDataView(uint64_t byteLength, MutableHandleValue vp);
+
+ [[nodiscard]] bool readArrayBuffer(StructuredDataType type, uint32_t data,
+ MutableHandleValue vp);
+ [[nodiscard]] bool readV1ArrayBuffer(uint32_t arrayType, uint32_t nelems,
+ MutableHandleValue vp);
+
+ [[nodiscard]] bool readSharedArrayBuffer(StructuredDataType type,
+ MutableHandleValue vp);
+
+ [[nodiscard]] bool readSharedWasmMemory(uint32_t nbytes,
+ MutableHandleValue vp);
+
+ // A serialized SavedFrame contains primitive values in a header followed by
+ // an optional parent frame that is read recursively.
+ [[nodiscard]] JSObject* readSavedFrameHeader(uint32_t principalsTag);
+ [[nodiscard]] bool readSavedFrameFields(Handle<SavedFrame*> frameObj,
+ HandleValue parent, bool* state);
+
+ // A serialized Error contains primitive values in a header followed by
+ // 'cause', 'errors', and 'stack' fields that are read recursively.
+ [[nodiscard]] JSObject* readErrorHeader(uint32_t type);
+ [[nodiscard]] bool readErrorFields(Handle<ErrorObject*> errorObj,
+ HandleValue cause, bool* state);
+
+ [[nodiscard]] bool readMapField(Handle<MapObject*> mapObj, HandleValue key);
+
+ [[nodiscard]] bool readObjectField(HandleObject obj, HandleValue key);
+
+ [[nodiscard]] bool startRead(
+ MutableHandleValue vp,
+ ShouldAtomizeStrings atomizeStrings = DontAtomizeStrings);
+
+ SCInput& in;
+
+ // The widest scope that the caller will accept, where
+ // SameProcess 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;
+
+ const JS::CloneDataPolicy cloneDataPolicy;
+
+ // Stack of objects with properties remaining to be read.
+ RootedValueVector objs;
+
+ // Maintain a stack of state values for the `objs` stack. Since this is only
+ // needed for a very small subset of objects (those with a known set of
+ // object children), the state information is stored as a stack of
+ // <object, state> pairs where the object determines which element of the
+ // `objs` stack that it corresponds to. So when reading from the `objs` stack,
+ // the state will be retrieved only if the top object on `objState` matches
+ // the top object of `objs`.
+ //
+ // Currently, the only state needed is a boolean indicating whether the fields
+ // have been read yet.
+ Rooted<GCVector<std::pair<HeapPtr<JSObject*>, bool>, 8>> objState;
+
+ // Array of all objects read during this deserialization, for resolving
+ // backreferences.
+ //
+ // For backreferences to work correctly, objects must be added to this
+ // array in exactly the order expected by the version of the Writer that
+ // created the serialized data, even across years and format versions. This
+ // is usually no problem, since both algorithms do a single linear pass
+ // over the serialized data. There is one hitch; see readTypedArray.
+ //
+ // The values in this vector are objects, except it can temporarily have
+ // one `undefined` placeholder value (the readTypedArray hack).
+ RootedValueVector allObjs;
+
+ size_t numItemsRead;
+
+ // The user defined callbacks that will be used for cloning.
+ const JSStructuredCloneCallbacks* callbacks;
+
+ // Any value passed to JS_ReadStructuredClone.
+ void* closure;
+
+ // The heap to use for allocating common GC things. This starts out as the
+ // nursery (the default) but may switch to the tenured heap if nursery
+ // collection occurs, as nursery allocation is pointless after the
+ // deserialized root object is tenured.
+ //
+ // This is only used for the most common kind, e.g. plain objects, strings
+ // and a couple of others.
+ AutoSelectGCHeap gcHeap;
+
+ friend bool JS_ReadString(JSStructuredCloneReader* r,
+ JS::MutableHandleString str);
+ friend bool JS_ReadTypedArray(JSStructuredCloneReader* r,
+ MutableHandleValue vp);
+
+ // Provide a way to detect whether any of the clone data is never used. When
+ // "tail" data (currently, this is only stored data for Transferred
+ // ArrayBuffers in the DifferentProcess scope) is read, record the first and
+ // last positions. At the end of deserialization, make sure there's nothing
+ // between the end of the main data and the beginning of the tail, nor after
+ // the end of the tail.
+ mozilla::Maybe<SCInput::BufferIterator> tailStartPos;
+ mozilla::Maybe<SCInput::BufferIterator> tailEndPos;
+};
+
+struct JSStructuredCloneWriter {
+ public:
+ explicit JSStructuredCloneWriter(JSContext* cx,
+ JS::StructuredCloneScope scope,
+ const JS::CloneDataPolicy& cloneDataPolicy,
+ const JSStructuredCloneCallbacks* cb,
+ void* cbClosure, const Value& tVal)
+ : out(cx, scope),
+ callbacks(cb),
+ closure(cbClosure),
+ objs(cx),
+ counts(cx),
+ objectEntries(cx),
+ otherEntries(cx),
+ memory(cx),
+ transferable(cx, tVal),
+ transferableObjects(cx, TransferableObjectsList(cx)),
+ cloneDataPolicy(cloneDataPolicy) {
+ out.setCallbacks(cb, cbClosure,
+ OwnTransferablePolicy::OwnsTransferablesIfAny);
+ }
+
+ bool init() {
+ return parseTransferable() && writeHeader() && writeTransferMap();
+ }
+
+ bool write(HandleValue v);
+
+ SCOutput& output() { return out; }
+
+ void extractBuffer(JSStructuredCloneData* newData) {
+ out.extractBuffer(newData);
+ }
+
+ private:
+ JSStructuredCloneWriter() = delete;
+ JSStructuredCloneWriter(const JSStructuredCloneWriter&) = delete;
+
+ JSContext* context() { return out.context(); }
+
+ bool writeHeader();
+ bool writeTransferMap();
+
+ bool writeString(uint32_t tag, JSString* str);
+ bool writeBigInt(uint32_t tag, BigInt* bi);
+ bool writeArrayBuffer(HandleObject obj);
+ bool writeTypedArray(HandleObject obj);
+ bool writeDataView(HandleObject obj);
+ bool writeSharedArrayBuffer(HandleObject obj);
+ bool writeSharedWasmMemory(HandleObject obj);
+ bool startObject(HandleObject obj, bool* backref);
+ bool writePrimitive(HandleValue v);
+ bool startWrite(HandleValue v);
+ bool traverseObject(HandleObject obj, ESClass cls);
+ bool traverseMap(HandleObject obj);
+ bool traverseSet(HandleObject obj);
+ bool traverseSavedFrame(HandleObject obj);
+ bool traverseError(HandleObject obj);
+
+ template <typename... Args>
+ bool reportDataCloneError(uint32_t errorId, Args&&... aArgs);
+
+ bool parseTransferable();
+ bool transferOwnership();
+
+ inline void checkStack();
+
+ SCOutput out;
+
+ // The user defined callbacks that will be used to signal cloning, in some
+ // cases.
+ const JSStructuredCloneCallbacks* callbacks;
+
+ // Any value passed to the callbacks.
+ void* closure;
+
+ // 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.
+ RootedValueVector 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<size_t> counts;
+
+ // For JSObject: Property IDs as value
+ RootedIdVector objectEntries;
+
+ // For Map: Key followed by value
+ // For Set: Key
+ // For SavedFrame: parent SavedFrame
+ // For Error: cause, errors, stack
+ RootedValueVector otherEntries;
+
+ // 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<JSObject*, uint32_t,
+ StableCellHasher<JSObject*>, SystemAllocPolicy>;
+ Rooted<CloneMemory> memory;
+
+ // Set of transferable objects
+ RootedValue transferable;
+ using TransferableObjectsList = GCVector<JSObject*>;
+ Rooted<TransferableObjectsList> 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_PUBLIC_API uint64_t js::GetSCOffset(JSStructuredCloneWriter* writer) {
+ MOZ_ASSERT(writer);
+ return writer->output().count() * sizeof(uint64_t);
+}
+
+static_assert(SCTAG_END_OF_BUILTIN_TYPES <= JS_SCTAG_USER_MIN);
+static_assert(JS_SCTAG_USER_MIN <= JS_SCTAG_USER_MAX);
+static_assert(Scalar::Int8 == 0);
+
+template <typename... Args>
+static void ReportDataCloneError(JSContext* cx,
+ const JSStructuredCloneCallbacks* callbacks,
+ uint32_t errorId, void* closure,
+ Args&&... aArgs) {
+ unsigned errorNumber;
+ switch (errorId) {
+ case JS_SCERR_DUP_TRANSFERABLE:
+ errorNumber = JSMSG_SC_DUP_TRANSFERABLE;
+ break;
+
+ case JS_SCERR_TRANSFERABLE:
+ errorNumber = JSMSG_SC_NOT_TRANSFERABLE;
+ break;
+
+ case JS_SCERR_UNSUPPORTED_TYPE:
+ errorNumber = JSMSG_SC_UNSUPPORTED_TYPE;
+ break;
+
+ case JS_SCERR_SHMEM_TRANSFERABLE:
+ errorNumber = JSMSG_SC_SHMEM_TRANSFERABLE;
+ break;
+
+ case JS_SCERR_TYPED_ARRAY_DETACHED:
+ errorNumber = JSMSG_TYPED_ARRAY_DETACHED;
+ break;
+
+ case JS_SCERR_WASM_NO_TRANSFER:
+ errorNumber = JSMSG_WASM_NO_TRANSFER;
+ break;
+
+ case JS_SCERR_NOT_CLONABLE:
+ errorNumber = JSMSG_SC_NOT_CLONABLE;
+ break;
+
+ case JS_SCERR_NOT_CLONABLE_WITH_COOP_COEP:
+ errorNumber = JSMSG_SC_NOT_CLONABLE_WITH_COOP_COEP;
+ break;
+
+ default:
+ MOZ_CRASH("Unkown errorId");
+ break;
+ }
+
+ if (callbacks && callbacks->reportError) {
+ MOZ_RELEASE_ASSERT(!cx->isExceptionPending());
+
+ JSErrorReport report;
+ report.errorNumber = errorNumber;
+ // Get js error message if it's possible and propagate it through callback.
+ if (JS_ExpandErrorArgumentsASCII(cx, GetErrorMessage, errorNumber, &report,
+ std::forward<Args>(aArgs)...) &&
+ report.message()) {
+ callbacks->reportError(cx, errorId, closure, report.message().c_str());
+ } else {
+ ReportOutOfMemory(cx);
+
+ callbacks->reportError(cx, errorId, closure, "");
+ }
+
+ return;
+ }
+
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, errorNumber,
+ std::forward<Args>(aArgs)...);
+}
+
+bool WriteStructuredClone(JSContext* cx, HandleValue v,
+ JSStructuredCloneData* bufp,
+ JS::StructuredCloneScope scope,
+ const JS::CloneDataPolicy& cloneDataPolicy,
+ const JSStructuredCloneCallbacks* cb, void* cbClosure,
+ const Value& transferable) {
+ JSStructuredCloneWriter w(cx, scope, cloneDataPolicy, cb, cbClosure,
+ transferable);
+ if (!w.init()) {
+ return false;
+ }
+ if (!w.write(v)) {
+ return false;
+ }
+ w.extractBuffer(bufp);
+ return true;
+}
+
+bool ReadStructuredClone(JSContext* cx, const JSStructuredCloneData& data,
+ JS::StructuredCloneScope scope, MutableHandleValue vp,
+ const JS::CloneDataPolicy& cloneDataPolicy,
+ const JSStructuredCloneCallbacks* cb,
+ void* cbClosure) {
+ if (data.Size() % 8) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_SC_BAD_SERIALIZED_DATA, "misaligned");
+ return false;
+ }
+ SCInput in(cx, data);
+ JSStructuredCloneReader r(in, scope, cloneDataPolicy, cb, cbClosure);
+ return r.read(vp, data.Size());
+}
+
+static bool StructuredCloneHasTransferObjects(
+ const JSStructuredCloneData& data) {
+ if (data.Size() < sizeof(uint64_t)) {
+ return false;
+ }
+
+ uint64_t u;
+ BufferIterator<uint64_t, SystemAllocPolicy> iter(data);
+ MOZ_ALWAYS_TRUE(iter.readBytes(reinterpret_cast<char*>(&u), sizeof(u)));
+ uint32_t tag = uint32_t(u >> 32);
+ return (tag == SCTAG_TRANSFER_MAP_HEADER);
+}
+
+namespace js {
+
+SCInput::SCInput(JSContext* cx, const JSStructuredCloneData& data)
+ : cx(cx), point(data) {
+ static_assert(JSStructuredCloneData::BufferList::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());
+ MOZ_ALWAYS_TRUE(point.advance());
+ 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) {
+ uint64_t u;
+ if (!read(&u)) {
+ return false;
+ }
+ *p = CanonicalizeNaN(mozilla::BitwiseCast<double>(u));
+ return true;
+}
+
+template <typename T>
+static void swapFromLittleEndianInPlace(T* ptr, size_t nelems) {
+ if (nelems > 0) {
+ NativeEndian::swapFromLittleEndianInPlace(ptr, nelems);
+ }
+}
+
+template <>
+void swapFromLittleEndianInPlace(uint8_t* ptr, size_t nelems) {}
+
+// Data is packed into an integral number of uint64_t words. Compute the
+// padding required to finish off the final word.
+static size_t ComputePadding(size_t nelems, size_t elemSize) {
+ // We want total length mod 8, where total length is nelems * sizeof(T),
+ // but that might overflow. So reduce nelems to nelems mod 8, since we are
+ // going to be doing a mod 8 later anyway.
+ size_t leftoverLength = (nelems % sizeof(uint64_t)) * elemSize;
+ return (-leftoverLength) & (sizeof(uint64_t) - 1);
+}
+
+template <class T>
+bool SCInput::readArray(T* p, size_t nelems) {
+ if (!nelems) {
+ return true;
+ }
+
+ static_assert(sizeof(uint64_t) % sizeof(T) == 0);
+
+ // Fail if nelems is so huge that computing the full size will overflow.
+ mozilla::CheckedInt<size_t> size =
+ mozilla::CheckedInt<size_t>(nelems) * sizeof(T);
+ if (!size.isValid()) {
+ return reportTruncated();
+ }
+
+ if (!point.readBytes(reinterpret_cast<char*>(p), size.value())) {
+ // To avoid any way in which uninitialized data could escape, zero the array
+ // if filling it failed.
+ std::uninitialized_fill_n(p, nelems, 0);
+ return false;
+ }
+
+ swapFromLittleEndianInPlace(p, nelems);
+
+ point += ComputePadding(nelems, sizeof(T));
+
+ 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) {
+ *ptr = reinterpret_cast<void*>(NativeEndian::swapFromLittleEndian(data));
+}
+
+bool SCInput::readPtr(void** p) {
+ uint64_t u;
+ if (!read(&u)) {
+ return false;
+ }
+ *p = reinterpret_cast<void*>(u);
+ return true;
+}
+
+SCOutput::SCOutput(JSContext* cx, JS::StructuredCloneScope scope)
+ : cx(cx), buf(scope) {}
+
+bool SCOutput::write(uint64_t u) {
+ uint64_t v = NativeEndian::swapToLittleEndian(u);
+ if (!buf.AppendBytes(reinterpret_cast<char*>(&v), sizeof(u))) {
+ ReportOutOfMemory(context());
+ return false;
+ }
+ return true;
+}
+
+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<double>(PairToUInt64(tag, data));
+}
+
+bool SCOutput::writeDouble(double d) {
+ return write(BitwiseCast<uint64_t>(CanonicalizeNaN(d)));
+}
+
+template <class T>
+bool SCOutput::writeArray(const T* p, size_t nelems) {
+ static_assert(8 % sizeof(T) == 0);
+ static_assert(sizeof(uint64_t) % sizeof(T) == 0);
+
+ if (nelems == 0) {
+ return true;
+ }
+
+ for (size_t i = 0; i < nelems; i++) {
+ T value = NativeEndian::swapToLittleEndian(p[i]);
+ if (!buf.AppendBytes(reinterpret_cast<char*>(&value), sizeof(value))) {
+ return false;
+ }
+ }
+
+ // Zero-pad to 8 bytes boundary.
+ size_t padbytes = ComputePadding(nelems, sizeof(T));
+ char zeroes[sizeof(uint64_t)] = {0};
+ if (!buf.AppendBytes(zeroes, padbytes)) {
+ return false;
+ }
+
+ return true;
+}
+
+template <>
+bool SCOutput::writeArray<uint8_t>(const uint8_t* p, size_t nelems) {
+ if (nelems == 0) {
+ return true;
+ }
+
+ if (!buf.AppendBytes(reinterpret_cast<const char*>(p), nelems)) {
+ return false;
+ }
+
+ // zero-pad to 8 bytes boundary
+ size_t padbytes = ComputePadding(nelems, 1);
+ char zeroes[sizeof(uint64_t)] = {0};
+ if (!buf.AppendBytes(zeroes, padbytes)) {
+ 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);
+}
+
+} // namespace js
+
+JSStructuredCloneData::~JSStructuredCloneData() { discardTransferables(); }
+
+// If the buffer contains Transferables, free them. Note that custom
+// Transferables will use the JSStructuredCloneCallbacks::freeTransfer() to
+// delete their transferables.
+void JSStructuredCloneData::discardTransferables() {
+ if (!Size()) {
+ return;
+ }
+
+ if (ownTransferables_ != OwnTransferablePolicy::OwnsTransferablesIfAny) {
+ return;
+ }
+
+ // DifferentProcess clones cannot contain pointers, so nothing needs to be
+ // released.
+ if (scope() == JS::StructuredCloneScope::DifferentProcess) {
+ return;
+ }
+
+ FreeTransferStructuredCloneOp freeTransfer = nullptr;
+ if (callbacks_) {
+ freeTransfer = callbacks_->freeTransfer;
+ }
+
+ auto point = BufferIterator<uint64_t, SystemAllocPolicy>(*this);
+ if (point.done()) {
+ return; // Empty buffer
+ }
+
+ uint32_t tag, data;
+ MOZ_RELEASE_ASSERT(point.canPeek());
+ SCInput::getPair(point.peek(), &tag, &data);
+ MOZ_ALWAYS_TRUE(point.advance());
+
+ if (tag == SCTAG_HEADER) {
+ if (point.done()) {
+ return;
+ }
+
+ MOZ_RELEASE_ASSERT(point.canPeek());
+ SCInput::getPair(point.peek(), &tag, &data);
+ MOZ_ALWAYS_TRUE(point.advance());
+ }
+
+ 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;
+ }
+
+ MOZ_RELEASE_ASSERT(point.canPeek());
+ uint64_t numTransferables = NativeEndian::swapFromLittleEndian(point.peek());
+ MOZ_ALWAYS_TRUE(point.advance());
+ while (numTransferables--) {
+ if (!point.canPeek()) {
+ return;
+ }
+
+ uint32_t ownership;
+ SCInput::getPair(point.peek(), &tag, &ownership);
+ MOZ_ALWAYS_TRUE(point.advance());
+ MOZ_ASSERT(tag >= SCTAG_TRANSFER_MAP_PENDING_ENTRY);
+ if (!point.canPeek()) {
+ return;
+ }
+
+ void* content;
+ SCInput::getPtr(point.peek(), &content);
+ MOZ_ALWAYS_TRUE(point.advance());
+ if (!point.canPeek()) {
+ return;
+ }
+
+ uint64_t extraData = NativeEndian::swapFromLittleEndian(point.peek());
+ MOZ_ALWAYS_TRUE(point.advance());
+
+ 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 (freeTransfer) {
+ freeTransfer(tag, JS::TransferableOwnership(ownership), content,
+ extraData, closure_);
+ } else {
+ MOZ_ASSERT(false, "unknown ownership");
+ }
+ }
+}
+
+static_assert(JSString::MAX_LENGTH < UINT32_MAX);
+
+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.empty(),
+ "parseTransferable called with stale data");
+
+ if (transferable.isNull() || transferable.isUndefined()) {
+ return true;
+ }
+
+ 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.reserve(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();
+
+ RootedObject unwrappedObj(cx, CheckedUnwrapStatic(tObj));
+ if (!unwrappedObj) {
+ ReportAccessDenied(cx);
+ return false;
+ }
+
+ // Shared memory cannot be transferred because it is not possible (nor
+ // desirable) to detach the memory in agents that already hold a
+ // reference to it.
+
+ if (unwrappedObj->is<SharedArrayBufferObject>()) {
+ return reportDataCloneError(JS_SCERR_SHMEM_TRANSFERABLE);
+ }
+
+ else if (unwrappedObj->is<WasmMemoryObject>()) {
+ if (unwrappedObj->as<WasmMemoryObject>().isShared()) {
+ return reportDataCloneError(JS_SCERR_SHMEM_TRANSFERABLE);
+ }
+ }
+
+ // External array buffers may be able to be transferred in the future,
+ // but that is not currently implemented.
+
+ else if (unwrappedObj->is<ArrayBufferObject>()) {
+ if (unwrappedObj->as<ArrayBufferObject>().isExternal()) {
+ return reportDataCloneError(JS_SCERR_TRANSFERABLE);
+ }
+ }
+
+ else {
+ if (!out.buf.callbacks_ || !out.buf.callbacks_->canTransfer) {
+ return reportDataCloneError(JS_SCERR_TRANSFERABLE);
+ }
+
+ JSAutoRealm ar(cx, unwrappedObj);
+ bool sameProcessScopeRequired = false;
+ if (!out.buf.callbacks_->canTransfer(
+ cx, unwrappedObj, &sameProcessScopeRequired, out.buf.closure_)) {
+ return reportDataCloneError(JS_SCERR_TRANSFERABLE);
+ }
+
+ if (sameProcessScopeRequired) {
+ output().sameProcessScopeRequired();
+ }
+ }
+
+ // No duplicates allowed
+ if (std::find(transferableObjects.begin(), transferableObjects.end(),
+ tObj) != transferableObjects.end()) {
+ return reportDataCloneError(JS_SCERR_DUP_TRANSFERABLE);
+ }
+
+ if (!transferableObjects.append(tObj)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+template <typename... Args>
+bool JSStructuredCloneWriter::reportDataCloneError(uint32_t errorId,
+ Args&&... aArgs) {
+ ReportDataCloneError(context(), out.buf.callbacks_, errorId, out.buf.closure_,
+ std::forward<Args>(aArgs)...);
+ return false;
+}
+
+bool JSStructuredCloneWriter::writeString(uint32_t tag, JSString* str) {
+ JSLinearString* linear = str->ensureLinear(context());
+ if (!linear) {
+ return false;
+ }
+
+#if FUZZING_JS_FUZZILLI
+ if (js::SupportDifferentialTesting()) {
+ // TODO we could always output a twoByteChar string
+ return true;
+ }
+#endif
+
+ 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);
+}
+
+bool JSStructuredCloneWriter::writeBigInt(uint32_t tag, BigInt* bi) {
+ bool signBit = bi->isNegative();
+ size_t length = bi->digitLength();
+ // The length must fit in 31 bits to leave room for a sign bit.
+ if (length > size_t(INT32_MAX)) {
+ return false;
+ }
+ uint32_t lengthAndSign = length | (static_cast<uint32_t>(signBit) << 31);
+
+ if (!out.writePair(tag, lengthAndSign)) {
+ return false;
+ }
+ return out.writeArray(bi->digits().data(), 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 = std::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 == objectEntries.length() + otherEntries.length());
+ } else {
+ MOZ_ASSERT(total <= objectEntries.length() + otherEntries.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<TypedArrayObject*> tarr(context(),
+ obj->maybeUnwrapAs<TypedArrayObject>());
+ JSAutoRealm ar(context(), tarr);
+
+#ifdef FUZZING_JS_FUZZILLI
+ if (js::SupportDifferentialTesting() && !tarr->hasBuffer()) {
+ // fake oom because differential testing will fail
+ fprintf(stderr, "[unhandlable oom]");
+ _exit(-1);
+ return false;
+ }
+#endif
+
+ if (!TypedArrayObject::ensureHasBuffer(context(), tarr)) {
+ return false;
+ }
+
+ if (!out.writePair(SCTAG_TYPED_ARRAY_OBJECT, uint32_t(tarr->type()))) {
+ return false;
+ }
+
+ mozilla::Maybe<size_t> nelems = tarr->length();
+ if (!nelems) {
+ return reportDataCloneError(JS_SCERR_TYPED_ARRAY_DETACHED);
+ }
+
+ // Auto-length TypedArrays are tagged by storing `-1` for the length. We still
+ // need to query the length to check for detached or out-of-bounds lengths.
+ bool isAutoLength = tarr->is<ResizableTypedArrayObject>() &&
+ tarr->as<ResizableTypedArrayObject>().isAutoLength();
+ uint64_t length = isAutoLength ? uint64_t(-1) : uint64_t(*nelems);
+ if (!out.write(length)) {
+ return false;
+ }
+
+ // Write out the ArrayBuffer tag and contents
+ RootedValue val(context(), tarr->bufferValue());
+ if (!startWrite(val)) {
+ return false;
+ }
+
+ uint64_t byteOffset = tarr->byteOffset().valueOr(0);
+ return out.write(byteOffset);
+}
+
+bool JSStructuredCloneWriter::writeDataView(HandleObject obj) {
+ Rooted<DataViewObject*> view(context(), obj->maybeUnwrapAs<DataViewObject>());
+ JSAutoRealm ar(context(), view);
+
+ if (!out.writePair(SCTAG_DATA_VIEW_OBJECT, 0)) {
+ return false;
+ }
+
+ mozilla::Maybe<size_t> byteLength = view->byteLength();
+ if (!byteLength) {
+ return reportDataCloneError(JS_SCERR_TYPED_ARRAY_DETACHED);
+ }
+
+ // Auto-length DataViews are tagged by storing `-1` for the length. We still
+ // need to query the length to check for detached or out-of-bounds lengths.
+ bool isAutoLength = view->is<ResizableDataViewObject>() &&
+ view->as<ResizableDataViewObject>().isAutoLength();
+ uint64_t length = isAutoLength ? uint64_t(-1) : uint64_t(*byteLength);
+ if (!out.write(length)) {
+ return false;
+ }
+
+ // Write out the ArrayBuffer tag and contents
+ RootedValue val(context(), view->bufferValue());
+ if (!startWrite(val)) {
+ return false;
+ }
+
+ uint64_t byteOffset = view->byteOffset().valueOr(0);
+ return out.write(byteOffset);
+}
+
+bool JSStructuredCloneWriter::writeArrayBuffer(HandleObject obj) {
+ Rooted<ArrayBufferObject*> buffer(context(),
+ obj->maybeUnwrapAs<ArrayBufferObject>());
+ JSAutoRealm ar(context(), buffer);
+
+ StructuredDataType type = !buffer->isResizable()
+ ? SCTAG_ARRAY_BUFFER_OBJECT
+ : SCTAG_RESIZABLE_ARRAY_BUFFER_OBJECT;
+
+ if (!out.writePair(type, 0)) {
+ return false;
+ }
+
+ uint64_t byteLength = buffer->byteLength();
+ if (!out.write(byteLength)) {
+ return false;
+ }
+
+ if (buffer->isResizable()) {
+ uint64_t maxByteLength =
+ buffer->as<ResizableArrayBufferObject>().maxByteLength();
+ if (!out.write(maxByteLength)) {
+ return false;
+ }
+ }
+
+ return out.writeBytes(buffer->dataPointer(), byteLength);
+}
+
+bool JSStructuredCloneWriter::writeSharedArrayBuffer(HandleObject obj) {
+ MOZ_ASSERT(obj->canUnwrapAs<SharedArrayBufferObject>());
+
+ if (!cloneDataPolicy.areSharedMemoryObjectsAllowed()) {
+ auto error = context()->realm()->creationOptions().getCoopAndCoepEnabled()
+ ? JS_SCERR_NOT_CLONABLE_WITH_COOP_COEP
+ : JS_SCERR_NOT_CLONABLE;
+ reportDataCloneError(error, "SharedArrayBuffer");
+ return false;
+ }
+
+ output().sameProcessScopeRequired();
+
+ // We must not transmit SAB pointers (including for WebAssembly.Memory)
+ // cross-process. The cloneDataPolicy should have guarded against this;
+ // since it did not then throw, with a very explicit message.
+
+ if (output().scope() > JS::StructuredCloneScope::SameProcess) {
+ JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr,
+ JSMSG_SC_SHMEM_POLICY);
+ return false;
+ }
+
+ Rooted<SharedArrayBufferObject*> sharedArrayBuffer(
+ context(), obj->maybeUnwrapAs<SharedArrayBufferObject>());
+ SharedArrayRawBuffer* rawbuf = sharedArrayBuffer->rawBufferObject();
+
+ if (!out.buf.refsHeld_.acquire(context(), rawbuf)) {
+ return false;
+ }
+
+ // We must serialize the length so that the buffer object arrives in the
+ // receiver with the same length, and not with the length read from the
+ // rawbuf - that length can be different, and it can change at any time.
+
+ StructuredDataType type = !sharedArrayBuffer->isGrowable()
+ ? SCTAG_SHARED_ARRAY_BUFFER_OBJECT
+ : SCTAG_GROWABLE_SHARED_ARRAY_BUFFER_OBJECT;
+
+ intptr_t p = reinterpret_cast<intptr_t>(rawbuf);
+ uint64_t byteLength = sharedArrayBuffer->byteLengthOrMaxByteLength();
+ if (!(out.writePair(type, /* unused data word */ 0) &&
+ out.writeBytes(&byteLength, sizeof(byteLength)) &&
+ out.writeBytes(&p, sizeof(p)))) {
+ return false;
+ }
+
+ if (callbacks && callbacks->sabCloned &&
+ !callbacks->sabCloned(context(), /*receiving=*/false, closure)) {
+ return false;
+ }
+
+ return true;
+}
+
+bool JSStructuredCloneWriter::writeSharedWasmMemory(HandleObject obj) {
+ MOZ_ASSERT(obj->canUnwrapAs<WasmMemoryObject>());
+
+ // Check the policy here so that we can report a sane error.
+ if (!cloneDataPolicy.areSharedMemoryObjectsAllowed()) {
+ auto error = context()->realm()->creationOptions().getCoopAndCoepEnabled()
+ ? JS_SCERR_NOT_CLONABLE_WITH_COOP_COEP
+ : JS_SCERR_NOT_CLONABLE;
+ reportDataCloneError(error, "WebAssembly.Memory");
+ return false;
+ }
+
+ // If this changes, might need to change what we write.
+ MOZ_ASSERT(WasmMemoryObject::RESERVED_SLOTS == 3);
+
+ Rooted<WasmMemoryObject*> memoryObj(context(),
+ &obj->unwrapAs<WasmMemoryObject>());
+ Rooted<SharedArrayBufferObject*> sab(
+ context(), &memoryObj->buffer().as<SharedArrayBufferObject>());
+
+ return out.writePair(SCTAG_SHARED_WASM_MEMORY_OBJECT, 0) &&
+ out.writePair(SCTAG_BOOLEAN, memoryObj->isHuge()) &&
+ writeSharedArrayBuffer(sab);
+}
+
+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;
+}
+
+static bool TryAppendNativeProperties(JSContext* cx, HandleObject obj,
+ MutableHandleIdVector entries,
+ size_t* properties, bool* optimized) {
+ *optimized = false;
+
+ if (!obj->is<NativeObject>()) {
+ return true;
+ }
+
+ Handle<NativeObject*> nobj = obj.as<NativeObject>();
+ if (nobj->isIndexed() || nobj->is<TypedArrayObject>() ||
+ nobj->getClass()->getNewEnumerate() || nobj->getClass()->getEnumerate()) {
+ return true;
+ }
+
+ *optimized = true;
+
+ size_t count = 0;
+ // We iterate from the last to the first property, so the property names
+ // are already in reverse order.
+ for (ShapePropertyIter<NoGC> iter(nobj->shape()); !iter.done(); iter++) {
+ jsid id = iter->key();
+
+ // Ignore symbols and non-enumerable properties.
+ if (!iter->enumerable() || id.isSymbol()) {
+ continue;
+ }
+
+ MOZ_ASSERT(id.isString());
+ if (!entries.append(id)) {
+ return false;
+ }
+
+ count++;
+ }
+
+ // Add dense element ids in reverse order.
+ for (uint32_t i = nobj->getDenseInitializedLength(); i > 0; --i) {
+ if (nobj->getDenseElement(i - 1).isMagic(JS_ELEMENTS_HOLE)) {
+ continue;
+ }
+
+ if (!entries.append(PropertyKey::Int(i - 1))) {
+ return false;
+ }
+
+ count++;
+ }
+
+ *properties = count;
+ return true;
+}
+
+// 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
+//
+// obj1 = { key1: { key1.1: val1.1, key1.2: val1.2 }, key2: {} }
+//
+// would be stored as:
+//
+// <Object tag for obj1>
+// <key1 data>
+// <Object tag for key1's value>
+// <key1.1 data>
+// <val1.1 data>
+// <key1.2 data>
+// <val1.2 data>
+// <end-of-children marker for key1's value>
+// <key2 data>
+// <Object tag for key2's value>
+// <end-of-children marker for key2's value>
+// <end-of-children marker for obj1>
+//
+// This nests nicely (ie, an entire recursive value starts with its tag and
+// ends with its end-of-children marker) and so it can be presented indented.
+// But see traverseMap below for how this looks different for Maps.
+bool JSStructuredCloneWriter::traverseObject(HandleObject obj, ESClass cls) {
+ size_t count;
+ bool optimized = false;
+ if (!js::SupportDifferentialTesting()) {
+ if (!TryAppendNativeProperties(context(), obj, &objectEntries, &count,
+ &optimized)) {
+ return false;
+ }
+ }
+
+ if (!optimized) {
+ // Get enumerable property ids and put them in reverse order so that they
+ // will come off the stack in forward order.
+ RootedIdVector properties(context());
+ if (!GetPropertyKeys(context(), obj, JSITER_OWNONLY, &properties)) {
+ return false;
+ }
+
+ for (size_t i = properties.length(); i > 0; --i) {
+ jsid id = properties[i - 1];
+
+ MOZ_ASSERT(id.isString() || id.isInt());
+ if (!objectEntries.append(id)) {
+ return false;
+ }
+ }
+
+ count = properties.length();
+ }
+
+ // Push obj and count to the stack.
+ if (!objs.append(ObjectValue(*obj)) || !counts.append(count)) {
+ return false;
+ }
+
+ checkStack();
+
+#if DEBUG
+ ESClass cls2;
+ if (!GetBuiltinClass(context(), obj, &cls2)) {
+ return false;
+ }
+ MOZ_ASSERT(cls2 == cls);
+#endif
+
+ // Write the header for obj.
+ if (cls == ESClass::Array) {
+ uint32_t length = 0;
+ if (!JS::GetArrayLength(context(), obj, &length)) {
+ return false;
+ }
+
+ return out.writePair(SCTAG_ARRAY_OBJECT,
+ NativeEndian::swapToLittleEndian(length));
+ }
+
+ return out.writePair(SCTAG_OBJECT_OBJECT, 0);
+}
+
+// Use the same basic setup as for traverseObject, but now keys can themselves
+// be complex objects. Keys and values are visited first via startWrite(), then
+// the key's children (if any) are handled, then the value's children.
+//
+// m = new Map();
+// m.set(key1 = ..., value1 = ...)
+//
+// where key1 and value2 are both objects would be stored as
+//
+// <Map tag>
+// <key1 class tag>
+// <value1 class tag>
+// ...key1 fields...
+// <end-of-children marker for key1>
+// ...value1 fields...
+// <end-of-children marker for value1>
+// <end-of-children marker for Map>
+//
+// Notice how the end-of-children marker for key1 is sandwiched between the
+// value1 beginning and end.
+bool JSStructuredCloneWriter::traverseMap(HandleObject obj) {
+ Rooted<GCVector<Value>> newEntries(context(), GCVector<Value>(context()));
+ {
+ // If there is no wrapper, the compartment munging is a no-op.
+ RootedObject unwrapped(context(), obj->maybeUnwrapAs<MapObject>());
+ MOZ_ASSERT(unwrapped);
+ JSAutoRealm ar(context(), unwrapped);
+ if (!MapObject::getKeysAndValuesInterleaved(unwrapped, &newEntries)) {
+ return false;
+ }
+ }
+ if (!context()->compartment()->wrap(context(), &newEntries)) {
+ return false;
+ }
+
+ for (size_t i = newEntries.length(); i > 0; --i) {
+ if (!otherEntries.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);
+}
+
+// Similar to traverseMap, only there is a single value instead of a key and
+// value, and thus no interleaving is possible: a value will be fully emitted
+// before the next value is begun.
+bool JSStructuredCloneWriter::traverseSet(HandleObject obj) {
+ Rooted<GCVector<Value>> keys(context(), GCVector<Value>(context()));
+ {
+ // If there is no wrapper, the compartment munging is a no-op.
+ RootedObject unwrapped(context(), obj->maybeUnwrapAs<SetObject>());
+ MOZ_ASSERT(unwrapped);
+ JSAutoRealm ar(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 (!otherEntries.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);
+}
+
+bool JSStructuredCloneWriter::traverseSavedFrame(HandleObject obj) {
+ Rooted<SavedFrame*> savedFrame(context(), obj->maybeUnwrapAs<SavedFrame>());
+ MOZ_ASSERT(savedFrame);
+
+ RootedObject parent(context(), savedFrame->getParent());
+ if (!context()->compartment()->wrap(context(), &parent)) {
+ return false;
+ }
+
+ if (!objs.append(ObjectValue(*obj)) ||
+ !otherEntries.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 = BooleanValue(savedFrame->getMutedErrors());
+ if (!writePrimitive(val)) {
+ return false;
+ }
+
+ context()->markAtom(savedFrame->getSource());
+ val = StringValue(savedFrame->getSource());
+ if (!writePrimitive(val)) {
+ return false;
+ }
+
+ val = NumberValue(savedFrame->getLine());
+ if (!writePrimitive(val)) {
+ return false;
+ }
+
+ val = NumberValue(*savedFrame->getColumn().addressOfValueForTranscode());
+ if (!writePrimitive(val)) {
+ return false;
+ }
+
+ auto name = savedFrame->getFunctionDisplayName();
+ if (name) {
+ context()->markAtom(name);
+ }
+ val = name ? StringValue(name) : NullValue();
+ if (!writePrimitive(val)) {
+ return false;
+ }
+
+ auto cause = savedFrame->getAsyncCause();
+ if (cause) {
+ context()->markAtom(cause);
+ }
+ val = cause ? StringValue(cause) : NullValue();
+ if (!writePrimitive(val)) {
+ return false;
+ }
+
+ return true;
+}
+
+// https://html.spec.whatwg.org/multipage/structured-data.html#structuredserializeinternal
+// 2.7.3 StructuredSerializeInternal ( value, forStorage [ , memory ] )
+//
+// Step 17. Otherwise, if value has an [[ErrorData]] internal slot and
+// value is not a platform object, then:
+//
+// Note: This contains custom extensions for handling non-standard properties.
+bool JSStructuredCloneWriter::traverseError(HandleObject obj) {
+ JSContext* cx = context();
+
+ // 1. Let name be ? Get(value, "name").
+ RootedValue name(cx);
+ if (!GetProperty(cx, obj, obj, cx->names().name, &name)) {
+ return false;
+ }
+
+ // 2. If name is not one of "Error", "EvalError", "RangeError",
+ // "ReferenceError", "SyntaxError", "TypeError", or "URIError",
+ // (not yet specified: or "AggregateError")
+ // then set name to "Error".
+ JSExnType type = JSEXN_ERR;
+ if (name.isString()) {
+ JSLinearString* linear = name.toString()->ensureLinear(cx);
+ if (!linear) {
+ return false;
+ }
+
+ if (EqualStrings(linear, cx->names().Error)) {
+ type = JSEXN_ERR;
+ } else if (EqualStrings(linear, cx->names().EvalError)) {
+ type = JSEXN_EVALERR;
+ } else if (EqualStrings(linear, cx->names().RangeError)) {
+ type = JSEXN_RANGEERR;
+ } else if (EqualStrings(linear, cx->names().ReferenceError)) {
+ type = JSEXN_REFERENCEERR;
+ } else if (EqualStrings(linear, cx->names().SyntaxError)) {
+ type = JSEXN_SYNTAXERR;
+ } else if (EqualStrings(linear, cx->names().TypeError)) {
+ type = JSEXN_TYPEERR;
+ } else if (EqualStrings(linear, cx->names().URIError)) {
+ type = JSEXN_URIERR;
+ } else if (EqualStrings(linear, cx->names().AggregateError)) {
+ type = JSEXN_AGGREGATEERR;
+ }
+ }
+
+ // 3. Let valueMessageDesc be ? value.[[GetOwnProperty]]("message").
+ RootedId messageId(cx, NameToId(cx->names().message));
+ Rooted<Maybe<PropertyDescriptor>> messageDesc(cx);
+ if (!GetOwnPropertyDescriptor(cx, obj, messageId, &messageDesc)) {
+ return false;
+ }
+
+ // 4. Let message be undefined if IsDataDescriptor(valueMessageDesc) is false,
+ // and ? ToString(valueMessageDesc.[[Value]]) otherwise.
+ RootedString message(cx);
+ if (messageDesc.isSome() && messageDesc->isDataDescriptor()) {
+ RootedValue messageVal(cx, messageDesc->value());
+ message = ToString<CanGC>(cx, messageVal);
+ if (!message) {
+ return false;
+ }
+ }
+
+ // 5. Set serialized to { [[Type]]: "Error", [[Name]]: name, [[Message]]:
+ // message }.
+
+ if (!objs.append(ObjectValue(*obj))) {
+ return false;
+ }
+
+ Rooted<ErrorObject*> unwrapped(cx, obj->maybeUnwrapAs<ErrorObject>());
+ MOZ_ASSERT(unwrapped);
+
+ // Non-standard: Serialize |stack|.
+ // The Error stack property is saved as SavedFrames.
+ RootedValue stack(cx, NullValue());
+ RootedObject stackObj(cx, unwrapped->stack());
+ if (stackObj) {
+ MOZ_ASSERT(stackObj->canUnwrapAs<SavedFrame>());
+ stack.setObject(*stackObj);
+ if (!cx->compartment()->wrap(cx, &stack)) {
+ return false;
+ }
+ }
+ if (!otherEntries.append(stack)) {
+ return false;
+ }
+
+ // Serialize |errors|
+ if (type == JSEXN_AGGREGATEERR) {
+ RootedValue errors(cx);
+ if (!GetProperty(cx, obj, obj, cx->names().errors, &errors)) {
+ return false;
+ }
+ if (!otherEntries.append(errors)) {
+ return false;
+ }
+ } else {
+ if (!otherEntries.append(NullValue())) {
+ return false;
+ }
+ }
+
+ // Non-standard: Serialize |cause|. Because this property
+ // might be missing we also write "hasCause" later.
+ RootedId causeId(cx, NameToId(cx->names().cause));
+ Rooted<Maybe<PropertyDescriptor>> causeDesc(cx);
+ if (!GetOwnPropertyDescriptor(cx, obj, causeId, &causeDesc)) {
+ return false;
+ }
+
+ Rooted<Maybe<Value>> cause(cx);
+ if (causeDesc.isSome() && causeDesc->isDataDescriptor()) {
+ cause = mozilla::Some(causeDesc->value());
+ }
+ if (!cx->compartment()->wrap(cx, &cause)) {
+ return false;
+ }
+ if (!otherEntries.append(cause.get().valueOr(NullValue()))) {
+ return false;
+ }
+
+ // |cause| + |errors| + |stack|, pushed in reverse order
+ if (!counts.append(3)) {
+ return false;
+ }
+
+ checkStack();
+
+ if (!out.writePair(SCTAG_ERROR_OBJECT, type)) {
+ return false;
+ }
+
+ RootedValue val(cx, message ? StringValue(message) : NullValue());
+ if (!writePrimitive(val)) {
+ return false;
+ }
+
+ // hasCause
+ val = BooleanValue(cause.isSome());
+ if (!writePrimitive(val)) {
+ return false;
+ }
+
+ // Non-standard: Also serialize fileName, lineNumber and columnNumber.
+ {
+ JSAutoRealm ar(cx, unwrapped);
+ val = StringValue(unwrapped->fileName(cx));
+ }
+ if (!cx->compartment()->wrap(cx, &val) || !writePrimitive(val)) {
+ return false;
+ }
+
+ val = Int32Value(unwrapped->lineNumber());
+ if (!writePrimitive(val)) {
+ return false;
+ }
+
+ val = Int32Value(*unwrapped->columnNumber().addressOfValueForTranscode());
+ return writePrimitive(val);
+}
+
+bool JSStructuredCloneWriter::writePrimitive(HandleValue v) {
+ MOZ_ASSERT(v.isPrimitive());
+ context()->check(v);
+
+ if (v.isString()) {
+ return writeString(SCTAG_STRING, v.toString());
+ } else if (v.isInt32()) {
+ if (js::SupportDifferentialTesting()) {
+ return out.writeDouble(v.toInt32());
+ }
+ 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.isBigInt()) {
+ return writeBigInt(SCTAG_BIGINT, v.toBigInt());
+ }
+
+ return reportDataCloneError(JS_SCERR_UNSUPPORTED_TYPE);
+}
+
+bool JSStructuredCloneWriter::startWrite(HandleValue v) {
+ context()->check(v);
+
+ if (v.isPrimitive()) {
+ return writePrimitive(v);
+ }
+
+ if (!v.isObject()) {
+ return reportDataCloneError(JS_SCERR_UNSUPPORTED_TYPE);
+ }
+
+ 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;
+ }
+
+ switch (cls) {
+ case ESClass::Object:
+ case ESClass::Array:
+ return traverseObject(obj, cls);
+ case ESClass::Number: {
+ RootedValue unboxed(context());
+ if (!Unbox(context(), obj, &unboxed)) {
+ return false;
+ }
+ return out.writePair(SCTAG_NUMBER_OBJECT, 0) &&
+ out.writeDouble(unboxed.toNumber());
+ }
+ case ESClass::String: {
+ RootedValue unboxed(context());
+ if (!Unbox(context(), obj, &unboxed)) {
+ return false;
+ }
+ return writeString(SCTAG_STRING_OBJECT, unboxed.toString());
+ }
+ case ESClass::Boolean: {
+ RootedValue unboxed(context());
+ if (!Unbox(context(), obj, &unboxed)) {
+ return false;
+ }
+ return out.writePair(SCTAG_BOOLEAN_OBJECT, unboxed.toBoolean());
+ }
+ case ESClass::RegExp: {
+ RegExpShared* re = RegExpToShared(context(), obj);
+ if (!re) {
+ return false;
+ }
+ return out.writePair(SCTAG_REGEXP_OBJECT, re->getFlags().value()) &&
+ writeString(SCTAG_STRING, re->getSource());
+ }
+ case ESClass::ArrayBuffer: {
+ if (JS::IsArrayBufferObject(obj) && JS::ArrayBufferHasData(obj)) {
+ return writeArrayBuffer(obj);
+ }
+ break;
+ }
+ case ESClass::SharedArrayBuffer:
+ if (JS::IsSharedArrayBufferObject(obj)) {
+ return writeSharedArrayBuffer(obj);
+ }
+ break;
+ case ESClass::Date: {
+ RootedValue unboxed(context());
+ if (!Unbox(context(), obj, &unboxed)) {
+ return false;
+ }
+ return out.writePair(SCTAG_DATE_OBJECT, 0) &&
+ out.writeDouble(unboxed.toNumber());
+ }
+ case ESClass::Set:
+ return traverseSet(obj);
+ case ESClass::Map:
+ return traverseMap(obj);
+ case ESClass::Error:
+ return traverseError(obj);
+ case ESClass::BigInt: {
+ RootedValue unboxed(context());
+ if (!Unbox(context(), obj, &unboxed)) {
+ return false;
+ }
+ return writeBigInt(SCTAG_BIGINT_OBJECT, unboxed.toBigInt());
+ }
+ case ESClass::Promise:
+ case ESClass::MapIterator:
+ case ESClass::SetIterator:
+ case ESClass::Arguments:
+ case ESClass::Function:
+ break;
+
+#ifdef ENABLE_RECORD_TUPLE
+ case ESClass::Record:
+ case ESClass::Tuple:
+ MOZ_CRASH("Record and Tuple are not supported");
+#endif
+
+ case ESClass::Other: {
+ if (obj->canUnwrapAs<TypedArrayObject>()) {
+ return writeTypedArray(obj);
+ }
+ if (obj->canUnwrapAs<DataViewObject>()) {
+ return writeDataView(obj);
+ }
+ if (wasm::IsSharedWasmMemoryObject(obj)) {
+ return writeSharedWasmMemory(obj);
+ }
+ if (obj->canUnwrapAs<SavedFrame>()) {
+ return traverseSavedFrame(obj);
+ }
+ break;
+ }
+ }
+
+ if (out.buf.callbacks_ && out.buf.callbacks_->write) {
+ bool sameProcessScopeRequired = false;
+ if (!out.buf.callbacks_->write(context(), this, obj,
+ &sameProcessScopeRequired,
+ out.buf.closure_)) {
+ return false;
+ }
+
+ if (sameProcessScopeRequired) {
+ output().sameProcessScopeRequired();
+ }
+
+ return true;
+ }
+
+ return reportDataCloneError(JS_SCERR_UNSUPPORTED_TYPE);
+}
+
+bool JSStructuredCloneWriter::writeHeader() {
+ return out.writePair(SCTAG_HEADER, (uint32_t)output().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.length())) {
+ return false;
+ }
+
+ RootedObject obj(context());
+ for (auto* o : transferableObjects) {
+ obj = o;
+ 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.write(0)) { // 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.length());
+ point++;
+
+ JSContext* cx = context();
+ RootedObject obj(cx);
+ JS::StructuredCloneScope scope = output().scope();
+ for (auto* o : transferableObjects) {
+ obj = o;
+
+ 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<ArrayBufferObject*> arrayBuffer(
+ cx, obj->maybeUnwrapAs<ArrayBufferObject>());
+ JSAutoRealm ar(cx, arrayBuffer);
+
+ if (arrayBuffer->isDetached()) {
+ reportDataCloneError(JS_SCERR_TYPED_ARRAY_DETACHED);
+ return false;
+ }
+
+ if (arrayBuffer->isPreparedForAsmJS()) {
+ reportDataCloneError(JS_SCERR_WASM_NO_TRANSFER);
+ return false;
+ }
+
+ if (scope == JS::StructuredCloneScope::DifferentProcess ||
+ scope == JS::StructuredCloneScope::DifferentProcessForIndexedDB ||
+ arrayBuffer->isResizable()) {
+ // 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.
+ //
+ // Resizable ArrayBuffers need to store two extra data, the byte length
+ // and the maximum byte length, but the current transferables format
+ // supports only a single additional datum. Therefore resizable buffers
+ // currently go through this slower code path.
+
+ 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)) {
+ ReportOutOfMemory(cx);
+ 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 {
+ size_t nbytes = arrayBuffer->byteLength();
+
+ using BufferContents = ArrayBufferObject::BufferContents;
+
+ BufferContents bufContents =
+ ArrayBufferObject::extractStructuredCloneContents(cx, arrayBuffer);
+ if (!bufContents) {
+ return false; // out of memory
+ }
+
+ content = bufContents.data();
+ if (bufContents.kind() == ArrayBufferObject::MAPPED) {
+ ownership = JS::SCTAG_TMO_MAPPED_DATA;
+ } else {
+ MOZ_ASSERT(
+ bufContents.kind() ==
+ ArrayBufferObject::MALLOCED_ARRAYBUFFER_CONTENTS_ARENA ||
+ bufContents.kind() ==
+ ArrayBufferObject::MALLOCED_UNKNOWN_ARENA,
+ "failing to handle new ArrayBuffer kind?");
+ ownership = JS::SCTAG_TMO_ALLOC_DATA;
+ }
+ extraData = nbytes;
+ }
+ } else {
+ if (!out.buf.callbacks_ || !out.buf.callbacks_->writeTransfer) {
+ return reportDataCloneError(JS_SCERR_TRANSFERABLE);
+ }
+ if (!out.buf.callbacks_->writeTransfer(cx, obj, out.buf.closure_, &tag,
+ &ownership, &content,
+ &extraData)) {
+ return false;
+ }
+ MOZ_ASSERT(tag > SCTAG_TRANSFER_MAP_PENDING_ENTRY);
+ }
+
+ point.write(NativeEndian::swapToLittleEndian(PairToUInt64(tag, ownership)));
+ MOZ_ALWAYS_TRUE(point.advance());
+ point.write(
+ NativeEndian::swapToLittleEndian(reinterpret_cast<uint64_t>(content)));
+ MOZ_ALWAYS_TRUE(point.advance());
+ point.write(NativeEndian::swapToLittleEndian(extraData));
+ MOZ_ALWAYS_TRUE(point.advance());
+ }
+
+#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;
+ }
+
+ RootedObject obj(context());
+ RootedValue key(context());
+ RootedValue val(context());
+ RootedId id(context());
+
+ RootedValue cause(context());
+ RootedValue errors(context());
+ RootedValue stack(context());
+
+ while (!counts.empty()) {
+ obj = &objs.back().toObject();
+ context()->check(obj);
+ if (counts.back()) {
+ counts.back()--;
+
+ ESClass cls;
+ if (!GetBuiltinClass(context(), obj, &cls)) {
+ return false;
+ }
+
+ if (cls == ESClass::Map) {
+ key = otherEntries.popCopy();
+ checkStack();
+
+ counts.back()--;
+ val = otherEntries.popCopy();
+ checkStack();
+
+ if (!startWrite(key) || !startWrite(val)) {
+ return false;
+ }
+ } else if (cls == ESClass::Set || obj->canUnwrapAs<SavedFrame>()) {
+ key = otherEntries.popCopy();
+ checkStack();
+
+ if (!startWrite(key)) {
+ return false;
+ }
+ } else if (cls == ESClass::Error) {
+ cause = otherEntries.popCopy();
+ checkStack();
+
+ counts.back()--;
+ errors = otherEntries.popCopy();
+ checkStack();
+
+ counts.back()--;
+ stack = otherEntries.popCopy();
+ checkStack();
+
+ if (!startWrite(cause) || !startWrite(errors) || !startWrite(stack)) {
+ return false;
+ }
+ } else {
+ id = objectEntries.popCopy();
+ key = IdToValue(id);
+ checkStack();
+
+ // If obj still has an own property named id, write it out.
+ bool found;
+ if (GetOwnPropertyPure(context(), obj, id, val.address(), &found)) {
+ if (found) {
+ if (!writePrimitive(key) || !startWrite(val)) {
+ return false;
+ }
+ }
+ continue;
+ }
+
+ if (!HasOwnProperty(context(), obj, id, &found)) {
+ return false;
+ }
+
+ if (found) {
+#if FUZZING_JS_FUZZILLI
+ // supress calls into user code
+ if (js::SupportDifferentialTesting()) {
+ fprintf(stderr, "Differential testing: cannot call GetProperty\n");
+ return false;
+ }
+#endif
+
+ if (!writePrimitive(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();
+}
+
+JSStructuredCloneReader::JSStructuredCloneReader(
+ SCInput& in, JS::StructuredCloneScope scope,
+ const JS::CloneDataPolicy& cloneDataPolicy,
+ const JSStructuredCloneCallbacks* cb, void* cbClosure)
+ : in(in),
+ allowedScope(scope),
+ cloneDataPolicy(cloneDataPolicy),
+ objs(in.context()),
+ objState(in.context(), in.context()),
+ allObjs(in.context()),
+ numItemsRead(0),
+ callbacks(cb),
+ closure(cbClosure),
+ gcHeap(in.context()) {
+ // Avoid the need to bounds check by keeping a never-matching element at the
+ // base of the `objState` stack. This append() will always succeed because
+ // the objState vector has a nonzero MinInlineCapacity.
+ MOZ_ALWAYS_TRUE(objState.append(std::make_pair(nullptr, true)));
+}
+
+template <typename CharT>
+JSString* JSStructuredCloneReader::readStringImpl(
+ uint32_t nchars, ShouldAtomizeStrings atomize) {
+ InlineCharBuffer<CharT> chars;
+ if (!chars.maybeAlloc(context(), nchars) ||
+ !in.readChars(chars.get(), nchars)) {
+ return nullptr;
+ }
+
+ if (atomize) {
+ return chars.toAtom(context(), nchars);
+ }
+
+ return chars.toStringDontDeflate(context(), nchars, gcHeap);
+}
+
+JSString* JSStructuredCloneReader::readString(uint32_t data,
+ ShouldAtomizeStrings atomize) {
+ uint32_t nchars = data & BitMask(31);
+ bool latin1 = data & (1 << 31);
+
+ if (nchars > JSString::MAX_LENGTH) {
+ JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr,
+ JSMSG_SC_BAD_SERIALIZED_DATA, "string length");
+ return nullptr;
+ }
+
+ return latin1 ? readStringImpl<Latin1Char>(nchars, atomize)
+ : readStringImpl<char16_t>(nchars, atomize);
+}
+
+[[nodiscard]] bool JSStructuredCloneReader::readUint32(uint32_t* num) {
+ Rooted<Value> lineVal(context());
+ if (!startRead(&lineVal)) {
+ return false;
+ }
+ if (!lineVal.isInt32()) {
+ JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr,
+ JSMSG_SC_BAD_SERIALIZED_DATA, "integer required");
+ return false;
+ }
+ *num = uint32_t(lineVal.toInt32());
+ return true;
+}
+
+BigInt* JSStructuredCloneReader::readBigInt(uint32_t data) {
+ size_t length = data & BitMask(31);
+ bool isNegative = data & (1 << 31);
+ if (length == 0) {
+ return BigInt::zero(context());
+ }
+ RootedBigInt result(context(), BigInt::createUninitialized(
+ context(), length, isNegative, gcHeap));
+ if (!result) {
+ return nullptr;
+ }
+ if (!in.readArray(result->digits().data(), length)) {
+ return nullptr;
+ }
+ return result;
+}
+
+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,
+ uint64_t nelems,
+ MutableHandleValue vp,
+ bool v1Read) {
+ if (arrayType > (v1Read ? Scalar::Uint8Clamped : Scalar::BigUint64)) {
+ 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;
+ }
+
+ // Auto-length TypedArrays are tagged by using `-1` for their length.
+ bool isAutoLength = nelems == uint64_t(-1);
+
+ // Zero |nelems| if it was only used as a tag.
+ if (isAutoLength) {
+ nelems = 0;
+ }
+
+ // Read the ArrayBuffer object and its contents (but no properties)
+ RootedValue v(context());
+ uint64_t byteOffset;
+ if (v1Read) {
+ MOZ_ASSERT(!isAutoLength, "v1Read can't produce auto-length TypedArrays");
+ if (!readV1ArrayBuffer(arrayType, nelems, &v)) {
+ return false;
+ }
+ byteOffset = 0;
+ } else {
+ if (!startRead(&v)) {
+ return false;
+ }
+ if (!in.read(&byteOffset)) {
+ return false;
+ }
+ }
+
+ // Ensure invalid 64-bit values won't be truncated below.
+ if (nelems > ArrayBufferObject::ByteLengthLimit ||
+ byteOffset > ArrayBufferObject::ByteLengthLimit) {
+ JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr,
+ JSMSG_SC_BAD_SERIALIZED_DATA,
+ "invalid typed array length or offset");
+ return false;
+ }
+
+ if (!v.isObject() || !v.toObject().is<ArrayBufferObjectMaybeShared>()) {
+ 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);
+
+ // Negative values represent an absent length parameter.
+ int64_t length = isAutoLength ? -1 : int64_t(nelems);
+
+ switch (arrayType) {
+#define CREATE_FROM_BUFFER(ExternalType, NativeType, Name) \
+ case Scalar::Name: \
+ obj = JS::TypedArray<Scalar::Name>::fromBuffer(context(), buffer, \
+ byteOffset, length) \
+ .asObject(); \
+ break;
+
+ JS_FOR_EACH_TYPED_ARRAY(CREATE_FROM_BUFFER)
+#undef CREATE_FROM_BUFFER
+
+ 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(uint64_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<ArrayBufferObjectMaybeShared>()) {
+ JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr,
+ JSMSG_SC_BAD_SERIALIZED_DATA,
+ "DataView must be backed by an ArrayBuffer");
+ return false;
+ }
+
+ // Read byteOffset.
+ uint64_t byteOffset;
+ if (!in.read(&byteOffset)) {
+ return false;
+ }
+
+ // Auto-length DataViews are tagged by using `-1` for their byte length.
+ bool isAutoLength = byteLength == uint64_t(-1);
+
+ // Zero |byteLength| if it was only used as a tag.
+ if (isAutoLength) {
+ byteLength = 0;
+ }
+
+ // Ensure invalid 64-bit values won't be truncated below.
+ if (byteLength > ArrayBufferObject::ByteLengthLimit ||
+ byteOffset > ArrayBufferObject::ByteLengthLimit) {
+ JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr,
+ JSMSG_SC_BAD_SERIALIZED_DATA,
+ "invalid DataView length or offset");
+ return false;
+ }
+
+ RootedObject buffer(context(), &v.toObject());
+ RootedObject obj(context());
+ if (!isAutoLength) {
+ obj = JS_NewDataView(context(), buffer, byteOffset, byteLength);
+ } else {
+ obj = js::NewDataView(context(), buffer, byteOffset);
+ }
+ if (!obj) {
+ return false;
+ }
+ vp.setObject(*obj);
+
+ allObjs[placeholderIndex].set(vp);
+
+ return true;
+}
+
+bool JSStructuredCloneReader::readArrayBuffer(StructuredDataType type,
+ uint32_t data,
+ MutableHandleValue vp) {
+ // V2 stores the length in |data|. The current version stores the
+ // length separately to allow larger length values.
+ uint64_t nbytes = 0;
+ uint64_t maxbytes = 0;
+ if (type == SCTAG_ARRAY_BUFFER_OBJECT) {
+ if (!in.read(&nbytes)) {
+ return false;
+ }
+ } else if (type == SCTAG_RESIZABLE_ARRAY_BUFFER_OBJECT) {
+ if (!in.read(&nbytes)) {
+ return false;
+ }
+ if (!in.read(&maxbytes)) {
+ return false;
+ }
+ } else {
+ MOZ_ASSERT(type == SCTAG_ARRAY_BUFFER_OBJECT_V2);
+ nbytes = data;
+ }
+
+ // The maximum ArrayBuffer size depends on the platform, and we cast to size_t
+ // below, so we have to check this here.
+ if (nbytes > ArrayBufferObject::ByteLengthLimit ||
+ maxbytes > ArrayBufferObject::ByteLengthLimit) {
+ JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr,
+ JSMSG_BAD_ARRAY_LENGTH);
+ return false;
+ }
+
+ JSObject* obj;
+ if (type != SCTAG_RESIZABLE_ARRAY_BUFFER_OBJECT) {
+ MOZ_ASSERT(maxbytes == 0);
+ obj = ArrayBufferObject::createZeroed(context(), size_t(nbytes));
+ } else {
+ obj = ResizableArrayBufferObject::createZeroed(context(), size_t(nbytes),
+ size_t(maxbytes));
+ }
+ if (!obj) {
+ return false;
+ }
+ vp.setObject(*obj);
+ ArrayBufferObject& buffer = obj->as<ArrayBufferObject>();
+ MOZ_ASSERT(buffer.byteLength() == nbytes);
+ return in.readArray(buffer.dataPointer(), nbytes);
+}
+
+bool JSStructuredCloneReader::readSharedArrayBuffer(StructuredDataType type,
+ MutableHandleValue vp) {
+ MOZ_ASSERT(type == SCTAG_SHARED_ARRAY_BUFFER_OBJECT ||
+ type == SCTAG_GROWABLE_SHARED_ARRAY_BUFFER_OBJECT);
+
+ if (!cloneDataPolicy.areIntraClusterClonableSharedObjectsAllowed() ||
+ !cloneDataPolicy.areSharedMemoryObjectsAllowed()) {
+ auto error = context()->realm()->creationOptions().getCoopAndCoepEnabled()
+ ? JS_SCERR_NOT_CLONABLE_WITH_COOP_COEP
+ : JS_SCERR_NOT_CLONABLE;
+ ReportDataCloneError(context(), callbacks, error, closure,
+ "SharedArrayBuffer");
+ return false;
+ }
+
+ uint64_t byteLength;
+ if (!in.readBytes(&byteLength, sizeof(byteLength))) {
+ return in.reportTruncated();
+ }
+
+ // The maximum ArrayBuffer size depends on the platform, and we cast to size_t
+ // below, so we have to check this here.
+ if (byteLength > ArrayBufferObject::ByteLengthLimit) {
+ JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr,
+ JSMSG_BAD_ARRAY_LENGTH);
+ return false;
+ }
+
+ intptr_t p;
+ if (!in.readBytes(&p, sizeof(p))) {
+ return in.reportTruncated();
+ }
+
+ bool isGrowable = type == SCTAG_GROWABLE_SHARED_ARRAY_BUFFER_OBJECT;
+
+ SharedArrayRawBuffer* rawbuf = reinterpret_cast<SharedArrayRawBuffer*>(p);
+ MOZ_RELEASE_ASSERT(isGrowable == rawbuf->isGrowable());
+
+ // 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()
+ ->realm()
+ ->creationOptions()
+ .getSharedMemoryAndAtomicsEnabled()) {
+ JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr,
+ JSMSG_SC_SAB_DISABLED);
+ return false;
+ }
+
+ // The new object will have a new reference to the rawbuf.
+
+ if (!rawbuf->addReference()) {
+ JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr,
+ JSMSG_SC_SAB_REFCNT_OFLO);
+ return false;
+ }
+
+ RootedObject obj(context());
+ if (!isGrowable) {
+ obj = SharedArrayBufferObject::New(context(), rawbuf, byteLength);
+ } else {
+ obj = SharedArrayBufferObject::NewGrowable(context(), rawbuf, byteLength);
+ }
+ if (!obj) {
+ rawbuf->dropReference();
+ return false;
+ }
+
+ // `rawbuf` is now owned by `obj`.
+
+ if (callbacks && callbacks->sabCloned &&
+ !callbacks->sabCloned(context(), /*receiving=*/true, closure)) {
+ return false;
+ }
+
+ vp.setObject(*obj);
+ return true;
+}
+
+bool JSStructuredCloneReader::readSharedWasmMemory(uint32_t nbytes,
+ MutableHandleValue vp) {
+ JSContext* cx = context();
+ if (nbytes != 0) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_SC_BAD_SERIALIZED_DATA,
+ "invalid shared wasm memory tag");
+ return false;
+ }
+
+ if (!cloneDataPolicy.areIntraClusterClonableSharedObjectsAllowed() ||
+ !cloneDataPolicy.areSharedMemoryObjectsAllowed()) {
+ auto error = context()->realm()->creationOptions().getCoopAndCoepEnabled()
+ ? JS_SCERR_NOT_CLONABLE_WITH_COOP_COEP
+ : JS_SCERR_NOT_CLONABLE;
+ ReportDataCloneError(cx, callbacks, error, closure, "WebAssembly.Memory");
+ return false;
+ }
+
+ // Read the isHuge flag
+ RootedValue isHuge(cx);
+ if (!startRead(&isHuge)) {
+ return false;
+ }
+
+ // Read the SharedArrayBuffer object.
+ RootedValue payload(cx);
+ if (!startRead(&payload)) {
+ return false;
+ }
+ if (!payload.isObject() ||
+ !payload.toObject().is<SharedArrayBufferObject>() ||
+ payload.toObject().as<SharedArrayBufferObject>().isGrowable()) {
+ JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr,
+ JSMSG_SC_BAD_SERIALIZED_DATA,
+ "shared wasm memory must be backed by a "
+ "non-growable SharedArrayBuffer");
+ return false;
+ }
+
+ Rooted<ArrayBufferObjectMaybeShared*> sab(
+ cx, &payload.toObject().as<SharedArrayBufferObject>());
+
+ // Construct the memory.
+ RootedObject proto(
+ cx, GlobalObject::getOrCreatePrototype(cx, JSProto_WasmMemory));
+ if (!proto) {
+ return false;
+ }
+ RootedObject memory(
+ cx, WasmMemoryObject::create(cx, sab, isHuge.toBoolean(), proto));
+ if (!memory) {
+ return false;
+ }
+
+ vp.setObject(*memory);
+ 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<size_t> nbytes =
+ mozilla::CheckedInt<size_t>(nelems) *
+ TypedArrayElemSize(static_cast<Scalar::Type>(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::createZeroed(context(), nbytes.value());
+ if (!obj) {
+ return false;
+ }
+ vp.setObject(*obj);
+ ArrayBufferObject& buffer = obj->as<ArrayBufferObject>();
+ 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:
+ case Scalar::BigInt64:
+ case Scalar::BigUint64:
+ 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,
+ ShouldAtomizeStrings atomizeStrings) {
+ uint32_t tag, data;
+ bool alreadAppended = false;
+
+ if (!in.readPair(&tag, &data)) {
+ return false;
+ }
+
+ numItemsRead++;
+
+ 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, atomizeStrings);
+ 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)) {
+ return false;
+ }
+ vp.setDouble(CanonicalizeNaN(d));
+ if (!PrimitiveToObject(context(), vp)) {
+ return false;
+ }
+ break;
+ }
+
+ case SCTAG_BIGINT:
+ case SCTAG_BIGINT_OBJECT: {
+ RootedBigInt bi(context(), readBigInt(data));
+ if (!bi) {
+ return false;
+ }
+ vp.setBigInt(bi);
+ if (tag == SCTAG_BIGINT_OBJECT && !PrimitiveToObject(context(), vp)) {
+ return false;
+ }
+ break;
+ }
+
+ case SCTAG_DATE_OBJECT: {
+ double d;
+ if (!in.readDouble(&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: {
+ if ((data & RegExpFlag::AllFlags) != data) {
+ JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr,
+ JSMSG_SC_BAD_SERIALIZED_DATA, "regexp");
+ return false;
+ }
+
+ RegExpFlags flags(AssertedCast<uint8_t>(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, AtomizeStrings);
+ if (!str) {
+ return false;
+ }
+
+ Rooted<JSAtom*> atom(context(), &str->asAtom());
+
+ NewObjectKind kind =
+ gcHeap == gc::Heap::Tenured ? TenuredObject : GenericObject;
+ RegExpObject* reobj = RegExpObject::create(context(), atom, flags, kind);
+ if (!reobj) {
+ return false;
+ }
+ vp.setObject(*reobj);
+ break;
+ }
+
+ case SCTAG_ARRAY_OBJECT:
+ case SCTAG_OBJECT_OBJECT: {
+ NewObjectKind kind =
+ gcHeap == gc::Heap::Tenured ? TenuredObject : GenericObject;
+ JSObject* obj;
+ if (tag == SCTAG_ARRAY_OBJECT) {
+ obj = NewDenseUnallocatedArray(
+ context(), NativeEndian::swapFromLittleEndian(data), kind);
+ } else {
+ obj = NewPlainObject(context(), kind);
+ }
+ 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, nullptr,
+ JSMSG_SC_BAD_SERIALIZED_DATA, "invalid input");
+ return false;
+
+ case SCTAG_ARRAY_BUFFER_OBJECT_V2:
+ case SCTAG_ARRAY_BUFFER_OBJECT:
+ case SCTAG_RESIZABLE_ARRAY_BUFFER_OBJECT:
+ if (!readArrayBuffer(StructuredDataType(tag), data, vp)) {
+ return false;
+ }
+ break;
+
+ case SCTAG_SHARED_ARRAY_BUFFER_OBJECT:
+ case SCTAG_GROWABLE_SHARED_ARRAY_BUFFER_OBJECT:
+ if (!readSharedArrayBuffer(StructuredDataType(tag), vp)) {
+ return false;
+ }
+ break;
+
+ case SCTAG_SHARED_WASM_MEMORY_OBJECT:
+ if (!readSharedWasmMemory(data, vp)) {
+ return false;
+ }
+ break;
+
+ case SCTAG_TYPED_ARRAY_OBJECT_V2: {
+ // readTypedArray adds the array to allObjs.
+ // V2 stores the length (nelems) in |data| and the arrayType separately.
+ uint64_t arrayType;
+ if (!in.read(&arrayType)) {
+ return false;
+ }
+ uint64_t nelems = data;
+ return readTypedArray(arrayType, nelems, vp);
+ }
+
+ case SCTAG_TYPED_ARRAY_OBJECT: {
+ // readTypedArray adds the array to allObjs.
+ // The current version stores the array type in |data| and the length
+ // (nelems) separately to support large TypedArrays.
+ uint32_t arrayType = data;
+ uint64_t nelems;
+ if (!in.read(&nelems)) {
+ return false;
+ }
+ return readTypedArray(arrayType, nelems, vp);
+ }
+
+ case SCTAG_DATA_VIEW_OBJECT_V2: {
+ // readDataView adds the array to allObjs.
+ uint64_t byteLength = data;
+ return readDataView(byteLength, vp);
+ }
+
+ case SCTAG_DATA_VIEW_OBJECT: {
+ // readDataView adds the array to allObjs.
+ uint64_t byteLength;
+ if (!in.read(&byteLength)) {
+ return false;
+ }
+ return readDataView(byteLength, 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 = readSavedFrameHeader(data);
+ if (!obj || !objs.append(ObjectValue(*obj)) ||
+ !objState.append(std::make_pair(obj, false))) {
+ return false;
+ }
+ vp.setObject(*obj);
+ break;
+ }
+
+ case SCTAG_ERROR_OBJECT: {
+ auto* obj = readErrorHeader(data);
+ if (!obj || !objs.append(ObjectValue(*obj)) ||
+ !objState.append(std::make_pair(obj, false))) {
+ return false;
+ }
+ vp.setObject(*obj);
+ break;
+ }
+
+ case SCTAG_END_OF_KEYS:
+ JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr,
+ JSMSG_SC_BAD_SERIALIZED_DATA,
+ "truncated input");
+ return false;
+ break;
+
+ default: {
+ if (tag <= SCTAG_FLOAT_MAX) {
+ double d = ReinterpretPairAsDouble(tag, data);
+ vp.setNumber(CanonicalizeNaN(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;
+ }
+
+ // callbacks->read() might read other objects from the buffer.
+ // In startWrite we always write the object itself before calling
+ // the custom function. We should do the same here to keep
+ // indexing consistent.
+ uint32_t placeholderIndex = allObjs.length();
+ Value dummy = UndefinedValue();
+ if (!allObjs.append(dummy)) {
+ return false;
+ }
+ JSObject* obj =
+ callbacks->read(context(), this, cloneDataPolicy, tag, data, closure);
+ if (!obj) {
+ return false;
+ }
+ vp.setObject(*obj);
+ allObjs[placeholderIndex].set(vp);
+ alreadAppended = true;
+ }
+ }
+
+ if (!alreadAppended && vp.isObject() && !allObjs.append(vp)) {
+ return false;
+ }
+
+ return true;
+}
+
+bool JSStructuredCloneReader::readHeader() {
+ uint32_t tag, data;
+ if (!in.getPair(&tag, &data)) {
+ return in.reportTruncated();
+ }
+
+ JS::StructuredCloneScope storedScope;
+ if (tag == SCTAG_HEADER) {
+ MOZ_ALWAYS_TRUE(in.readPair(&tag, &data));
+ storedScope = JS::StructuredCloneScope(data);
+ } else {
+ // Old structured clone buffer. We must have read it from disk.
+ storedScope = JS::StructuredCloneScope::DifferentProcessForIndexedDB;
+ }
+
+ // Backward compatibility with old structured clone buffers. Value '0' was
+ // used for SameProcessSameThread scope.
+ if ((int)storedScope == 0) {
+ storedScope = JS::StructuredCloneScope::SameProcess;
+ }
+
+ if (storedScope < JS::StructuredCloneScope::SameProcess ||
+ storedScope > JS::StructuredCloneScope::DifferentProcessForIndexedDB) {
+ JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr,
+ JSMSG_SC_BAD_SERIALIZED_DATA,
+ "invalid structured clone scope");
+ return false;
+ }
+
+ if (allowedScope == JS::StructuredCloneScope::DifferentProcessForIndexedDB) {
+ // Bug 1434308 and bug 1458320 - the scopes stored in old IndexedDB
+ // clones are incorrect. Treat them as if they were DifferentProcess.
+ allowedScope = JS::StructuredCloneScope::DifferentProcess;
+ return true;
+ }
+
+ 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;
+ }
+
+ if (tag == SCTAG_TRANSFER_MAP_PENDING_ENTRY) {
+ ReportDataCloneError(cx, callbacks, JS_SCERR_TRANSFERABLE, closure);
+ return false;
+ }
+
+ 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 (allowedScope == JS::StructuredCloneScope::DifferentProcess ||
+ allowedScope ==
+ JS::StructuredCloneScope::DifferentProcessForIndexedDB) {
+ // Transferred ArrayBuffers in a DifferentProcess clone buffer
+ // are treated as if they weren't Transferred at all. We should
+ // only see SCTAG_TRANSFER_MAP_STORED_ARRAY_BUFFER.
+ ReportDataCloneError(cx, callbacks, JS_SCERR_TRANSFERABLE, closure);
+ return false;
+ }
+
+ MOZ_RELEASE_ASSERT(extraData <= ArrayBufferObject::ByteLengthLimit);
+ 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) {
+ // When the ArrayBuffer can't be allocated, |content| will be free'ed
+ // in `JSStructuredCloneData::discardTransferables()`.
+ obj = JS::NewArrayBufferWithContents(
+ cx, nbytes, content,
+ JS::NewArrayBufferOutOfMemory::CallerMustFreeMemory);
+ } 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);
+ if (!in.seekBy(static_cast<size_t>(extraData))) {
+ return false;
+ }
+
+ if (tailStartPos.isNothing()) {
+ tailStartPos = mozilla::Some(in.tell());
+ }
+
+ uint32_t tag, data;
+ if (!in.readPair(&tag, &data)) {
+ return false;
+ }
+ if (tag != SCTAG_ARRAY_BUFFER_OBJECT_V2 &&
+ tag != SCTAG_ARRAY_BUFFER_OBJECT &&
+ tag != SCTAG_RESIZABLE_ARRAY_BUFFER_OBJECT) {
+ ReportDataCloneError(cx, callbacks, JS_SCERR_TRANSFERABLE, closure);
+ return false;
+ }
+ RootedValue val(cx);
+ if (!readArrayBuffer(StructuredDataType(tag), data, &val)) {
+ return false;
+ }
+ obj = &val.toObject();
+ tailEndPos = mozilla::Some(in.tell());
+ } else {
+ if (!callbacks || !callbacks->readTransfer) {
+ ReportDataCloneError(cx, callbacks, JS_SCERR_TRANSFERABLE, closure);
+ return false;
+ }
+ if (!callbacks->readTransfer(cx, this, cloneDataPolicy, tag, content,
+ extraData, closure, &obj)) {
+ if (!cx->isExceptionPending()) {
+ ReportDataCloneError(cx, callbacks, JS_SCERR_TRANSFERABLE, closure);
+ }
+ 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::readSavedFrameHeader(
+ uint32_t principalsTag) {
+ Rooted<SavedFrame*> 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;
+ }
+
+ RootedValue mutedErrors(context());
+ RootedValue source(context());
+ {
+ // Read a |mutedErrors| boolean followed by a |source| string.
+ // The |mutedErrors| boolean is present in all new structured-clone data,
+ // but in older data it will be absent and only the |source| string will be
+ // found.
+ if (!startRead(&mutedErrors, AtomizeStrings)) {
+ return nullptr;
+ }
+
+ if (mutedErrors.isBoolean()) {
+ if (!startRead(&source, AtomizeStrings) || !source.isString()) {
+ return nullptr;
+ }
+ } else if (mutedErrors.isString()) {
+ // Backwards compatibility: Handle missing |mutedErrors| boolean,
+ // this is actually just a |source| string.
+ source = mutedErrors;
+ mutedErrors.setBoolean(true); // Safe default value.
+ } else {
+ // Invalid type.
+ return nullptr;
+ }
+ }
+
+ savedFrame->initPrincipalsAlreadyHeldAndMutedErrors(principals,
+ mutedErrors.toBoolean());
+
+ savedFrame->initSource(&source.toString()->asAtom());
+
+ uint32_t line;
+ if (!readUint32(&line)) {
+ return nullptr;
+ }
+ savedFrame->initLine(line);
+
+ JS::TaggedColumnNumberOneOrigin column;
+ if (!readUint32(column.addressOfValueForTranscode())) {
+ return nullptr;
+ }
+ savedFrame->initColumn(column);
+
+ // Don't specify a source ID when reading a cloned saved frame, as these IDs
+ // are only valid within a specific process.
+ savedFrame->initSourceId(0);
+
+ RootedValue name(context());
+ if (!startRead(&name, AtomizeStrings)) {
+ return nullptr;
+ }
+ if (!(name.isString() || name.isNull())) {
+ JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr,
+ JSMSG_SC_BAD_SERIALIZED_DATA,
+ "invalid saved frame cause");
+ return nullptr;
+ }
+ JSAtom* atomName = nullptr;
+ if (name.isString()) {
+ atomName = &name.toString()->asAtom();
+ }
+
+ savedFrame->initFunctionDisplayName(atomName);
+
+ RootedValue cause(context());
+ if (!startRead(&cause, AtomizeStrings)) {
+ return nullptr;
+ }
+ if (!(cause.isString() || cause.isNull())) {
+ JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr,
+ JSMSG_SC_BAD_SERIALIZED_DATA,
+ "invalid saved frame cause");
+ return nullptr;
+ }
+ JSAtom* atomCause = nullptr;
+ if (cause.isString()) {
+ atomCause = &cause.toString()->asAtom();
+ }
+ savedFrame->initAsyncCause(atomCause);
+
+ return savedFrame;
+}
+
+// SavedFrame object: there is one child value, the parent SavedFrame,
+// which is either null or another SavedFrame object.
+bool JSStructuredCloneReader::readSavedFrameFields(Handle<SavedFrame*> frameObj,
+ HandleValue parent,
+ bool* state) {
+ if (*state) {
+ JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr,
+ JSMSG_SC_BAD_SERIALIZED_DATA,
+ "multiple SavedFrame parents");
+ return false;
+ }
+
+ SavedFrame* parentFrame;
+ if (parent.isNull()) {
+ parentFrame = nullptr;
+ } else if (parent.isObject() && parent.toObject().is<SavedFrame>()) {
+ parentFrame = &parent.toObject().as<SavedFrame>();
+ } else {
+ JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr,
+ JSMSG_SC_BAD_SERIALIZED_DATA,
+ "invalid SavedFrame parent");
+ return false;
+ }
+
+ frameObj->initParent(parentFrame);
+ *state = true;
+ return true;
+}
+
+JSObject* JSStructuredCloneReader::readErrorHeader(uint32_t type) {
+ JSContext* cx = context();
+
+ switch (type) {
+ case JSEXN_ERR:
+ case JSEXN_EVALERR:
+ case JSEXN_RANGEERR:
+ case JSEXN_REFERENCEERR:
+ case JSEXN_SYNTAXERR:
+ case JSEXN_TYPEERR:
+ case JSEXN_URIERR:
+ case JSEXN_AGGREGATEERR:
+ break;
+ default:
+ JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr,
+ JSMSG_SC_BAD_SERIALIZED_DATA,
+ "invalid error type");
+ return nullptr;
+ }
+
+ RootedString message(cx);
+ {
+ RootedValue messageVal(cx);
+ if (!startRead(&messageVal)) {
+ return nullptr;
+ }
+ if (messageVal.isString()) {
+ message = messageVal.toString();
+ } else if (!messageVal.isNull()) {
+ JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr,
+ JSMSG_SC_BAD_SERIALIZED_DATA,
+ "invalid 'message' field for Error object");
+ return nullptr;
+ }
+ }
+
+ // We have to set |cause| to something if it exists, otherwise the shape
+ // would be wrong. The actual value will be overwritten later.
+ RootedValue val(cx);
+ if (!startRead(&val)) {
+ return nullptr;
+ }
+ bool hasCause = ToBoolean(val);
+ Rooted<Maybe<Value>> cause(cx, mozilla::Nothing());
+ if (hasCause) {
+ cause = mozilla::Some(BooleanValue(true));
+ }
+
+ if (!startRead(&val)) {
+ return nullptr;
+ }
+ if (!val.isString()) {
+ JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr,
+ JSMSG_SC_BAD_SERIALIZED_DATA,
+ "invalid 'fileName' field for Error object");
+ return nullptr;
+ }
+ RootedString fileName(cx, val.toString());
+
+ uint32_t lineNumber;
+ JS::ColumnNumberOneOrigin columnNumber;
+ if (!readUint32(&lineNumber) ||
+ !readUint32(columnNumber.addressOfValueForTranscode())) {
+ return nullptr;
+ }
+
+ // The |cause| and |stack| slots of the objects might be overwritten later.
+ // For AggregateErrors the |errors| property will be added.
+ RootedObject errorObj(
+ cx, ErrorObject::create(cx, static_cast<JSExnType>(type), nullptr,
+ fileName, 0, lineNumber, columnNumber, nullptr,
+ message, cause));
+ if (!errorObj) {
+ return nullptr;
+ }
+
+ return errorObj;
+}
+
+// Error objects have 3 fields, some or all of them null: cause,
+// errors, and stack.
+bool JSStructuredCloneReader::readErrorFields(Handle<ErrorObject*> errorObj,
+ HandleValue cause, bool* state) {
+ JSContext* cx = context();
+ if (*state) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_SC_BAD_SERIALIZED_DATA,
+ "unexpected child value seen for Error object");
+ return false;
+ }
+
+ RootedValue errors(cx);
+ RootedValue stack(cx);
+ if (!startRead(&errors) || !startRead(&stack)) {
+ return false;
+ }
+
+ bool hasCause = errorObj->getCause().isSome();
+ if (hasCause) {
+ errorObj->setCauseSlot(cause);
+ } else if (!cause.isNull()) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_SC_BAD_SERIALIZED_DATA,
+ "invalid 'cause' field for Error object");
+ return false;
+ }
+
+ if (errorObj->type() == JSEXN_AGGREGATEERR) {
+ if (!DefineDataProperty(context(), errorObj, cx->names().errors, errors,
+ 0)) {
+ return false;
+ }
+ } else if (!errors.isNull()) {
+ JS_ReportErrorNumberASCII(
+ cx, GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA,
+ "unexpected 'errors' field seen for non-AggregateError");
+ return false;
+ }
+
+ if (stack.isObject()) {
+ RootedObject stackObj(cx, &stack.toObject());
+ if (!stackObj->is<SavedFrame>()) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_SC_BAD_SERIALIZED_DATA,
+ "invalid 'stack' field for Error object");
+ return false;
+ }
+ errorObj->setStackSlot(stack);
+ } else if (!stack.isNull()) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_SC_BAD_SERIALIZED_DATA,
+ "invalid 'stack' field for Error object");
+ return false;
+ }
+
+ *state = true;
+ return true;
+}
+
+// Read a value and treat as a key,value pair.
+bool JSStructuredCloneReader::readMapField(Handle<MapObject*> mapObj,
+ HandleValue key) {
+ RootedValue val(context());
+ if (!startRead(&val)) {
+ return false;
+ }
+ return MapObject::set(context(), mapObj, key, val);
+}
+
+// Read a value and treat as a key,value pair. Interpret as a plain property
+// value.
+bool JSStructuredCloneReader::readObjectField(HandleObject obj,
+ HandleValue key) {
+ if (!key.isString() && !key.isInt32()) {
+ JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr,
+ JSMSG_SC_BAD_SERIALIZED_DATA,
+ "property key expected");
+ return false;
+ }
+
+ RootedValue val(context());
+ if (!startRead(&val)) {
+ return false;
+ }
+
+ RootedId id(context());
+ if (!PrimitiveValueToId<CanGC>(context(), key, &id)) {
+ return false;
+ }
+
+ // Fast path for adding a new property to a plain object. The property names
+ // we see here should be unique, but we check for duplicates to guard against
+ // corrupt or malicious data.
+ if (id.isString() && obj->is<PlainObject>() &&
+ MOZ_LIKELY(!obj->as<PlainObject>().contains(context(), id))) {
+ return AddDataPropertyToPlainObject(context(), obj.as<PlainObject>(), id,
+ val);
+ }
+
+ // Fast path for adding an array element. The index shouldn't exceed the
+ // array's length, but we check for this in `addDenseElementNoLengthChange` to
+ // guard against corrupt or malicious data.
+ if (id.isInt() && obj->is<ArrayObject>()) {
+ ArrayObject* arr = &obj->as<ArrayObject>();
+ switch (arr->addDenseElementNoLengthChange(context(), id.toInt(), val)) {
+ case DenseElementResult::Failure:
+ return false;
+ case DenseElementResult::Success:
+ return true;
+ case DenseElementResult::Incomplete:
+ // Fall-through to slow path.
+ break;
+ }
+ }
+
+ return DefineDataProperty(context(), obj, id, val);
+}
+
+// Perform the whole recursive reading procedure.
+bool JSStructuredCloneReader::read(MutableHandleValue vp, size_t nbytes) {
+ auto startTime = mozilla::TimeStamp::Now();
+
+ if (!readHeader()) {
+ return false;
+ }
+
+ if (!readTransferMap()) {
+ return false;
+ }
+
+ MOZ_ASSERT(objs.length() == 0);
+ MOZ_ASSERT(objState.length() == 1);
+
+ // 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();
+ if (objState.back().first == obj) {
+ objState.popBack();
+ }
+ continue;
+ }
+
+ // Remember the index of the current top of the state stack, which will
+ // correspond to the state for `obj` iff `obj` is a type that uses state.
+ // startRead() may push additional entries before the state is accessed and
+ // updated while filling in the object's data.
+ size_t objStateIdx = objState.length() - 1;
+
+ // 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 traverseMap() for an example.
+
+ bool expectKeyValuePairs =
+ !(obj->is<MapObject>() || obj->is<SetObject>() ||
+ obj->is<SavedFrame>() || obj->is<ErrorObject>());
+
+ RootedValue key(context());
+ ShouldAtomizeStrings atomize =
+ expectKeyValuePairs ? AtomizeStrings : DontAtomizeStrings;
+ if (!startRead(&key, atomize)) {
+ return false;
+ }
+
+ if (key.isNull() && expectKeyValuePairs) {
+ // Backwards compatibility: Null formerly indicated the end of
+ // object properties.
+
+ // No legacy objects used the state stack.
+ MOZ_ASSERT(objState[objStateIdx].first() != obj);
+
+ objs.popBack();
+ continue;
+ }
+
+ context()->check(key);
+
+ if (obj->is<SetObject>()) {
+ // 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 (!SetObject::add(context(), obj, key)) {
+ return false;
+ }
+ } else if (obj->is<MapObject>()) {
+ Rooted<MapObject*> mapObj(context(), &obj->as<MapObject>());
+ if (!readMapField(mapObj, key)) {
+ return false;
+ }
+ } else if (obj->is<SavedFrame>()) {
+ Rooted<SavedFrame*> frameObj(context(), &obj->as<SavedFrame>());
+ MOZ_ASSERT(objState[objStateIdx].first() == obj);
+ bool state = objState[objStateIdx].second();
+ if (!readSavedFrameFields(frameObj, key, &state)) {
+ return false;
+ }
+ objState[objStateIdx].second() = state;
+ } else if (obj->is<ErrorObject>()) {
+ Rooted<ErrorObject*> errorObj(context(), &obj->as<ErrorObject>());
+ MOZ_ASSERT(objState[objStateIdx].first() == obj);
+ bool state = objState[objStateIdx].second();
+ if (!readErrorFields(errorObj, key, &state)) {
+ return false;
+ }
+ objState[objStateIdx].second() = state;
+ } else {
+ MOZ_ASSERT(expectKeyValuePairs);
+ // Everything else uses a series of key,value,key,value,... Value
+ // objects.
+ if (!readObjectField(obj, key)) {
+ return false;
+ }
+ }
+ }
+
+ allObjs.clear();
+
+ // For fuzzing, it is convenient to allow extra data at the end
+ // of the input buffer so that more possible inputs are considered
+ // valid.
+#ifndef FUZZING
+ bool extraData;
+ if (tailStartPos.isSome()) {
+ // in.tell() is the end of the main data. If "tail" data was consumed,
+ // then check whether there's any data between the main data and the
+ // beginning of the tail, or after the last read point in the tail.
+ extraData = (in.tell() != *tailStartPos || !tailEndPos->done());
+ } else {
+ extraData = !in.tell().done();
+ }
+ if (extraData) {
+ JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr,
+ JSMSG_SC_BAD_SERIALIZED_DATA,
+ "extra data after end");
+ return false;
+ }
+#endif
+
+ JSRuntime* rt = context()->runtime();
+ rt->metrics().DESERIALIZE_BYTES(nbytes);
+ rt->metrics().DESERIALIZE_ITEMS(numItemsRead);
+ mozilla::TimeDuration elapsed = mozilla::TimeStamp::Now() - startTime;
+ rt->metrics().DESERIALIZE_US(elapsed);
+
+ return true;
+}
+
+JS_PUBLIC_API bool JS_ReadStructuredClone(
+ JSContext* cx, const JSStructuredCloneData& buf, uint32_t version,
+ JS::StructuredCloneScope scope, MutableHandleValue vp,
+ const JS::CloneDataPolicy& cloneDataPolicy,
+ const JSStructuredCloneCallbacks* optionalCallbacks, void* closure) {
+ AssertHeapIsIdle();
+ CHECK_THREAD(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, cloneDataPolicy, callbacks,
+ closure);
+}
+
+JS_PUBLIC_API bool JS_WriteStructuredClone(
+ JSContext* cx, HandleValue value, JSStructuredCloneData* bufp,
+ JS::StructuredCloneScope scope, const JS::CloneDataPolicy& cloneDataPolicy,
+ const JSStructuredCloneCallbacks* optionalCallbacks, void* closure,
+ HandleValue transferable) {
+ AssertHeapIsIdle();
+ CHECK_THREAD(cx);
+ cx->check(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();
+ CHECK_THREAD(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::SameProcess,
+ callbacks, closure);
+ {
+ if (value.isObject()) {
+ RootedObject obj(cx, &value.toObject());
+ obj = CheckedUnwrapStatic(obj);
+ if (!obj) {
+ ReportAccessDenied(cx);
+ return false;
+ }
+ AutoRealm ar(cx, obj);
+ RootedValue unwrappedVal(cx, ObjectValue(*obj));
+ if (!buf.write(cx, unwrappedVal, callbacks, closure)) {
+ return false;
+ }
+ } else {
+ if (!buf.write(cx, value, callbacks, closure)) {
+ return false;
+ }
+ }
+ }
+
+ return buf.read(cx, vp, JS::CloneDataPolicy(), callbacks, closure);
+}
+
+JSAutoStructuredCloneBuffer::JSAutoStructuredCloneBuffer(
+ JSAutoStructuredCloneBuffer&& other)
+ : data_(other.scope()) {
+ version_ = other.version_;
+ other.giveTo(&data_);
+}
+
+JSAutoStructuredCloneBuffer& JSAutoStructuredCloneBuffer::operator=(
+ JSAutoStructuredCloneBuffer&& other) {
+ MOZ_ASSERT(&other != this);
+ MOZ_ASSERT(scope() == other.scope());
+ clear();
+ version_ = other.version_;
+ other.giveTo(&data_);
+ return *this;
+}
+
+void JSAutoStructuredCloneBuffer::clear() {
+ data_.discardTransferables();
+ data_.ownTransferables_ = OwnTransferablePolicy::NoTransferables;
+ data_.refsHeld_.releaseAll();
+ data_.Clear();
+ version_ = 0;
+}
+
+void JSAutoStructuredCloneBuffer::adopt(
+ JSStructuredCloneData&& data, uint32_t version,
+ const JSStructuredCloneCallbacks* callbacks, void* closure) {
+ clear();
+ data_ = std::move(data);
+ version_ = version;
+ data_.setCallbacks(callbacks, closure,
+ OwnTransferablePolicy::OwnsTransferablesIfAny);
+}
+
+void JSAutoStructuredCloneBuffer::giveTo(JSStructuredCloneData* data) {
+ *data = std::move(data_);
+ version_ = 0;
+ data_.setCallbacks(nullptr, nullptr, OwnTransferablePolicy::NoTransferables);
+ data_.Clear();
+}
+
+bool JSAutoStructuredCloneBuffer::read(
+ JSContext* cx, MutableHandleValue vp,
+ const JS::CloneDataPolicy& cloneDataPolicy,
+ const JSStructuredCloneCallbacks* optionalCallbacks, void* closure) {
+ MOZ_ASSERT(cx);
+ return !!JS_ReadStructuredClone(
+ cx, data_, version_, data_.scope(), vp, cloneDataPolicy,
+ optionalCallbacks ? optionalCallbacks : data_.callbacks_,
+ optionalCallbacks ? closure : data_.closure_);
+}
+
+bool JSAutoStructuredCloneBuffer::write(
+ JSContext* cx, HandleValue value,
+ const JSStructuredCloneCallbacks* optionalCallbacks, void* closure) {
+ HandleValue transferable = UndefinedHandleValue;
+ return write(cx, value, transferable, JS::CloneDataPolicy(),
+ optionalCallbacks ? optionalCallbacks : data_.callbacks_,
+ optionalCallbacks ? closure : data_.closure_);
+}
+
+bool JSAutoStructuredCloneBuffer::write(
+ JSContext* cx, HandleValue value, HandleValue transferable,
+ const JS::CloneDataPolicy& cloneDataPolicy,
+ const JSStructuredCloneCallbacks* optionalCallbacks, void* closure) {
+ clear();
+ bool ok = JS_WriteStructuredClone(
+ cx, value, &data_, data_.scopeForInternalWriting(), cloneDataPolicy,
+ optionalCallbacks ? optionalCallbacks : data_.callbacks_,
+ optionalCallbacks ? closure : data_.closure_, transferable);
+ if (!ok) {
+ version_ = JS_STRUCTURED_CLONE_VERSION;
+ }
+ 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_ReadString(JSStructuredCloneReader* r,
+ MutableHandleString str) {
+ uint32_t tag, data;
+ if (!r->input().readPair(&tag, &data)) {
+ return false;
+ }
+
+ if (tag == SCTAG_STRING) {
+ if (JSString* s =
+ r->readString(data, JSStructuredCloneReader::DontAtomizeStrings)) {
+ str.set(s);
+ return true;
+ }
+ return false;
+ }
+
+ JS_ReportErrorNumberASCII(r->context(), GetErrorMessage, nullptr,
+ JSMSG_SC_BAD_SERIALIZED_DATA, "expected string");
+ return false;
+}
+
+JS_PUBLIC_API bool JS_ReadDouble(JSStructuredCloneReader* r, double* v) {
+ return r->input().readDouble(v);
+}
+
+JS_PUBLIC_API bool JS_ReadTypedArray(JSStructuredCloneReader* r,
+ MutableHandleValue vp) {
+ uint32_t tag, data;
+ if (!r->input().readPair(&tag, &data)) {
+ return false;
+ }
+
+ if (tag >= SCTAG_TYPED_ARRAY_V1_MIN && tag <= SCTAG_TYPED_ARRAY_V1_MAX) {
+ return r->readTypedArray(TagToV1ArrayType(tag), data, vp, true);
+ }
+
+ if (tag == SCTAG_TYPED_ARRAY_OBJECT_V2) {
+ // V2 stores the length (nelems) in |data| and the arrayType separately.
+ uint64_t arrayType;
+ if (!r->input().read(&arrayType)) {
+ return false;
+ }
+ uint64_t nelems = data;
+ return r->readTypedArray(arrayType, nelems, vp);
+ }
+
+ if (tag == SCTAG_TYPED_ARRAY_OBJECT) {
+ // The current version stores the array type in |data| and the length
+ // (nelems) separately to support large TypedArrays.
+ uint32_t arrayType = data;
+ uint64_t nelems;
+ if (!r->input().read(&nelems)) {
+ return false;
+ }
+ return r->readTypedArray(arrayType, nelems, vp);
+ }
+
+ 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_WriteDouble(JSStructuredCloneWriter* w, double v) {
+ return w->output().writeDouble(v);
+}
+
+JS_PUBLIC_API bool JS_WriteTypedArray(JSStructuredCloneWriter* w,
+ HandleValue v) {
+ MOZ_ASSERT(v.isObject());
+ w->context()->check(v);
+ RootedObject obj(w->context(), &v.toObject());
+
+ // startWrite can write everything, thus we should check here
+ // and report error if the user passes a wrong type.
+ if (!obj->canUnwrapAs<TypedArrayObject>()) {
+ ReportAccessDenied(w->context());
+ return false;
+ }
+
+ // We should use startWrite instead of writeTypedArray, because
+ // typed array is an object, we should add it to the |memory|
+ // (allObjs) list. Directly calling writeTypedArray won't add it.
+ return w->startWrite(v);
+}
+
+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->output().scope();
+}