Bug 1588839 - Part 1 - Add helper to allow structured cloning Error/Exception objects. r=bzbarsky
☠☠ backed out by 8a2f3dae304d ☠ ☠
authorKris Maglione <maglione.k@gmail.com>
Sat, 07 Dec 2019 04:43:24 +0000
changeset 505939 65cc1910918cf410ada326fd353db1da51c2c0eb
parent 505938 2f8b5b48c89641ade121b2f184c78dd04be6d8ba
child 505940 cde41501372a607c0b357ab844f87ce512bf8e51
push id102577
push usertjovanovic@mozilla.com
push dateSat, 07 Dec 2019 16:31:17 +0000
treeherderautoland@7cfcd0f5da4f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbzbarsky
bugs1588839
milestone73.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1588839 - Part 1 - Add helper to allow structured cloning Error/Exception objects. r=bzbarsky In order to be able to reasonably debug error results from things like JSWindowActor.sendQuery, we need to be able to clone errors across process boundaries, so that they can be propagated to the caller that initiated a query. The standard for the structured clone algorithm does not allow cloning errors directly, so this patch instead adds a chrome-only wrapper object which supports structured clone writing, and on reading, automatically decodes to the error object it wraps. Callers who wish to clone an Error or Exception object simply need to wrap it in a ClonedErrorHolder before sending. Differential Revision: https://phabricator.services.mozilla.com/D50881
dom/base/DOMException.cpp
dom/base/DOMException.h
dom/base/StructuredCloneHolder.cpp
dom/base/StructuredCloneTags.h
dom/bindings/Bindings.conf
dom/bindings/Exceptions.cpp
dom/bindings/Exceptions.h
dom/chrome-webidl/ClonedErrorHolder.webidl
dom/chrome-webidl/moz.build
dom/ipc/ClonedErrorHolder.cpp
dom/ipc/ClonedErrorHolder.h
dom/ipc/moz.build
--- a/dom/base/DOMException.cpp
+++ b/dom/base/DOMException.cpp
@@ -323,18 +323,19 @@ void Exception::GetStack(JSContext* aCx,
 
 void Exception::Stringify(JSContext* aCx, nsString& retval) {
   nsCString str;
   ToString(aCx, str);
   CopyUTF8toUTF16(str, retval);
 }
 
 DOMException::DOMException(nsresult aRv, const nsACString& aMessage,
-                           const nsACString& aName, uint16_t aCode)
-    : Exception(aMessage, aRv, aName, nullptr, nullptr), mCode(aCode) {}
+                           const nsACString& aName, uint16_t aCode,
+                           nsIStackFrame* aLocation)
+    : Exception(aMessage, aRv, aName, aLocation, nullptr), mCode(aCode) {}
 
 void DOMException::ToString(JSContext* aCx, nsACString& aReturn) {
   aReturn.Truncate();
 
   static const char defaultMsg[] = "<no message>";
   static const char defaultLocation[] = "<unknown>";
   static const char defaultName[] = "<unknown>";
   static const char format[] =
--- a/dom/base/DOMException.h
+++ b/dom/base/DOMException.h
@@ -132,17 +132,18 @@ class Exception : public nsIException, p
   JS::Heap<JS::Value> mThrownJSVal;
 };
 
 NS_DEFINE_STATIC_IID_ACCESSOR(Exception, MOZILLA_EXCEPTION_IID)
 
 class DOMException : public Exception {
  public:
   DOMException(nsresult aRv, const nsACString& aMessage,
-               const nsACString& aName, uint16_t aCode);
+               const nsACString& aName, uint16_t aCode,
+               nsIStackFrame* aLocation = nullptr);
 
   NS_INLINE_DECL_REFCOUNTING_INHERITED(DOMException, Exception)
 
   // nsWrapperCache overrides
   virtual JSObject* WrapObject(JSContext* aCx,
                                JS::Handle<JSObject*> aGivenProto) override;
 
   static already_AddRefed<DOMException> Constructor(
--- a/dom/base/StructuredCloneHolder.cpp
+++ b/dom/base/StructuredCloneHolder.cpp
@@ -7,16 +7,18 @@
 #include "StructuredCloneHolder.h"
 
 #include "ImageContainer.h"
 #include "mozilla/AutoRestore.h"
 #include "mozilla/dom/BindingUtils.h"
 #include "mozilla/dom/BlobBinding.h"
 #include "mozilla/dom/BrowsingContext.h"
 #include "mozilla/dom/BrowsingContextBinding.h"
+#include "mozilla/dom/ClonedErrorHolder.h"
+#include "mozilla/dom/ClonedErrorHolderBinding.h"
 #include "mozilla/dom/StructuredCloneBlob.h"
 #include "mozilla/dom/Directory.h"
 #include "mozilla/dom/DirectoryBinding.h"
 #include "mozilla/dom/File.h"
 #include "mozilla/dom/FileList.h"
 #include "mozilla/dom/FileListBinding.h"
 #include "mozilla/dom/FormData.h"
 #include "mozilla/dom/ImageBitmap.h"
@@ -941,16 +943,20 @@ JSObject* StructuredCloneHolder::CustomR
   if (aTag == SCTAG_DOM_INPUTSTREAM) {
     return ReadInputStream(aCx, aIndex, this);
   }
 
   if (aTag == SCTAG_DOM_BROWSING_CONTEXT) {
     return BrowsingContext::ReadStructuredClone(aCx, aReader, this);
   }
 
+  if (aTag == SCTAG_DOM_CLONED_ERROR_OBJECT) {
+    return ClonedErrorHolder::ReadStructuredClone(aCx, aReader, this);
+  }
+
   return ReadFullySerializableObjects(aCx, aReader, aTag);
 }
 
 bool StructuredCloneHolder::CustomWriteHandler(JSContext* aCx,
                                                JSStructuredCloneWriter* aWriter,
                                                JS::Handle<JSObject*> aObj) {
   if (!mSupportsCloning) {
     return false;
@@ -1013,16 +1019,24 @@ bool StructuredCloneHolder::CustomWriteH
   if (mStructuredCloneScope == StructuredCloneScope::SameProcessSameThread ||
       mStructuredCloneScope == StructuredCloneScope::DifferentProcess) {
     BrowsingContext* holder = nullptr;
     if (NS_SUCCEEDED(UNWRAP_OBJECT(BrowsingContext, &obj, holder))) {
       return holder->WriteStructuredClone(aCx, aWriter, this);
     }
   }
 
+  // See if this is a ClonedErrorHolder object.
+  {
+    ClonedErrorHolder* holder = nullptr;
+    if (NS_SUCCEEDED(UNWRAP_OBJECT(ClonedErrorHolder, &obj, holder))) {
+      return holder->WriteStructuredClone(aCx, aWriter, this);
+    }
+  }
+
   // See if this is a WasmModule.
   if ((mStructuredCloneScope == StructuredCloneScope::SameProcessSameThread ||
        mStructuredCloneScope ==
            StructuredCloneScope::SameProcessDifferentThread) &&
       JS::IsWasmModuleObject(obj)) {
     RefPtr<JS::WasmModule> module = JS::GetWasmModule(obj);
     MOZ_ASSERT(module);
 
--- a/dom/base/StructuredCloneTags.h
+++ b/dom/base/StructuredCloneTags.h
@@ -134,16 +134,18 @@ enum StructuredCloneTags {
   SCTAG_DOM_DIRECTORY,
 
   SCTAG_DOM_INPUTSTREAM,
 
   SCTAG_DOM_STRUCTURED_CLONE_HOLDER,
 
   SCTAG_DOM_BROWSING_CONTEXT,
 
+  SCTAG_DOM_CLONED_ERROR_OBJECT,
+
   // IMPORTANT: If you plan to add an new IDB tag, it _must_ be add before the
   // "less stable" tags!
 };
 
 }  // namespace dom
 }  // namespace mozilla
 
 #endif  // StructuredCloneTags_h__
--- a/dom/bindings/Bindings.conf
+++ b/dom/bindings/Bindings.conf
@@ -118,16 +118,20 @@ DOMInterfaces = {
 'Client' : {
     'concrete': True,
 },
 
 'Clipboard' : {
     'implicitJSContext' : ['write', 'writeText', 'read', 'readText'],
 },
 
+'ClonedErrorHolder': {
+    'wrapperCache': False
+},
+
 'console': {
     'nativeType': 'mozilla::dom::Console',
 },
 
 'ConsoleInstance': {
     'implicitJSContext': ['clear', 'count', 'countReset', 'groupEnd', 'time', 'timeEnd'],
 },
 
--- a/dom/bindings/Exceptions.cpp
+++ b/dom/bindings/Exceptions.cpp
@@ -737,19 +737,22 @@ void JSStackFrame::ToString(JSContext* a
 
 already_AddRefed<nsIStackFrame> CreateStack(JSContext* aCx,
                                             JS::StackCapture&& aCaptureMode) {
   JS::Rooted<JSObject*> stack(aCx);
   if (!JS::CaptureCurrentStack(aCx, &stack, std::move(aCaptureMode))) {
     return nullptr;
   }
 
-  if (!stack) {
-    return nullptr;
+  return CreateStack(aCx, stack);
+}
+
+already_AddRefed<nsIStackFrame> CreateStack(JSContext* aCx,
+                                            JS::Handle<JSObject*> aStack) {
+  if (aStack) {
+    return MakeAndAddRef<JSStackFrame>(aStack);
   }
-
-  nsCOMPtr<nsIStackFrame> frame = new JSStackFrame(stack);
-  return frame.forget();
+  return nullptr;
 }
 
 }  // namespace exceptions
 }  // namespace dom
 }  // namespace mozilla
--- a/dom/bindings/Exceptions.h
+++ b/dom/bindings/Exceptions.h
@@ -50,13 +50,18 @@ already_AddRefed<Exception> CreateExcept
 already_AddRefed<nsIStackFrame> GetCurrentJSStack(int32_t aMaxDepth = -1);
 
 // Internal stuff not intended to be widely used.
 namespace exceptions {
 
 already_AddRefed<nsIStackFrame> CreateStack(JSContext* aCx,
                                             JS::StackCapture&& aCaptureMode);
 
+// Like the above, but creates a JSStackFrame wrapper for an existing
+// JS::SavedFrame object, passed as aStack.
+already_AddRefed<nsIStackFrame> CreateStack(JSContext* aCx,
+                                            JS::Handle<JSObject*> aStack);
+
 }  // namespace exceptions
 }  // namespace dom
 }  // namespace mozilla
 
 #endif
