From 51fadcc12101ecb663cdbe37cd10ec47b947a4bf Mon Sep 17 00:00:00 2001 From: Tom Schuster Date: Thu, 16 Jun 2022 22:01:14 +0000 Subject: [PATCH] Bug 1556604 - [Structured Clone] Implement clone of Error objects r=evilpie At this point I am convinced that not changing the SavedFrame code is the best way forward. - We need to maintain the SavedFrame code for backwards compat. - We already use the ChildCounter anyway for errors and cause. - DOM Exception ended up landing before this without stack cloning support. Differential Revision: https://phabricator.services.mozilla.com/D145184 --- js/public/StructuredClone.h | 12 +- js/src/builtin/TestingFunctions.cpp | 49 ++ .../jit-test/tests/structured-clone/errors.js | 144 +++++ .../tests/non262/extensions/clone-errors.js | 1 - js/src/vm/ErrorObject.h | 11 + js/src/vm/StructuredClone.cpp | 549 +++++++++++++++--- .../IndexedDB/structured-clone.any.js.ini | 87 +-- .../the-history-interface/001.html.ini | 3 - .../the-history-interface/002.html.ini | 3 - .../structured-cloning-error-extra.html.ini | 8 - .../structuredclone_0.html.ini | 37 -- ...Generator-in-service-worker.https.html.ini | 3 +- ...kGenerator-in-shared-worker.https.html.ini | 3 +- ...eamTrackGenerator-in-worker.https.html.ini | 7 +- .../meta/streams/transferable/reason.html.ini | 33 -- .../test_ext_storage_idb_data_migration.js | 2 +- 16 files changed, 699 insertions(+), 253 deletions(-) create mode 100644 js/src/jit-test/tests/structured-clone/errors.js delete mode 100644 testing/web-platform/meta/html/infrastructure/safe-passing-of-structured-data/structured-cloning-error-extra.html.ini delete mode 100644 testing/web-platform/meta/html/infrastructure/safe-passing-of-structured-data/structuredclone_0.html.ini delete mode 100644 testing/web-platform/meta/streams/transferable/reason.html.ini diff --git a/js/public/StructuredClone.h b/js/public/StructuredClone.h index 47690a1c4e2a..b1b80cf92a3a 100644 --- a/js/public/StructuredClone.h +++ b/js/public/StructuredClone.h @@ -217,13 +217,15 @@ enum TransferableOwnership { class CloneDataPolicy { bool allowIntraClusterClonableSharedObjects_; bool allowSharedMemoryObjects_; + bool allowErrorStackFrames_; public: // The default is to deny all policy-controlled aspects. CloneDataPolicy() : allowIntraClusterClonableSharedObjects_(false), - allowSharedMemoryObjects_(false) {} + allowSharedMemoryObjects_(false), + allowErrorStackFrames_(false) {} // SharedArrayBuffers and WASM modules can only be cloned intra-process // because the shared memory areas are allocated in process-private memory or @@ -234,16 +236,20 @@ class CloneDataPolicy { void allowIntraClusterClonableSharedObjects() { allowIntraClusterClonableSharedObjects_ = true; } - bool areIntraClusterClonableSharedObjectsAllowed() const { return allowIntraClusterClonableSharedObjects_; } void allowSharedMemoryObjects() { allowSharedMemoryObjects_ = true; } - bool areSharedMemoryObjectsAllowed() const { return allowSharedMemoryObjects_; } + + // The Error stack property is saved as SavedFrames, which + // have an associated principal. This principal can't be cloned + // in certain cases. + void allowErrorStackFrames() { allowErrorStackFrames_ = true; } + bool areErrorStackFramesAllowed() const { return allowErrorStackFrames_; } }; } /* namespace JS */ diff --git a/js/src/builtin/TestingFunctions.cpp b/js/src/builtin/TestingFunctions.cpp index 1ac23f22b188..e50bf9245fab 100644 --- a/js/src/builtin/TestingFunctions.cpp +++ b/js/src/builtin/TestingFunctions.cpp @@ -4741,6 +4741,30 @@ bool js::testingFunc_serialize(JSContext* cx, unsigned argc, Value* vp) { } clonebuf.emplace(*scope, nullptr, nullptr); } + + if (!JS_GetProperty(cx, opts, "ErrorStackFrames", &v)) { + return false; + } + + if (!v.isUndefined()) { + JSString* str = JS::ToString(cx, v); + if (!str) { + return false; + } + JSLinearString* poli = str->ensureLinear(cx); + if (!poli) { + return false; + } + + if (StringEqualsLiteral(poli, "allow")) { + policy.allowErrorStackFrames(); + } else if (StringEqualsLiteral(poli, "deny")) { + // default + } else { + JS_ReportErrorASCII(cx, "Invalid policy value for 'ErrorStackFrames'"); + return false; + } + } } if (!clonebuf) { @@ -4771,6 +4795,7 @@ static bool Deserialize(JSContext* cx, unsigned argc, Value* vp) { &args[0].toObject().as()); JS::CloneDataPolicy policy; + JS::StructuredCloneScope scope = obj->isSynthetic() ? JS::StructuredCloneScope::DifferentProcess : JS::StructuredCloneScope::SameProcess; @@ -4830,6 +4855,30 @@ static bool Deserialize(JSContext* cx, unsigned argc, Value* vp) { scope = *maybeScope; } + + if (!JS_GetProperty(cx, opts, "ErrorStackFrames", &v)) { + return false; + } + + if (!v.isUndefined()) { + JSString* str = JS::ToString(cx, v); + if (!str) { + return false; + } + JSLinearString* poli = str->ensureLinear(cx); + if (!poli) { + return false; + } + + if (StringEqualsLiteral(poli, "allow")) { + policy.allowErrorStackFrames(); + } else if (StringEqualsLiteral(poli, "deny")) { + // default + } else { + JS_ReportErrorASCII(cx, "Invalid policy value for 'ErrorStackFrames'"); + return false; + } + } } // Clone buffer was already consumed? diff --git a/js/src/jit-test/tests/structured-clone/errors.js b/js/src/jit-test/tests/structured-clone/errors.js new file mode 100644 index 000000000000..7c58b2095a35 --- /dev/null +++ b/js/src/jit-test/tests/structured-clone/errors.js @@ -0,0 +1,144 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/licenses/publicdomain/ + */ + +load(libdir + "asserts.js"); + +function roundtrip(error) { + let opts = {ErrorStackFrames: "allow"}; + return deserialize(serialize(error, [], opts), opts); +} + +// Basic +{ + let error = new Error("hello world"); + let cloned = roundtrip(error); + + assertDeepEq(cloned, error); + assertEq(cloned.name, "Error"); + assertEq(cloned.message, "hello world"); + assertEq(cloned.stack, error.stack); +} + +let constructors = [Error, EvalError, RangeError, ReferenceError, + SyntaxError, TypeError, URIError]; +for (let constructor of constructors) { + // With message + let error = new constructor("hello"); + let cloned = roundtrip(error); + assertDeepEq(cloned, error); + assertEq(cloned.hasOwnProperty('message'), true); + assertEq(cloned instanceof constructor, true); + + // Without message + error = new constructor(); + cloned = roundtrip(error); + assertDeepEq(cloned, error); + assertEq(cloned.hasOwnProperty('message'), false); + assertEq(cloned instanceof constructor, true); + + // Custom name + error = new constructor("hello"); + error.name = "MyError"; + cloned = roundtrip(error); + assertEq(cloned.name, "Error"); + assertEq(cloned.message, "hello"); + assertEq(cloned.stack, error.stack); + if (constructor !== Error) { + assertEq(cloned instanceof constructor, false); + } + + // |cause| property + error = new constructor("hello", { cause: new Error("foobar") }); + cloned = roundtrip(error); + assertDeepEq(cloned, error); + assertEq(cloned.hasOwnProperty('message'), true); + assertEq(cloned instanceof constructor, true); + assertEq(cloned.stack, error.stack); + assertEq(cloned.stack === undefined, false); + + // Subclassing + error = new (class MyError extends constructor {}); + cloned = roundtrip(error); + assertEq(cloned.name, constructor.name); + assertEq(cloned.hasOwnProperty('message'), false); + assertEq(cloned.stack, error.stack); + assertEq(cloned instanceof Error, true); + + // Cross-compartment + error = evalcx(`new ${constructor.name}("hello")`); + cloned = roundtrip(error); + assertEq(cloned.name, constructor.name); + assertEq(cloned.message, "hello"); + assertEq(cloned.stack, error.stack); + assertEq(cloned instanceof constructor, true); +} + +// Non-string message +{ + let error = new Error("hello world"); + error.message = 123; + let cloned = roundtrip(error); + assertEq(cloned.message, "123"); + assertEq(cloned.hasOwnProperty('message'), true); + + error = new Error(); + Object.defineProperty(error, 'message', { get: () => {} }); + cloned = roundtrip(error); + assertEq(cloned.message, ""); + assertEq(cloned.hasOwnProperty('message'), false); +} + +// AggregateError +{ + // With message + let error = new AggregateError([{a: 1}, {b: 2}], "hello"); + let cloned = roundtrip(error); + assertDeepEq(cloned, error); + assertEq(cloned.hasOwnProperty('message'), true); + assertEq(cloned instanceof AggregateError, true); + + // Without message + error = new AggregateError([{a: 1}, {b: 2}]); + cloned = roundtrip(error); + assertDeepEq(cloned, error); + assertEq(cloned.hasOwnProperty('message'), false); + assertEq(cloned instanceof AggregateError, true); + + // Custom name breaks this! + error = new AggregateError([{a: 1}, {b: 2}]); + error.name = "MyError"; + cloned = roundtrip(error); + assertEq(cloned.name, "Error"); + assertEq(cloned.message, ""); + assertEq(cloned.stack, error.stack); + assertEq(cloned instanceof AggregateError, false); + assertEq(cloned.errors, undefined); + assertEq(cloned.hasOwnProperty('errors'), false); +} + +{ + let error = new Error(); + + // When serializing without stack-frames, deserialization is empty. + let cloned = deserialize(serialize(error, [], {ErrorStackFrames: "deny"}), + {ErrorStackFrames: "allow"}); + assertEq(cloned.name, "Error"); + assertEq(cloned.stack, ""); + + // Defaults to disallow. + cloned = deserialize(serialize(error)); + assertEq(cloned.name, "Error"); + assertEq(cloned.stack, ""); + + // Unexpected stack frames during deserialization throw. + assertErrorMessage(() => { + deserialize(serialize(error, [], {ErrorStackFrames: "allow"}), + {ErrorStackFrames: "deny"}); + }, InternalError, "bad serialized structured data (disallowed 'stack' field encountered for Error object)"); + + // Sanity check + cloned = roundtrip(error); + assertEq(cloned.stack.length > 0, true); +} \ No newline at end of file diff --git a/js/src/tests/non262/extensions/clone-errors.js b/js/src/tests/non262/extensions/clone-errors.js index 0fcf133ac6f1..38e0e78a6fb2 100644 --- a/js/src/tests/non262/extensions/clone-errors.js +++ b/js/src/tests/non262/extensions/clone-errors.js @@ -13,7 +13,6 @@ function check(v) { } // Unsupported object types. -check(new Error("oops")); check(this); check(Math); check(function () {}); diff --git a/js/src/vm/ErrorObject.h b/js/src/vm/ErrorObject.h index 739fe04f1d12..fa431c96a1a2 100644 --- a/js/src/vm/ErrorObject.h +++ b/js/src/vm/ErrorObject.h @@ -126,6 +126,17 @@ class ErrorObject : public NativeObject { return mozilla::Some(value); } + void setStackSlot(const Value& stack) { + MOZ_ASSERT(stack.isObjectOrNull()); + setReservedSlot(STACK_SLOT, stack); + } + + void setCauseSlot(const Value& cause) { + MOZ_ASSERT(!cause.isMagic()); + MOZ_ASSERT(getCause().isSome()); + setReservedSlot(CAUSE_SLOT, cause); + } + // Getter and setter for the Error.prototype.stack accessor. static bool getStack(JSContext* cx, unsigned argc, Value* vp); static bool getStack_impl(JSContext* cx, const CallArgs& args); diff --git a/js/src/vm/StructuredClone.cpp b/js/src/vm/StructuredClone.cpp index 4fa3a5f65025..1e58ad2d1463 100644 --- a/js/src/vm/StructuredClone.cpp +++ b/js/src/vm/StructuredClone.cpp @@ -32,6 +32,7 @@ #include "mozilla/CheckedInt.h" #include "mozilla/EndianUtils.h" #include "mozilla/FloatingPoint.h" +#include "mozilla/Maybe.h" #include "mozilla/RangedPtr.h" #include "mozilla/ScopeExit.h" @@ -56,6 +57,7 @@ #include "js/SharedArrayBuffer.h" // JS::IsSharedArrayBufferObject #include "js/Wrapper.h" #include "vm/BigIntType.h" +#include "vm/ErrorObject.h" #include "vm/JSContext.h" #include "vm/PlainObject.h" // js::PlainObject #include "vm/RegExpObject.h" @@ -65,6 +67,8 @@ #include "vm/WrapperObject.h" #include "wasm/WasmJS.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" @@ -78,6 +82,7 @@ using JS::RegExpFlags; using JS::RootedValueVector; using mozilla::AssertedCast; using mozilla::BitwiseCast; +using mozilla::Maybe; using mozilla::NativeEndian; using mozilla::NumbersAreIdentical; using mozilla::RangedPtr; @@ -135,6 +140,8 @@ enum StructuredDataType : uint32_t { SCTAG_TYPED_ARRAY_OBJECT, SCTAG_DATA_VIEW_OBJECT, + SCTAG_ERROR_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, @@ -436,6 +443,8 @@ struct JSStructuredCloneReader { bool readHeader(); bool readTransferMap(); + [[nodiscard]] bool readUint32(uint32_t* num); + template JSString* readStringImpl(uint32_t nchars, gc::InitialHeap heap); JSString* readString(uint32_t data, gc::InitialHeap heap = gc::DefaultHeap); @@ -444,15 +453,35 @@ struct JSStructuredCloneReader { [[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 readSharedArrayBuffer(MutableHandleValue vp); - [[nodiscard]] bool readSharedWasmMemory(uint32_t nbytes, - MutableHandleValue vp); [[nodiscard]] bool readV1ArrayBuffer(uint32_t arrayType, uint32_t nelems, MutableHandleValue vp); - JSObject* readSavedFrame(uint32_t principalsTag); + + [[nodiscard]] bool readSharedArrayBuffer(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 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 errorObj, + HandleValue cause, bool* state); + + [[nodiscard]] bool readMapField(Handle mapObj, HandleValue key); + + [[nodiscard]] bool readObjectField(HandleObject obj, HandleValue key); + [[nodiscard]] bool startRead(MutableHandleValue vp, gc::InitialHeap strHeap = gc::DefaultHeap); @@ -575,6 +604,7 @@ struct JSStructuredCloneWriter { bool traverseMap(HandleObject obj); bool traverseSet(HandleObject obj); bool traverseSavedFrame(HandleObject obj); + bool traverseError(HandleObject obj); template bool reportDataCloneError(uint32_t errorId, Args&&... aArgs); @@ -609,6 +639,7 @@ struct JSStructuredCloneWriter { // 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 @@ -1611,9 +1642,9 @@ bool JSStructuredCloneWriter::traverseObject(HandleObject obj, ESClass cls) { // // // -// ...key1 data... +// ...key1 fields... // -// ...value1 data... +// ...value1 fields... // // // @@ -1778,6 +1809,163 @@ bool JSStructuredCloneWriter::traverseSavedFrame(HandleObject obj) { 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> 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(cx, messageVal); + if (!message) { + return false; + } + } + + // 5. Set serialized to { [[Type]]: "Error", [[Name]]: name, [[Message]]: + // message }. + + if (!objs.append(ObjectValue(*obj))) { + return false; + } + + Rooted unwrapped(cx, obj->maybeUnwrapAs()); + MOZ_ASSERT(unwrapped); + + // Non-standard: Serialize |stack|. + // The Error stack property is saved as SavedFrames, which + // have an associated principal. This principal can't be cloned + // in certain cases. + RootedValue stack(cx, NullValue()); + if (cloneDataPolicy.areErrorStackFramesAllowed()) { + RootedObject stackObj(cx, unwrapped->stack()); + if (stackObj && stackObj->canUnwrapAs()) { + 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. + Rooted> cause(cx, unwrapped->getCause()); + 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()); + return writePrimitive(val); +} + bool JSStructuredCloneWriter::writePrimitive(HandleValue v) { MOZ_ASSERT(v.isPrimitive()); context()->check(v); @@ -1884,6 +2072,8 @@ bool JSStructuredCloneWriter::startWrite(HandleValue v) { 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)) { @@ -1895,7 +2085,6 @@ bool JSStructuredCloneWriter::startWrite(HandleValue v) { case ESClass::MapIterator: case ESClass::SetIterator: case ESClass::Arguments: - case ESClass::Error: case ESClass::Function: break; @@ -2136,6 +2325,10 @@ bool JSStructuredCloneWriter::write(HandleValue v) { 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); @@ -2165,6 +2358,21 @@ bool JSStructuredCloneWriter::write(HandleValue v) { 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); @@ -2230,6 +2438,20 @@ JSString* JSStructuredCloneReader::readString(uint32_t data, : readStringImpl(nchars, heap); } +[[nodiscard]] bool JSStructuredCloneReader::readUint32(uint32_t* num) { + Rooted 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); @@ -2836,7 +3058,17 @@ bool JSStructuredCloneReader::startRead(MutableHandleValue vp, } case SCTAG_SAVED_FRAME_OBJECT: { - auto obj = readSavedFrame(data); + 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; @@ -3079,7 +3311,8 @@ bool JSStructuredCloneReader::readTransferMap() { return true; } -JSObject* JSStructuredCloneReader::readSavedFrame(uint32_t principalsTag) { +JSObject* JSStructuredCloneReader::readSavedFrameHeader( + uint32_t principalsTag) { Rooted savedFrame(context(), SavedFrame::create(context())); if (!savedFrame) { return nullptr; @@ -3150,16 +3383,14 @@ JSObject* JSStructuredCloneReader::readSavedFrame(uint32_t principalsTag) { RootedValue lineVal(context()); uint32_t line; - if (!startRead(&lineVal) || !lineVal.isNumber() || - !ToUint32(context(), lineVal, &line)) { + if (!readUint32(&line)) { return nullptr; } savedFrame->initLine(line); RootedValue columnVal(context()); uint32_t column; - if (!startRead(&columnVal) || !columnVal.isNumber() || - !ToUint32(context(), columnVal, &column)) { + if (!readUint32(&column)) { return nullptr; } savedFrame->initColumn(column); @@ -3210,6 +3441,216 @@ JSObject* JSStructuredCloneReader::readSavedFrame(uint32_t principalsTag) { return savedFrame; } +// SavedFrame object: there is one child value, the parent SavedFrame, +// which is either null or another SavedFrame object. +bool JSStructuredCloneReader::readSavedFrameFields(Handle 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()) { + parentFrame = &parent.toObject().as(); + } 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> 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, columnNumber; + if (!readUint32(&lineNumber) || !readUint32(&columnNumber)) { + 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(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 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()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_SC_BAD_SERIALIZED_DATA, + "invalid 'stack' field for Error object"); + return false; + } + if (!cloneDataPolicy.areErrorStackFramesAllowed()) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, + "disallowed 'stack' field encountered 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 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) { + RootedValue val(context()); + if (!startRead(&val)) { + return false; + } + + if (!key.isString() && !key.isInt32()) { + JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, + JSMSG_SC_BAD_SERIALIZED_DATA, + "property key expected"); + return false; + } + + RootedId id(context()); + if (!PrimitiveValueToId(context(), key, &id)) { + return false; + } + + if (!DefineDataProperty(context(), obj, id, val)) { + return false; + } + + return true; +} + // Perform the whole recursive reading procedure. bool JSStructuredCloneReader::read(MutableHandleValue vp, size_t nbytes) { auto startTime = mozilla::TimeStamp::Now(); @@ -3278,7 +3719,7 @@ bool JSStructuredCloneReader::read(MutableHandleValue vp, size_t nbytes) { } if (key.isNull() && !(obj->is() || obj->is() || - obj->is())) { + obj->is() || obj->is())) { // Backwards compatibility: Null formerly indicated the end of // object properties. @@ -3289,73 +3730,39 @@ bool JSStructuredCloneReader::read(MutableHandleValue vp, size_t nbytes) { continue; } - // Set object: the values between obj header (from startRead()) and - // SCTAG_END_OF_KEYS are all interpreted as values to add to the set. + context()->check(key); + if (obj->is()) { + // 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; } - continue; - } - - // SavedFrame object: there is one child value, the parent SavedFrame, - // which is either null or another SavedFrame object. - if (obj->is()) { - SavedFrame* parentFrame; - if (key.isNull()) { - parentFrame = nullptr; - } else if (key.isObject() && key.toObject().is()) { - parentFrame = &key.toObject().as(); - } else { - JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, - JSMSG_SC_BAD_SERIALIZED_DATA, - "invalid SavedFrame parent"); + } else if (obj->is()) { + Rooted mapObj(context(), &obj->as()); + if (!readMapField(mapObj, key)) { return false; } - + } else if (obj->is()) { + Rooted frameObj(context(), &obj->as()); MOZ_ASSERT(objState[objStateIdx].first() == obj); - bool& state = objState[objStateIdx].second(); - if (state == false) { - obj->as().initParent(parentFrame); - state = true; - } else { - JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, - JSMSG_SC_BAD_SERIALIZED_DATA, - "multiple SavedFrame parents"); + bool state = objState[objStateIdx].second(); + if (!readSavedFrameFields(frameObj, key, &state)) { return false; } - continue; - } - - // Everything else uses a series of key,value,key,value,... Value - // objects. - RootedValue val(context()); - if (!startRead(&val)) { - return false; - } - - if (obj->is()) { - // For a Map, store those pairs in the contained map - // data structure. - if (!MapObject::set(context(), obj, key, val)) { + objState[objStateIdx].second() = state; + } else if (obj->is()) { + Rooted errorObj(context(), &obj->as()); + MOZ_ASSERT(objState[objStateIdx].first() == obj); + bool state = objState[objStateIdx].second(); + if (!readErrorFields(errorObj, key, &state)) { return false; } + objState[objStateIdx].second() = state; } else { - // For any other Object, interpret them as plain properties. - RootedId id(context()); - - if (!key.isString() && !key.isInt32()) { - JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, - JSMSG_SC_BAD_SERIALIZED_DATA, - "property key expected"); - return false; - } - - if (!PrimitiveValueToId(context(), key, &id)) { - return false; - } - - if (!DefineDataProperty(context(), obj, id, val)) { + // Everything else uses a series of key,value,key,value,... Value + // objects. + if (!readObjectField(obj, key)) { return false; } } @@ -3369,8 +3776,8 @@ bool JSStructuredCloneReader::read(MutableHandleValue vp, size_t nbytes) { #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 + // 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 { diff --git a/testing/web-platform/meta/IndexedDB/structured-clone.any.js.ini b/testing/web-platform/meta/IndexedDB/structured-clone.any.js.ini index 65b27e2b9496..a703639911d3 100644 --- a/testing/web-platform/meta/IndexedDB/structured-clone.any.js.ini +++ b/testing/web-platform/meta/IndexedDB/structured-clone.any.js.ini @@ -5,68 +5,10 @@ expected: OK [structured-clone.any.html?81-100] - [SyntaxError: SyntaxError] - expected: FAIL - - [Error: Error: abc] - expected: FAIL - - [Error: Error] - expected: FAIL - - [RangeError: RangeError: ghi] - expected: FAIL - - [SyntaxError: SyntaxError: ghi] - expected: FAIL - - [ReferenceError: ReferenceError] - expected: FAIL - - [RangeError: RangeError] - expected: FAIL - - [EvalError: EvalError] - expected: FAIL - - [EvalError: EvalError: ghi] - expected: FAIL - - [ReferenceError: ReferenceError: ghi] - expected: FAIL - + expected: OK [structured-clone.any.worker.html?81-100] - [SyntaxError: SyntaxError] - expected: FAIL - - [Error: Error: abc] - expected: FAIL - - [Error: Error] - expected: FAIL - - [RangeError: RangeError: ghi] - expected: FAIL - - [SyntaxError: SyntaxError: ghi] - expected: FAIL - - [ReferenceError: ReferenceError] - expected: FAIL - - [RangeError: RangeError] - expected: FAIL - - [EvalError: EvalError] - expected: FAIL - - [EvalError: EvalError: ghi] - expected: FAIL - - [ReferenceError: ReferenceError: ghi] - expected: FAIL - + expected: OK [structured-clone.any.html?101-last] expected: @@ -134,18 +76,6 @@ if (os == "mac") and debug: TIMEOUT [TIMEOUT, PASS] - [TypeError: TypeError] - expected: FAIL - - [TypeError: TypeError: ghi] - expected: FAIL - - [URIError: URIError] - expected: FAIL - - [URIError: URIError: ghi] - expected: FAIL - [structured-clone.any.worker.html?101-last] expected: @@ -213,19 +143,6 @@ if (os == "mac") and debug: [PASS, TIMEOUT] [TIMEOUT, PASS] - [TypeError: TypeError] - expected: FAIL - - [TypeError: TypeError: ghi] - expected: FAIL - - [URIError: URIError] - expected: FAIL - - [URIError: URIError: ghi] - expected: FAIL - - [structured-clone.any.html?1-20] [structured-clone.any.worker.html?1-20] diff --git a/testing/web-platform/meta/html/browsers/history/the-history-interface/001.html.ini b/testing/web-platform/meta/html/browsers/history/the-history-interface/001.html.ini index 967bada1325e..c3ac94396fee 100644 --- a/testing/web-platform/meta/html/browsers/history/the-history-interface/001.html.ini +++ b/testing/web-platform/meta/html/browsers/history/the-history-interface/001.html.ini @@ -4,6 +4,3 @@ [pushState must not be allowed to create cross-origin URLs (data:URI)] expected: FAIL - - [pushState must be able to use an error object as data] - expected: FAIL diff --git a/testing/web-platform/meta/html/browsers/history/the-history-interface/002.html.ini b/testing/web-platform/meta/html/browsers/history/the-history-interface/002.html.ini index 5b3cfc64cbf0..fa68e88181d0 100644 --- a/testing/web-platform/meta/html/browsers/history/the-history-interface/002.html.ini +++ b/testing/web-platform/meta/html/browsers/history/the-history-interface/002.html.ini @@ -4,6 +4,3 @@ [replaceState must not be allowed to create cross-origin URLs (data:URI)] expected: FAIL - - [replaceState must be able to use an error object as data] - expected: FAIL diff --git a/testing/web-platform/meta/html/infrastructure/safe-passing-of-structured-data/structured-cloning-error-extra.html.ini b/testing/web-platform/meta/html/infrastructure/safe-passing-of-structured-data/structured-cloning-error-extra.html.ini deleted file mode 100644 index ec9170c372a8..000000000000 --- a/testing/web-platform/meta/html/infrastructure/safe-passing-of-structured-data/structured-cloning-error-extra.html.ini +++ /dev/null @@ -1,8 +0,0 @@ -[structured-cloning-error-extra.html] - expected: ERROR - [Throwing name getter fails serialization] - expected: FAIL - - [Errors sent across realms should preserve their type] - expected: TIMEOUT - diff --git a/testing/web-platform/meta/html/infrastructure/safe-passing-of-structured-data/structuredclone_0.html.ini b/testing/web-platform/meta/html/infrastructure/safe-passing-of-structured-data/structuredclone_0.html.ini deleted file mode 100644 index b199fd63bc02..000000000000 --- a/testing/web-platform/meta/html/infrastructure/safe-passing-of-structured-data/structuredclone_0.html.ini +++ /dev/null @@ -1,37 +0,0 @@ -[structuredclone_0.html] - [ReferenceError objects can be cloned] - expected: FAIL - - [Error.message: getter is ignored when cloning] - expected: FAIL - - [EvalError objects can be cloned] - expected: FAIL - - [URIError objects can be cloned] - expected: FAIL - - [Cloning a modified Error] - expected: FAIL - - [RangeError objects can be cloned] - expected: FAIL - - [Empty Error objects can be cloned] - expected: FAIL - - [TypeError objects can be cloned] - expected: FAIL - - [Error objects can be cloned] - expected: FAIL - - [Error.message: undefined property is stringified] - expected: FAIL - - [SyntaxError objects can be cloned] - expected: FAIL - - [URIError objects from other realms are treated as URIError] - expected: FAIL - diff --git a/testing/web-platform/meta/mediacapture-insertable-streams/MediaStreamTrackGenerator-in-service-worker.https.html.ini b/testing/web-platform/meta/mediacapture-insertable-streams/MediaStreamTrackGenerator-in-service-worker.https.html.ini index a9280caaecfc..c5cfc50443a2 100644 --- a/testing/web-platform/meta/mediacapture-insertable-streams/MediaStreamTrackGenerator-in-service-worker.https.html.ini +++ b/testing/web-platform/meta/mediacapture-insertable-streams/MediaStreamTrackGenerator-in-service-worker.https.html.ini @@ -1,4 +1,3 @@ [MediaStreamTrackGenerator-in-service-worker.https.html] - expected: TIMEOUT [A service worker is able to initialize a MediaStreamTrackGenerator without crashing] - expected: TIMEOUT + expected: FAIL diff --git a/testing/web-platform/meta/mediacapture-insertable-streams/MediaStreamTrackGenerator-in-shared-worker.https.html.ini b/testing/web-platform/meta/mediacapture-insertable-streams/MediaStreamTrackGenerator-in-shared-worker.https.html.ini index 6031a51d4b3c..9161d981d7c6 100644 --- a/testing/web-platform/meta/mediacapture-insertable-streams/MediaStreamTrackGenerator-in-shared-worker.https.html.ini +++ b/testing/web-platform/meta/mediacapture-insertable-streams/MediaStreamTrackGenerator-in-shared-worker.https.html.ini @@ -1,4 +1,3 @@ [MediaStreamTrackGenerator-in-shared-worker.https.html] - expected: TIMEOUT [A shared worker is able to initialize a MediaStreamTrackGenerator without crashing] - expected: TIMEOUT + expected: FAIL diff --git a/testing/web-platform/meta/mediacapture-insertable-streams/MediaStreamTrackGenerator-in-worker.https.html.ini b/testing/web-platform/meta/mediacapture-insertable-streams/MediaStreamTrackGenerator-in-worker.https.html.ini index 05161bf54f96..62797e78c3ca 100644 --- a/testing/web-platform/meta/mediacapture-insertable-streams/MediaStreamTrackGenerator-in-worker.https.html.ini +++ b/testing/web-platform/meta/mediacapture-insertable-streams/MediaStreamTrackGenerator-in-worker.https.html.ini @@ -1,10 +1,9 @@ [MediaStreamTrackGenerator-in-worker.https.html] - expected: ERROR [A worker is able to initialize a MediaStreamTrackGenerator without crashing] - expected: TIMEOUT + expected: FAIL [A worker is able to enable a MediaStreamTrackGenerator without crashing] - expected: NOTRUN + expected: FAIL [A worker is able to disable a MediaStreamTrackGenerator without crashing] - expected: NOTRUN + expected: FAIL diff --git a/testing/web-platform/meta/streams/transferable/reason.html.ini b/testing/web-platform/meta/streams/transferable/reason.html.ini deleted file mode 100644 index 99be2a93dd0a..000000000000 --- a/testing/web-platform/meta/streams/transferable/reason.html.ini +++ /dev/null @@ -1,33 +0,0 @@ -[reason.html] - [a TypeError message should not be preserved if it is inherited] - expected: FAIL - - [URIError should be preserved] - expected: FAIL - - [a TypeError message should be converted to a string] - expected: FAIL - - [TypeError should be preserved] - expected: FAIL - - [RangeError should be preserved] - expected: FAIL - - [other attributes of a TypeError should not be preserved] - expected: FAIL - - [ReferenceError should be preserved] - expected: FAIL - - [SyntaxError should be preserved] - expected: FAIL - - [EvalError should be preserved] - expected: FAIL - - [the type and message of a TypeError should be preserved] - expected: FAIL - - [a TypeError message should not be preserved if it is a getter] - expected: FAIL diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_storage_idb_data_migration.js b/toolkit/components/extensions/test/xpcshell/test_ext_storage_idb_data_migration.js index 90d4740bf9be..227ada30ad96 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_storage_idb_data_migration.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_storage_idb_data_migration.js @@ -591,7 +591,7 @@ add_task(async function test_storage_local_data_migration_failure() { // (because it can't be cloned and it is going to raise a DataCloneError), which // will trigger a data migration failure that we expect to increment the related // telemetry histogram. - jsonFile.data.set("fake_invalid_key", new Error()); + jsonFile.data.set("fake_invalid_key", function() {}); async function background() { await browser.storage.local.set({