/* -*- 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/. */ /* * JS standard exception implementation. */ #include "jsexn.h" #include "mozilla/Assertions.h" #include "mozilla/ScopeExit.h" #include #include #include #include #include #include #include "jsapi.h" #include "jsfriendapi.h" #include "jstypes.h" #include "gc/Rooting.h" #include "js/CharacterEncoding.h" #include "js/Class.h" #include "js/Conversions.h" #include "js/ErrorReport.h" // JS::PrintError #include "js/Exception.h" // JS::ExceptionStack #include "js/experimental/TypedData.h" // JS_IsArrayBufferViewObject #include "js/friend/ErrorMessages.h" // JSErrNum, js::GetErrorMessage, JSMSG_* #include "js/Object.h" // JS::GetBuiltinClass #include "js/SavedFrameAPI.h" #include "js/UniquePtr.h" #include "js/Value.h" #include "js/Warnings.h" // JS::{,Set}WarningReporter #include "js/Wrapper.h" #include "util/Memory.h" #include "util/StringBuffer.h" #include "vm/Compartment.h" #include "vm/ErrorObject.h" #include "vm/FrameIter.h" // js::NonBuiltinFrameIter #include "vm/JSAtom.h" #include "vm/JSContext.h" #include "vm/JSObject.h" #include "vm/JSScript.h" #include "vm/Realm.h" #include "vm/SavedFrame.h" #include "vm/SavedStacks.h" #include "vm/SelfHosting.h" #include "vm/Stack.h" #include "vm/StringType.h" #include "vm/SymbolType.h" #include "vm/ErrorObject-inl.h" #include "vm/JSContext-inl.h" #include "vm/JSObject-inl.h" #include "vm/ObjectOperations-inl.h" // js::GetProperty #include "vm/SavedStacks-inl.h" using namespace js; using JS::SavedFrameSelfHosted; size_t ExtraMallocSize(JSErrorReport* report) { if (report->linebuf()) { /* * Count with null terminator and alignment. * See CopyExtraData for the details about alignment. */ return (report->linebufLength() + 1) * sizeof(char16_t) + 1; } return 0; } size_t ExtraMallocSize(JSErrorNotes::Note* note) { return 0; } bool CopyExtraData(JSContext* cx, uint8_t** cursor, JSErrorReport* copy, JSErrorReport* report) { if (report->linebuf()) { /* * Make sure cursor is properly aligned for char16_t for platforms * which need it and it's at the end of the buffer on exit. */ size_t alignment_backlog = 0; if (size_t(*cursor) % 2) { (*cursor)++; } else { alignment_backlog = 1; } size_t linebufSize = (report->linebufLength() + 1) * sizeof(char16_t); const char16_t* linebufCopy = (const char16_t*)(*cursor); js_memcpy(*cursor, report->linebuf(), linebufSize); *cursor += linebufSize + alignment_backlog; copy->initBorrowedLinebuf(linebufCopy, report->linebufLength(), report->tokenOffset()); } /* Copy non-pointer members. */ copy->isMuted = report->isMuted; copy->exnType = report->exnType; copy->isWarning_ = report->isWarning_; /* Deep copy notes. */ if (report->notes) { auto copiedNotes = report->notes->copy(cx); if (!copiedNotes) { return false; } copy->notes = std::move(copiedNotes); } else { copy->notes.reset(nullptr); } return true; } bool CopyExtraData(JSContext* cx, uint8_t** cursor, JSErrorNotes::Note* copy, JSErrorNotes::Note* report) { return true; } template static UniquePtr CopyErrorHelper(JSContext* cx, T* report) { /* * We use a single malloc block to make a deep copy of JSErrorReport or * JSErrorNotes::Note, except JSErrorNotes linked from JSErrorReport with * the following layout: * JSErrorReport or JSErrorNotes::Note * char array with characters for message_ * char array with characters for filename * char16_t array with characters for linebuf (only for JSErrorReport) * Such layout together with the properties enforced by the following * asserts does not need any extra alignment padding. */ static_assert(sizeof(T) % sizeof(const char*) == 0); static_assert(sizeof(const char*) % sizeof(char16_t) == 0); size_t filenameSize = report->filename ? strlen(report->filename) + 1 : 0; size_t messageSize = 0; if (report->message()) { messageSize = strlen(report->message().c_str()) + 1; } /* * The mallocSize can not overflow since it represents the sum of the * sizes of already allocated objects. */ size_t mallocSize = sizeof(T) + messageSize + filenameSize + ExtraMallocSize(report); uint8_t* cursor = cx->pod_calloc(mallocSize); if (!cursor) { return nullptr; } UniquePtr copy(new (cursor) T()); cursor += sizeof(T); if (report->message()) { copy->initBorrowedMessage((const char*)cursor); js_memcpy(cursor, report->message().c_str(), messageSize); cursor += messageSize; } if (report->filename) { copy->filename = (const char*)cursor; js_memcpy(cursor, report->filename, filenameSize); cursor += filenameSize; } if (!CopyExtraData(cx, &cursor, copy.get(), report)) { return nullptr; } MOZ_ASSERT(cursor == (uint8_t*)copy.get() + mallocSize); // errorMessageName should be static. copy->errorMessageName = report->errorMessageName; /* Copy non-pointer members. */ copy->sourceId = report->sourceId; copy->lineno = report->lineno; copy->column = report->column; copy->errorNumber = report->errorNumber; return copy; } UniquePtr js::CopyErrorNote(JSContext* cx, JSErrorNotes::Note* note) { return CopyErrorHelper(cx, note); } UniquePtr js::CopyErrorReport(JSContext* cx, JSErrorReport* report) { return CopyErrorHelper(cx, report); } struct SuppressErrorsGuard { JSContext* cx; JS::WarningReporter prevReporter; JS::AutoSaveExceptionState prevState; explicit SuppressErrorsGuard(JSContext* cx) : cx(cx), prevReporter(JS::SetWarningReporter(cx, nullptr)), prevState(cx) {} ~SuppressErrorsGuard() { JS::SetWarningReporter(cx, prevReporter); } }; // Cut off the stack if it gets too deep (most commonly for infinite recursion // errors). static const size_t MAX_REPORTED_STACK_DEPTH = 1u << 7; bool js::CaptureStack(JSContext* cx, MutableHandleObject stack) { return CaptureCurrentStack( cx, stack, JS::StackCapture(JS::MaxFrames(MAX_REPORTED_STACK_DEPTH))); } JSString* js::ComputeStackString(JSContext* cx) { SuppressErrorsGuard seg(cx); RootedObject stack(cx); if (!CaptureStack(cx, &stack)) { return nullptr; } RootedString str(cx); if (!BuildStackString(cx, cx->realm()->principals(), stack, &str)) { return nullptr; } return str.get(); } JSErrorReport* js::ErrorFromException(JSContext* cx, HandleObject objArg) { // It's ok to UncheckedUnwrap here, since all we do is get the // JSErrorReport, and consumers are careful with the information they get // from that anyway. Anyone doing things that would expose anything in the // JSErrorReport to page script either does a security check on the // JSErrorReport's principal or also tries to do toString on our object and // will fail if they can't unwrap it. RootedObject obj(cx, UncheckedUnwrap(objArg)); if (!obj->is()) { return nullptr; } JSErrorReport* report = obj->as().getOrCreateErrorReport(cx); if (!report) { MOZ_ASSERT(cx->isThrowingOutOfMemory()); cx->recoverFromOutOfMemory(); } return report; } JS_PUBLIC_API JSObject* JS::ExceptionStackOrNull(HandleObject objArg) { ErrorObject* obj = objArg->maybeUnwrapIf(); if (!obj) { return nullptr; } return obj->stack(); } JS_FRIEND_API JSLinearString* js::GetErrorTypeName(JSContext* cx, int16_t exnType) { /* * JSEXN_INTERNALERR returns null to prevent that "InternalError: " * is prepended before "uncaught exception: " */ if (exnType < 0 || exnType >= JSEXN_LIMIT || exnType == JSEXN_INTERNALERR || exnType == JSEXN_WARN || exnType == JSEXN_NOTE) { return nullptr; } JSProtoKey key = GetExceptionProtoKey(JSExnType(exnType)); return ClassName(key, cx); } void js::ErrorToException(JSContext* cx, JSErrorReport* reportp, JSErrorCallback callback, void* userRef) { MOZ_ASSERT(!reportp->isWarning()); // We cannot throw a proper object inside the self-hosting realm, as we // cannot construct the Error constructor without self-hosted code. Just // print the error to stderr to help debugging. if (cx->realm()->isSelfHostingRealm()) { JS::PrintError(cx, stderr, reportp, true); return; } // Find the exception index associated with this error. JSErrNum errorNumber = static_cast(reportp->errorNumber); if (!callback) { callback = GetErrorMessage; } const JSErrorFormatString* errorString = callback(userRef, errorNumber); JSExnType exnType = errorString ? static_cast(errorString->exnType) : JSEXN_ERR; MOZ_ASSERT(exnType < JSEXN_ERROR_LIMIT); // Prevent infinite recursion. if (cx->generatingError) { return; } cx->generatingError = true; auto restore = mozilla::MakeScopeExit([cx] { cx->generatingError = false; }); // Create an exception object. RootedString messageStr(cx, reportp->newMessageString(cx)); if (!messageStr) { return; } RootedString fileName(cx, JS_NewStringCopyZ(cx, reportp->filename)); if (!fileName) { return; } uint32_t sourceId = reportp->sourceId; uint32_t lineNumber = reportp->lineno; uint32_t columnNumber = reportp->column; RootedObject stack(cx); if (!CaptureStack(cx, &stack)) { return; } UniquePtr report = CopyErrorReport(cx, reportp); if (!report) { return; } ErrorObject* errObject = ErrorObject::create(cx, exnType, stack, fileName, sourceId, lineNumber, columnNumber, std::move(report), messageStr); if (!errObject) { return; } // Throw it. RootedValue errValue(cx, ObjectValue(*errObject)); RootedSavedFrame nstack(cx); if (stack) { nstack = &stack->as(); } cx->setPendingException(errValue, nstack); } using SniffingBehavior = JS::ErrorReportBuilder::SniffingBehavior; static bool IsDuckTypedErrorObject(JSContext* cx, HandleObject exnObject, const char** filename_strp) { /* * This function is called from ErrorReport::init and so should not generate * any new exceptions. */ AutoClearPendingException acpe(cx); bool found; if (!JS_HasProperty(cx, exnObject, js_message_str, &found) || !found) { return false; } // First try "filename". const char* filename_str = *filename_strp; if (!JS_HasProperty(cx, exnObject, filename_str, &found)) { return false; } if (!found) { // If that doesn't work, try "fileName". filename_str = js_fileName_str; if (!JS_HasProperty(cx, exnObject, filename_str, &found) || !found) { return false; } } if (!JS_HasProperty(cx, exnObject, js_lineNumber_str, &found) || !found) { return false; } *filename_strp = filename_str; return true; } static bool GetPropertyNoException(JSContext* cx, HandleObject obj, SniffingBehavior behavior, HandlePropertyName name, MutableHandleValue vp) { // This function has no side-effects so always use it. if (GetPropertyPure(cx, obj, NameToId(name), vp.address())) { return true; } if (behavior == SniffingBehavior::WithSideEffects) { AutoClearPendingException acpe(cx); return GetProperty(cx, obj, obj, name, vp); } return false; } // Create a new error message similar to what Error.prototype.toString would // produce when called on an object with those property values for name and // message. static JSString* FormatErrorMessage(JSContext* cx, HandleString name, HandleString message) { if (name && message) { AutoClearPendingException acpe(cx); JSStringBuilder sb(cx); // Prefix the message with the error type, if it exists. if (!sb.append(name) || !sb.append(": ") || !sb.append(message)) { return nullptr; } return sb.finishString(); } return name ? name : message; } static JSString* ErrorReportToString(JSContext* cx, HandleObject exn, JSErrorReport* reportp, SniffingBehavior behavior) { // The error object might have custom `name` overwriting the exnType in the // error report. Try getting that property and use the exnType as a fallback. RootedString name(cx); RootedValue nameV(cx); if (GetPropertyNoException(cx, exn, behavior, cx->names().name, &nameV) && nameV.isString()) { name = nameV.toString(); } // We do NOT want to use GetErrorTypeName() here because it will not do the // "right thing" for JSEXN_INTERNALERR. That is, the caller of this API // expects that "InternalError: " will be prepended but GetErrorTypeName // goes out of its way to avoid this. if (!name) { JSExnType type = static_cast(reportp->exnType); if (type != JSEXN_WARN && type != JSEXN_NOTE) { name = ClassName(GetExceptionProtoKey(type), cx); } } RootedString message(cx); RootedValue messageV(cx); if (GetPropertyNoException(cx, exn, behavior, cx->names().message, &messageV) && messageV.isString()) { message = messageV.toString(); } if (!message) { message = reportp->newMessageString(cx); if (!message) { return nullptr; } } return FormatErrorMessage(cx, name, message); } JS::ErrorReportBuilder::ErrorReportBuilder(JSContext* cx) : reportp(nullptr), exnObject(cx) {} JS::ErrorReportBuilder::~ErrorReportBuilder() = default; bool JS::ErrorReportBuilder::init(JSContext* cx, const JS::ExceptionStack& exnStack, SniffingBehavior sniffingBehavior) { MOZ_ASSERT(!cx->isExceptionPending()); MOZ_ASSERT(!reportp); if (exnStack.exception().isObject()) { // Because ToString below could error and an exception object could become // unrooted, we must root our exception object, if any. exnObject = &exnStack.exception().toObject(); reportp = ErrorFromException(cx, exnObject); } // Be careful not to invoke ToString if we've already successfully extracted // an error report, since the exception might be wrapped in a security // wrapper, and ToString-ing it might throw. RootedString str(cx); if (reportp) { str = ErrorReportToString(cx, exnObject, reportp, sniffingBehavior); } else if (exnStack.exception().isSymbol()) { RootedValue strVal(cx); if (js::SymbolDescriptiveString(cx, exnStack.exception().toSymbol(), &strVal)) { str = strVal.toString(); } else { str = nullptr; } } else if (exnObject && sniffingBehavior == NoSideEffects) { str = cx->names().Object; } else { str = js::ToString(cx, exnStack.exception()); } if (!str) { cx->clearPendingException(); } // If ErrorFromException didn't get us a JSErrorReport, then the object // was not an ErrorObject, security-wrapped or otherwise. However, it might // still quack like one. Give duck-typing a chance. We start by looking for // "filename" (all lowercase), since that's where DOMExceptions store their // filename. Then we check "fileName", which is where Errors store it. We // have to do it in that order, because DOMExceptions have Error.prototype // on their proto chain, and hence also have a "fileName" property, but its // value is "". const char* filename_str = "filename"; if (!reportp && exnObject && sniffingBehavior == WithSideEffects && IsDuckTypedErrorObject(cx, exnObject, &filename_str)) { // Temporary value for pulling properties off of duck-typed objects. RootedValue val(cx); RootedString name(cx); if (JS_GetProperty(cx, exnObject, js_name_str, &val) && val.isString()) { name = val.toString(); } else { cx->clearPendingException(); } RootedString msg(cx); if (JS_GetProperty(cx, exnObject, js_message_str, &val) && val.isString()) { msg = val.toString(); } else { cx->clearPendingException(); } // If we have the right fields, override the ToString we performed on // the exception object above with something built out of its quacks // (i.e. as much of |NameQuack: MessageQuack| as we can make). str = FormatErrorMessage(cx, name, msg); { AutoClearPendingException acpe(cx); if (JS_GetProperty(cx, exnObject, filename_str, &val)) { RootedString tmp(cx, js::ToString(cx, val)); if (tmp) { filename = JS_EncodeStringToUTF8(cx, tmp); } } } if (!filename) { filename = DuplicateString(""); if (!filename) { ReportOutOfMemory(cx); return false; } } uint32_t lineno; if (!JS_GetProperty(cx, exnObject, js_lineNumber_str, &val) || !ToUint32(cx, val, &lineno)) { cx->clearPendingException(); lineno = 0; } uint32_t column; if (!JS_GetProperty(cx, exnObject, js_columnNumber_str, &val) || !ToUint32(cx, val, &column)) { cx->clearPendingException(); column = 0; } reportp = &ownedReport; new (reportp) JSErrorReport(); ownedReport.filename = filename.get(); ownedReport.lineno = lineno; ownedReport.exnType = JSEXN_INTERNALERR; ownedReport.column = column; if (str) { // Note that using |str| for |message_| here is kind of wrong, // because |str| is supposed to be of the format // |ErrorName: ErrorMessage|, and |message_| is supposed to // correspond to |ErrorMessage|. But this is what we've // historically done for duck-typed error objects. // // If only this stuff could get specced one day... if (auto utf8 = JS_EncodeStringToUTF8(cx, str)) { ownedReport.initOwnedMessage(utf8.release()); } else { cx->clearPendingException(); str = nullptr; } } } const char* utf8Message = nullptr; if (str) { toStringResultBytesStorage = JS_EncodeStringToUTF8(cx, str); utf8Message = toStringResultBytesStorage.get(); if (!utf8Message) { cx->clearPendingException(); } } if (!utf8Message) { utf8Message = "unknown (can't convert to string)"; } if (!reportp) { // This is basically an inlined version of // // JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, // JSMSG_UNCAUGHT_EXCEPTION, utf8Message); // // but without the reporting bits. Instead it just puts all // the stuff we care about in our ownedReport and message_. if (!populateUncaughtExceptionReportUTF8(cx, exnStack.stack(), utf8Message)) { // Just give up. We're out of memory or something; not much we can // do here. return false; } } else { toStringResult_ = JS::ConstUTF8CharsZ(utf8Message, strlen(utf8Message)); } return true; } bool JS::ErrorReportBuilder::populateUncaughtExceptionReportUTF8( JSContext* cx, HandleObject stack, ...) { va_list ap; va_start(ap, stack); bool ok = populateUncaughtExceptionReportUTF8VA(cx, stack, ap); va_end(ap); return ok; } bool JS::ErrorReportBuilder::populateUncaughtExceptionReportUTF8VA( JSContext* cx, HandleObject stack, va_list ap) { new (&ownedReport) JSErrorReport(); ownedReport.isWarning_ = false; ownedReport.errorNumber = JSMSG_UNCAUGHT_EXCEPTION; bool skippedAsync; RootedSavedFrame frame( cx, UnwrapSavedFrame(cx, cx->realm()->principals(), stack, SavedFrameSelfHosted::Exclude, skippedAsync)); if (frame) { filename = StringToNewUTF8CharsZ(cx, *frame->getSource()); if (!filename) { return false; } // |ownedReport.filename| inherits the lifetime of |ErrorReport::filename|. ownedReport.filename = filename.get(); ownedReport.sourceId = frame->getSourceId(); ownedReport.lineno = frame->getLine(); // Follow FixupColumnForDisplay and set column to 1 for WASM. ownedReport.column = frame->isWasm() ? 1 : frame->getColumn(); ownedReport.isMuted = frame->getMutedErrors(); } else { // XXXbz this assumes the stack we have right now is still // related to our exception object. NonBuiltinFrameIter iter(cx, cx->realm()->principals()); if (!iter.done()) { ownedReport.filename = iter.filename(); uint32_t column; ownedReport.sourceId = iter.hasScript() ? iter.script()->scriptSource()->id() : 0; ownedReport.lineno = iter.computeLine(&column); ownedReport.column = FixupColumnForDisplay(column); ownedReport.isMuted = iter.mutedErrors(); } } if (!ExpandErrorArgumentsVA(cx, GetErrorMessage, nullptr, JSMSG_UNCAUGHT_EXCEPTION, ArgumentsAreUTF8, &ownedReport, ap)) { return false; } toStringResult_ = ownedReport.message(); reportp = &ownedReport; return true; } JSObject* js::CopyErrorObject(JSContext* cx, Handle err) { UniquePtr copyReport; if (JSErrorReport* errorReport = err->getErrorReport()) { copyReport = CopyErrorReport(cx, errorReport); if (!copyReport) { return nullptr; } } RootedString message(cx, err->getMessage()); if (message && !cx->compartment()->wrap(cx, &message)) { return nullptr; } RootedString fileName(cx, err->fileName(cx)); if (!cx->compartment()->wrap(cx, &fileName)) { return nullptr; } RootedObject stack(cx, err->stack()); if (!cx->compartment()->wrap(cx, &stack)) { return nullptr; } uint32_t sourceId = err->sourceId(); uint32_t lineNumber = err->lineNumber(); uint32_t columnNumber = err->columnNumber(); JSExnType errorType = err->type(); // Create the Error object. return ErrorObject::create(cx, errorType, stack, fileName, sourceId, lineNumber, columnNumber, std::move(copyReport), message); } JS_PUBLIC_API bool JS::CreateError(JSContext* cx, JSExnType type, HandleObject stack, HandleString fileName, uint32_t lineNumber, uint32_t columnNumber, JSErrorReport* report, HandleString message, MutableHandleValue rval) { cx->check(stack, fileName, message); AssertObjectIsSavedFrameOrWrapper(cx, stack); js::UniquePtr rep; if (report) { rep = CopyErrorReport(cx, report); if (!rep) { return false; } } JSObject* obj = js::ErrorObject::create(cx, type, stack, fileName, 0, lineNumber, columnNumber, std::move(rep), message); if (!obj) { return false; } rval.setObject(*obj); return true; } const char* js::ValueToSourceForError(JSContext* cx, HandleValue val, UniqueChars& bytes) { if (val.isUndefined()) { return "undefined"; } if (val.isNull()) { return "null"; } AutoClearPendingException acpe(cx); RootedString str(cx, JS_ValueToSource(cx, val)); if (!str) { return "<>"; } JSStringBuilder sb(cx); if (val.isObject()) { RootedObject valObj(cx, val.toObjectOrNull()); ESClass cls; if (!JS::GetBuiltinClass(cx, valObj, &cls)) { return "<>"; } const char* s; if (cls == ESClass::Array) { s = "the array "; } else if (cls == ESClass::ArrayBuffer) { s = "the array buffer "; } else if (JS_IsArrayBufferViewObject(valObj)) { s = "the typed array "; } else { s = "the object "; } if (!sb.append(s, strlen(s))) { return "<>"; } } else if (val.isNumber()) { if (!sb.append("the number ")) { return "<>"; } } else if (val.isString()) { if (!sb.append("the string ")) { return "<>"; } } else if (val.isBigInt()) { if (!sb.append("the BigInt ")) { return "<>"; } } else { MOZ_ASSERT(val.isBoolean() || val.isSymbol()); bytes = StringToNewUTF8CharsZ(cx, *str); return bytes.get(); } if (!sb.append(str)) { return "<>"; } str = sb.finishString(); if (!str) { return "<>"; } bytes = StringToNewUTF8CharsZ(cx, *str); return bytes.get(); } bool js::GetInternalError(JSContext* cx, unsigned errorNumber, MutableHandleValue error) { FixedInvokeArgs<1> args(cx); args[0].set(Int32Value(errorNumber)); return CallSelfHostedFunction(cx, cx->names().GetInternalError, NullHandleValue, args, error); } bool js::GetTypeError(JSContext* cx, unsigned errorNumber, MutableHandleValue error) { FixedInvokeArgs<1> args(cx); args[0].set(Int32Value(errorNumber)); return CallSelfHostedFunction(cx, cx->names().GetTypeError, NullHandleValue, args, error); } bool js::GetAggregateError(JSContext* cx, unsigned errorNumber, MutableHandleValue error) { FixedInvokeArgs<1> args(cx); args[0].set(Int32Value(errorNumber)); return CallSelfHostedFunction(cx, cx->names().GetAggregateError, NullHandleValue, args, error); }