new file mode 100644
--- /dev/null
+++ b/dom/chrome-webidl/ClonedErrorHolder.webidl
@@ -0,0 +1,18 @@
+/* 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/. */
+
+/**
+ * A stub object to hold an Error, Exception, or DOMException object to be
+ * transferred via structured clone. The object will automatically be decoded
+ * directly into the type of the error object that it wraps.
+ *
+ * This is a temporary workaround for lack of native Error and Exception
+ * cloning support, and can be removed once bug 1556604 and bug 1561357 are
+ * fixed.
+ */
+[ChromeOnly, Exposed=Window]
+interface ClonedErrorHolder {
+  [Throws]
+  constructor(object aError);
+};
--- a/dom/chrome-webidl/moz.build
+++ b/dom/chrome-webidl/moz.build
@@ -30,16 +30,17 @@ with Files("WebExtension*.webidl"):
 
 PREPROCESSED_WEBIDL_FILES = [
     'ChromeUtils.webidl',
 ]
 
 WEBIDL_FILES = [
     'BrowsingContext.webidl',
     'ChannelWrapper.webidl',
+    'ClonedErrorHolder.webidl',
     'DebuggerNotification.webidl',
     'DebuggerNotificationObserver.webidl',
     'DebuggerUtils.webidl',
     'DocumentL10n.webidl',
     'DOMCollectedFrames.webidl',
     'DominatorTree.webidl',
     'DOMLocalization.webidl',
     'Flex.webidl',
new file mode 100644
--- /dev/null
+++ b/dom/ipc/ClonedErrorHolder.cpp
@@ -0,0 +1,343 @@
+/* -*- 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/. */
+
+#include "mozilla/dom/ClonedErrorHolder.h"
+
+#include "mozilla/dom/BindingUtils.h"
+#include "mozilla/dom/ClonedErrorHolderBinding.h"
+#include "mozilla/dom/DOMException.h"
+#include "mozilla/dom/DOMExceptionBinding.h"
+#include "mozilla/dom/Exceptions.h"
+#include "mozilla/dom/StructuredCloneTags.h"
+#include "mozilla/dom/ToJSValue.h"
+#include "jsapi.h"
+#include "jsfriendapi.h"
+#include "js/StructuredClone.h"
+#include "nsReadableUtils.h"
+#include "xpcpublic.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+// static
+already_AddRefed<ClonedErrorHolder> ClonedErrorHolder::Constructor(
+    const GlobalObject& aGlobal, JS::Handle<JSObject*> aError,
+    ErrorResult& aRv) {
+  return Create(aGlobal.Context(), aError, aRv);
+}
+
+// static
+already_AddRefed<ClonedErrorHolder> ClonedErrorHolder::Create(
+    JSContext* aCx, JS::Handle<JSObject*> aError, ErrorResult& aRv) {
+  RefPtr<ClonedErrorHolder> ceh = new ClonedErrorHolder();
+  ceh->Init(aCx, aError, aRv);
+  if (aRv.Failed()) {
+    return nullptr;
+  }
+  return ceh.forget();
+}
+
+ClonedErrorHolder::ClonedErrorHolder()
+    : mName(VoidCString()),
+      mMessage(VoidCString()),
+      mFilename(VoidCString()),
+      mSourceLine(VoidCString()) {}
+
+void ClonedErrorHolder::Init(JSContext* aCx, JS::Handle<JSObject*> aError,
+                             ErrorResult& aRv) {
+  JS::Rooted<JSObject*> stack(aCx);
+
+  if (JSErrorReport* err = JS_ErrorFromException(aCx, aError)) {
+    mType = Type::JSError;
+    if (err->message()) {
+      mMessage = err->message().c_str();
+    }
+    if (err->filename) {
+      mFilename = err->filename;
+    }
+    if (err->linebuf()) {
+      AppendUTF16toUTF8(nsDependentString(err->linebuf(), err->linebufLength()),
+                        mSourceLine);
+      mTokenOffset = err->tokenOffset();
+    }
+    mLineNumber = err->lineno;
+    mColumn = err->column;
+    mErrorNumber = err->errorNumber;
+    mExnType = JSExnType(err->exnType);
+
+    // Note: We don't save the souce ID here, since this object is cross-process
+    // clonable, and the source ID won't be valid in other processes.
+    // We don't store the source notes either, though for no other reason that
+    // it isn't clear that it's worth the complexity.
+
+    stack = JS::ExceptionStackOrNull(aError);
+  } else {
+    RefPtr<DOMException> domExn;
+    RefPtr<Exception> exn;
+    if (NS_SUCCEEDED(UNWRAP_OBJECT(DOMException, aError, domExn))) {
+      mType = Type::DOMException;
+      mCode = domExn->Code();
+      exn = domExn.forget();
+    } else if (NS_SUCCEEDED(UNWRAP_OBJECT(Exception, aError, exn))) {
+      mType = Type::Exception;
+    } else {
+      aRv.ThrowDOMException(
+          NS_ERROR_DOM_NOT_SUPPORTED_ERR,
+          "We can only clone DOM Exceptions and native JS Error objects");
+      return;
+    }
+
+    nsAutoString str;
+
+    exn->GetName(str);
+    CopyUTF16toUTF8(str, mName);
+
+    exn->GetMessageMoz(str);
+    CopyUTF16toUTF8(str, mMessage);
+
+    // Note: In DOM exceptions, filename, line number, and column number come
+    // from the stack frame, and don't need to be stored separately. mFilename,
+    // mLineNumber, and mColumn are only used for JS exceptions.
+    //
+    // We also don't serialized Exception's mThrownJSVal or mData fields, since
+    // they generally won't be clonable.
+
+    mResult = nsresult(exn->Result());
+
+    if (nsCOMPtr<nsIStackFrame> frame = exn->GetLocation()) {
+      JS::Rooted<JS::Value> value(aCx);
+      frame->GetNativeSavedFrame(&value);
+      if (value.isObject()) {
+        stack = &value.toObject();
+      }
+    }
+  }
+
+  Maybe<JSAutoRealm> ar;
+  if (stack) {
+    ar.emplace(aCx, stack);
+  }
+  JS::RootedValue stackValue(aCx, JS::ObjectOrNullValue(stack));
+  mStack.Write(aCx, stackValue, aRv);
+}
+
+bool ClonedErrorHolder::WrapObject(JSContext* aCx,
+                                   JS::Handle<JSObject*> aGivenProto,
+                                   JS::MutableHandle<JSObject*> aReflector) {
+  return ClonedErrorHolder_Binding::Wrap(aCx, this, aGivenProto, aReflector);
+}
+
+static constexpr uint32_t kVoidStringLength = ~0;
+
+static bool WriteStringPair(JSStructuredCloneWriter* aWriter,
+                            const nsACString& aString1,
+                            const nsACString& aString2) {
+  auto StringLength = [](const nsACString& aStr) {
+    MOZ_DIAGNOSTIC_ASSERT(uint32_t(aStr.Length()) != kVoidStringLength,
+                          "We should not be serializing a 4GiB string");
+    if (aStr.IsVoid()) {
+      return kVoidStringLength;
+    }
+    return aStr.Length();
+  };
+
+  return JS_WriteUint32Pair(aWriter, StringLength(aString1),
+                            StringLength(aString2)) &&
+         JS_WriteBytes(aWriter, aString1.BeginReading(), aString1.Length()) &&
+         JS_WriteBytes(aWriter, aString2.BeginReading(), aString2.Length());
+}
+
+static bool ReadStringPair(JSStructuredCloneReader* aReader,
+                           nsACString& aString1, nsACString& aString2) {
+  auto ReadString = [&](nsACString& aStr, uint32_t aLength) {
+    if (aLength == kVoidStringLength) {
+      aStr.SetIsVoid(true);
+      return true;
+    }
+    char* data = nullptr;
+    return aStr.GetMutableData(&data, aLength, fallible) &&
+           JS_ReadBytes(aReader, data, aLength);
+  };
+
+  aString1.Truncate(0);
+  aString2.Truncate(0);
+
+  uint32_t length1, length2;
+  return JS_ReadUint32Pair(aReader, &length1, &length2) &&
+         ReadString(aString1, length1) && ReadString(aString2, length2);
+}
+
+bool ClonedErrorHolder::WriteStructuredClone(JSContext* aCx,
+                                             JSStructuredCloneWriter* aWriter,
+                                             StructuredCloneHolder* aHolder) {
+  auto& data = mStack.BufferData();
+  return JS_WriteUint32Pair(aWriter, SCTAG_DOM_CLONED_ERROR_OBJECT, 0) &&
+         WriteStringPair(aWriter, mName, mMessage) &&
+         WriteStringPair(aWriter, mFilename, mSourceLine) &&
+         JS_WriteUint32Pair(aWriter, mLineNumber, mColumn) &&
+         JS_WriteUint32Pair(aWriter, mTokenOffset, mErrorNumber) &&
+         JS_WriteUint32Pair(aWriter, uint32_t(mType), uint32_t(mExnType)) &&
+         JS_WriteUint32Pair(aWriter, mCode, uint32_t(mResult)) &&
+         JS_WriteUint32Pair(aWriter, data.Size(),
+                            JS_STRUCTURED_CLONE_VERSION) &&
+         data.ForEachDataChunk([&](const char* aData, size_t aSize) {
+           return JS_WriteBytes(aWriter, aData, aSize);
+         });
+}
+
+bool ClonedErrorHolder::Init(JSContext* aCx, JSStructuredCloneReader* aReader) {
+  uint32_t type, exnType, result, code;
+  if (!(ReadStringPair(aReader, mName, mMessage) &&
+        ReadStringPair(aReader, mFilename, mSourceLine) &&
+        JS_ReadUint32Pair(aReader, &mLineNumber, &mColumn) &&
+        JS_ReadUint32Pair(aReader, &mTokenOffset, &mErrorNumber) &&
+        JS_ReadUint32Pair(aReader, &type, &exnType) &&
+        JS_ReadUint32Pair(aReader, &code, &result) &&
+        mStack.ReadStructuredCloneInternal(aCx, aReader))) {
+    return false;
+  }
+
+  if (type == uint32_t(Type::Uninitialized) || type >= uint32_t(Type::Max_) ||
+      exnType >= uint32_t(JSEXN_ERROR_LIMIT)) {
+    return false;
+  }
+
+  mType = Type(type);
+  mExnType = JSExnType(exnType);
+  mResult = nsresult(result);
+  mCode = code;
+
+  return true;
+}
+
+/* static */
+JSObject* ClonedErrorHolder::ReadStructuredClone(
+    JSContext* aCx, JSStructuredCloneReader* aReader,
+    StructuredCloneHolder* aHolder) {
+  // Keep the result object rooted across the call to ClonedErrorHolder::Release
+  // to avoid a potential rooting hazard.
+  JS::Rooted<JS::Value> errorVal(aCx);
+  {
+    RefPtr<ClonedErrorHolder> ceh = new ClonedErrorHolder();
+    if (!ceh->Init(aCx, aReader) || !ceh->ToErrorValue(aCx, &errorVal)) {
+      return nullptr;
+    }
+  }
+  return &errorVal.toObject();
+}
+
+static JS::UniqueTwoByteChars ToJSStringBuffer(JSContext* aCx,
+                                               const nsString& aStr) {
+  size_t nbytes = aStr.Length() * sizeof(char16_t);
+  JS::UniqueTwoByteChars buffer(static_cast<char16_t*>(JS_malloc(aCx, nbytes)));
+  if (buffer) {
+    memcpy(buffer.get(), aStr.get(), nbytes);
+  }
+  return buffer;
+}
+
+static bool ToJSString(JSContext* aCx, const nsACString& aStr,
+                       JS::MutableHandle<JSString*> aJSString) {
+  if (aStr.IsVoid()) {
+    aJSString.set(nullptr);
+    return true;
+  }
+  JS::Rooted<JS::Value> res(aCx);
+  if (xpc::NonVoidStringToJsval(aCx, NS_ConvertUTF8toUTF16(aStr), &res)) {
+    aJSString.set(res.toString());
+    return true;
+  }
+  return false;
+}
+
+bool ClonedErrorHolder::ToErrorValue(JSContext* aCx,
+                                     JS::MutableHandleValue aResult) {
+  JS::Rooted<JS::Value> stackVal(aCx);
+  JS::Rooted<JSObject*> stack(aCx);
+
+  IgnoredErrorResult rv;
+  mStack.Read(xpc::CurrentNativeGlobal(aCx), aCx, &stackVal, rv);
+  // Note: We continue even if reading the stack fails, since we can still
+  // produce a useful error object even without a stack. That said, if decoding
+  // the stack fails, there's a pretty good chance that the rest of the message
+  // is corrupt, and there's no telling how useful the final result will
+  // actually be.
+  if (!rv.Failed() && stackVal.isObject()) {
+    stack = &stackVal.toObject();
+    // Make sure that this is really a saved frame. This mainly ensures that
+    // malicious code on the child side can't trigger a memory exploit by
+    // sending an incompatible data type, but also protects against potential
+    // issues like a cross-compartment wrapper being unexpectedly cut.
+    if (!js::IsSavedFrame(stack)) {
+      stack = nullptr;
+    }
+  }
+
+  if (mType == Type::JSError) {
+    JS::Rooted<JSString*> filename(aCx);
+    JS::Rooted<JSString*> message(aCx);
+    if (!ToJSString(aCx, mFilename, &filename) ||
+        !ToJSString(aCx, mMessage, &message)) {
+      return false;
+    }
+    if (!JS::CreateError(aCx, mExnType, stack, filename, mLineNumber, mColumn,
+                         nullptr, message, aResult)) {
+      return false;
+    }
+
+    if (!mSourceLine.IsVoid()) {
+      JS::Rooted<JSObject*> errObj(aCx, &aResult.toObject());
+      if (JSErrorReport* err = JS_ErrorFromException(aCx, errObj)) {
+        NS_ConvertUTF8toUTF16 sourceLine(mSourceLine);
+        if (JS::UniqueTwoByteChars buffer = ToJSStringBuffer(aCx, sourceLine)) {
+          err->initOwnedLinebuf(buffer.release(), sourceLine.Length(),
+                                mTokenOffset);
+        } else {
+          // Just ignore OOM and continue if the string copy failed.
+          JS_ClearPendingException(aCx);
+        }
+      }
+    }
+
+    return true;
+  }
+
+  nsCOMPtr<nsIStackFrame> frame(exceptions::CreateStack(aCx, stack));
+
+  RefPtr<Exception> exn;
+  if (mType == Type::Exception) {
+    exn = new Exception(mMessage, mResult, mName, frame, nullptr);
+  } else {
+    MOZ_ASSERT(mType == Type::DOMException);
+    exn = new DOMException(mResult, mMessage, mName, mCode, frame);
+  }
+
+  return ToJSValue(aCx, exn, aResult);
+}
+
+bool ClonedErrorHolder::Holder::ReadStructuredCloneInternal(
+    JSContext* aCx, JSStructuredCloneReader* aReader) {
+  uint32_t length;
+  uint32_t version;
+  if (!JS_ReadUint32Pair(aReader, &length, &version)) {
+    return false;
+  }
+
+  JSStructuredCloneData data(mStructuredCloneScope);
+  while (length) {
+    size_t size;
+    char* buffer = data.AllocateBytes(length, &size);
+    if (!buffer || !JS_ReadBytes(aReader, buffer, size)) {
+      return false;
+    }
+    length -= size;
+  }
+
+  mBuffer = MakeUnique<JSAutoStructuredCloneBuffer>(
+      mStructuredCloneScope, &StructuredCloneHolder::sCallbacks, this);
+  mBuffer->adopt(std::move(data), version, &StructuredCloneHolder::sCallbacks);
+  return true;
+}
new file mode 100644
--- /dev/null
+++ b/dom/ipc/ClonedErrorHolder.h
@@ -0,0 +1,103 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#ifndef mozilla_dom_ClonedErrorHolder_h
+#define mozilla_dom_ClonedErrorHolder_h
+
+#include "nsISupportsImpl.h"
+#include "js/ErrorReport.h"
+#include "js/TypeDecls.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/StructuredCloneHolder.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/ErrorResult.h"
+
+class nsIGlobalObject;
+class nsQueryActor;
+
+namespace mozilla {
+namespace dom {
+
+class ClonedErrorHolder final {
+  NS_INLINE_DECL_REFCOUNTING(ClonedErrorHolder)
+
+ public:
+  static already_AddRefed<ClonedErrorHolder> Constructor(
+      const GlobalObject& aGlobal, JS::Handle<JSObject*> aError,
+      ErrorResult& aRv);
+
+  static already_AddRefed<ClonedErrorHolder> Create(
+      JSContext* aCx, JS::Handle<JSObject*> aError, ErrorResult& aRv);
+
+  enum class Type : uint8_t {
+    Uninitialized,
+    JSError,
+    Exception,
+    DOMException,
+    Max_,
+  };
+
+  bool WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto,
+                  JS::MutableHandle<JSObject*> aReflector);
+
+  bool WriteStructuredClone(JSContext* aCx, JSStructuredCloneWriter* aWriter,
+                            StructuredCloneHolder* aHolder);
+
+  // Reads the structured clone data for the ClonedErrorHolder and returns the
+  // wrapped object (either a JS Error or an Exception/DOMException object)
+  // directly. Never returns an actual ClonedErrorHolder object.
+  static JSObject* ReadStructuredClone(JSContext* aCx,
+                                       JSStructuredCloneReader* aReader,
+                                       StructuredCloneHolder* aHolder);
+
+ private:
+  ClonedErrorHolder();
+  ~ClonedErrorHolder() = default;
+
+  void Init(JSContext* aCx, JS::Handle<JSObject*> aError, ErrorResult& aRv);
+
+  bool Init(JSContext* aCx, JSStructuredCloneReader* aReader);
+
+  // Creates a new JS Error or Exception/DOMException object based on the
+  // values stored in the holders. Returns false and sets an exception on aCx
+  // if it fails.
+  bool ToErrorValue(JSContext* aCx, JS::MutableHandleValue aResult);
+
+  class Holder final : public StructuredCloneHolder {
+   public:
+    using StructuredCloneHolder::StructuredCloneHolder;
+
+    bool ReadStructuredCloneInternal(JSContext* aCx,
+                                     JSStructuredCloneReader* aReader);
+  };
+
+  // Only a subset of the following fields are used, depending on the mType of
+  // the error stored:
+  nsCString mName;        // Exception, DOMException
+  nsCString mMessage;     // JSError, Exception, DOMException
+  nsCString mFilename;    // JSError only
+  nsCString mSourceLine;  // JSError only
+
+  uint32_t mLineNumber = 0;   // JSError only
+  uint32_t mColumn = 0;       // JSError only
+  uint32_t mTokenOffset = 0;  // JSError only
+  uint32_t mErrorNumber = 0;  // JSError only
+
+  Type mType = Type::Uninitialized;
+
+  uint16_t mCode = 0;                 // DOMException only
+  JSExnType mExnType = JSExnType(0);  // JSError only
+  nsresult mResult = NS_OK;           // Exception, DOMException
+
+  // JSError, Exception, DOMException
+  Holder mStack{Holder::CloningSupported, Holder::TransferringNotSupported,
+                Holder::StructuredCloneScope::DifferentProcess};
+};
+
+}  // namespace dom
+}  // namespace mozilla
+
+#endif  // !defined(mozilla_dom_ClonedErrorHolder_h)
--- a/dom/ipc/moz.build
+++ b/dom/ipc/moz.build
@@ -35,16 +35,17 @@ EXPORTS.mozilla.dom.ipc += [
 
 EXPORTS.mozilla.dom += [
     'BrowserBridgeChild.h',
     'BrowserBridgeHost.h',
     'BrowserBridgeParent.h',
     'BrowserChild.h',
     'BrowserHost.h',
     'BrowserParent.h',
+    'ClonedErrorHolder.h',
     'CoalescedInputData.h',
     'CoalescedMouseData.h',
     'CoalescedWheelData.h',
     'ContentChild.h',
     'ContentParent.h',
     'ContentProcess.h',
     'ContentProcessManager.h',
     'CPOWManagerGetter.h',
@@ -86,16 +87,17 @@ EXPORTS += [
 
 UNIFIED_SOURCES += [
     'BrowserBridgeChild.cpp',
     'BrowserBridgeHost.cpp',
     'BrowserBridgeParent.cpp',
     'BrowserChild.cpp',
     'BrowserHost.cpp',
     'BrowserParent.cpp',
+    'ClonedErrorHolder.cpp',
     'CoalescedMouseData.cpp',
     'CoalescedWheelData.cpp',
     'ColorPickerParent.cpp',
     'ContentParent.cpp',
     'ContentProcess.cpp',
     'ContentProcessManager.cpp',
     'CSPMessageUtils.cpp',
     'DocShellMessageUtils.cpp',