diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /dom/ipc/ClonedErrorHolder.cpp | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/ipc/ClonedErrorHolder.cpp')
-rw-r--r-- | dom/ipc/ClonedErrorHolder.cpp | 371 |
1 files changed, 371 insertions, 0 deletions
diff --git a/dom/ipc/ClonedErrorHolder.cpp b/dom/ipc/ClonedErrorHolder.cpp new file mode 100644 index 0000000000..af5e765fac --- /dev/null +++ b/dom/ipc/ClonedErrorHolder.cpp @@ -0,0 +1,371 @@ +/* -*- 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( + nsDependentSubstring(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 = std::move(domExn); + } else if (NS_SUCCEEDED(UNWRAP_OBJECT(Exception, aError, exn))) { + mType = Type::Exception; + } else { + aRv.ThrowNotSupportedError( + "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::Rooted<JS::Value> 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) -> uint32_t { + auto length = uint32_t(aStr.Length()); + MOZ_DIAGNOSTIC_ASSERT(length != kVoidStringLength, + "We should not be serializing a 4GiB string"); + if (aStr.IsVoid()) { + return kVoidStringLength; + } + return 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 ToNullTerminatedJSStringBuffer( + JSContext* aCx, const nsString& aStr) { + // Since nsString is null terminated, we can simply copy + 1 characters. + size_t nbytes = (aStr.Length() + 1) * 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::MutableHandle<JS::Value> 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); + + // For some unknown reason, we can end up with a void string in mFilename, + // which will cause filename to be null, which causes JS::CreateError() to + // crash. Make this code against robust against this by treating void + // strings as the empty string. + if (mFilename.IsVoid()) { + mFilename.Assign(""_ns); + } + + // When fuzzing, we can also end up with the message to be null, + // so we should handle that case as well. + if (mMessage.IsVoid()) { + mMessage.Assign(""_ns); + } + + if (!ToJSString(aCx, mFilename, &filename) || + !ToJSString(aCx, mMessage, &message)) { + return false; + } + if (!JS::CreateError(aCx, mExnType, stack, filename, mLineNumber, mColumn, + nullptr, message, JS::NothingHandleValue, aResult)) { + return false; + } + + if (!mSourceLine.IsVoid()) { + JS::Rooted<JSObject*> errObj(aCx, &aResult.toObject()); + if (JSErrorReport* err = JS_ErrorFromException(aCx, errObj)) { + NS_ConvertUTF8toUTF16 sourceLine(mSourceLine); + // Because this string ends up being consumed as an nsDependentString + // in nsXPCComponents_Utils::ReportError, this needs to be a null + // terminated string. + // + // See Bug 1699569. + if (mTokenOffset >= sourceLine.Length()) { + // Corrupt data, leave linebuf unset. + } else if (JS::UniqueTwoByteChars buffer = + ToNullTerminatedJSStringBuffer(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; + } + if (length % 8 != 0) { + 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; +} |