Bug 1588839 - Part 1 - Add helper to allow structured cloning Error/Exception objects. r=bzbarsky
authorKris Maglione <maglione.k@gmail.com>
Sat, 07 Dec 2019 18:59:14 +0000
changeset 505928 e4783542b7c854ae4aa5cf60ac27e5e1ff19d56a
parent 505927 df371f905d3afb90b216009a8d4fc4d78ad33ae2
child 505929 c51a9a372a814900562f90376156528329d946c2
push id36891
push userccoroiu@mozilla.com
push dateSat, 07 Dec 2019 21:55:34 +0000
treeherdermozilla-central@8d30b15ca8f9 [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 serializable.
+
+    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 aLength == 0 || (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 holder. 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',