/* -*- 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 "vm/ErrorReporting.h" #include #include #include "jsexn.h" #include "jsfriendapi.h" #include "frontend/FrontendContext.h" // AutoReportFrontendContext #include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* #include "js/Printf.h" // JS_vsmprintf #include "js/Warnings.h" // JS::WarningReporter #include "vm/FrameIter.h" #include "vm/GlobalObject.h" #include "vm/JSContext.h" using namespace js; using JS::HandleObject; using JS::HandleValue; using JS::UniqueTwoByteChars; void js::CallWarningReporter(JSContext* cx, JSErrorReport* reportp) { MOZ_ASSERT(reportp->isWarning()); if (JS::WarningReporter warningReporter = cx->runtime()->warningReporter) { warningReporter(cx, reportp); } } bool js::CompileError::throwError(JSContext* cx) { if (isWarning()) { CallWarningReporter(cx, this); return true; } // If there's a runtime exception type associated with this error // number, set that as the pending exception. For errors occurring at // compile time, this is very likely to be a JSEXN_SYNTAXERR. return ErrorToException(cx, this, nullptr, nullptr); } bool js::ReportExceptionClosure::operator()(JSContext* cx) { cx->setPendingException(exn_, ShouldCaptureStack::Always); return false; } bool js::ReportCompileWarning(FrontendContext* fc, ErrorMetadata&& metadata, UniquePtr notes, unsigned errorNumber, va_list* args) { // On the main thread, report the error immediately. When compiling off // thread, save the error so that the thread finishing the parse can report // it later. CompileError err; err.notes = std::move(notes); err.isWarning_ = true; err.errorNumber = errorNumber; err.filename = metadata.filename; err.lineno = metadata.lineNumber; err.column = metadata.columnNumber; err.isMuted = metadata.isMuted; if (UniqueTwoByteChars lineOfContext = std::move(metadata.lineOfContext)) { err.initOwnedLinebuf(lineOfContext.release(), metadata.lineLength, metadata.tokenOffset); } if (!ExpandErrorArgumentsVA(fc, GetErrorMessage, nullptr, errorNumber, ArgumentsAreLatin1, &err, *args)) { return false; } return fc->reportWarning(std::move(err)); } static void ReportCompileErrorImpl(FrontendContext* fc, js::ErrorMetadata&& metadata, js::UniquePtr notes, unsigned errorNumber, va_list* args, ErrorArgumentsType argumentsType) { js::CompileError err; err.notes = std::move(notes); err.isWarning_ = false; err.errorNumber = errorNumber; err.filename = metadata.filename; err.lineno = metadata.lineNumber; err.column = metadata.columnNumber; err.isMuted = metadata.isMuted; if (UniqueTwoByteChars lineOfContext = std::move(metadata.lineOfContext)) { err.initOwnedLinebuf(lineOfContext.release(), metadata.lineLength, metadata.tokenOffset); } if (!js::ExpandErrorArgumentsVA(fc, js::GetErrorMessage, nullptr, errorNumber, argumentsType, &err, *args)) { return; } fc->reportError(std::move(err)); } void js::ReportCompileErrorLatin1(FrontendContext* fc, ErrorMetadata&& metadata, UniquePtr notes, unsigned errorNumber, va_list* args) { ReportCompileErrorImpl(fc, std::move(metadata), std::move(notes), errorNumber, args, ArgumentsAreLatin1); } void js::ReportCompileErrorUTF8(FrontendContext* fc, ErrorMetadata&& metadata, UniquePtr notes, unsigned errorNumber, va_list* args) { ReportCompileErrorImpl(fc, std::move(metadata), std::move(notes), errorNumber, args, ArgumentsAreUTF8); } void js::ReportErrorToGlobal(JSContext* cx, Handle global, HandleValue error) { MOZ_ASSERT(!cx->isExceptionPending()); #ifdef DEBUG // No assertSameCompartment version that doesn't take JSContext... if (error.isObject()) { AssertSameCompartment(global, &error.toObject()); } #endif // DEBUG js::ReportExceptionClosure report(error); PrepareScriptEnvironmentAndInvoke(cx, global, report); } static bool ReportError(JSContext* cx, JSErrorReport* reportp, JSErrorCallback callback, void* userRef) { if (reportp->isWarning()) { CallWarningReporter(cx, reportp); return true; } // Check the error report, and set a JavaScript-catchable exception // if the error is defined to have an associated exception. return ErrorToException(cx, reportp, callback, userRef); } /* * The given JSErrorReport object have been zeroed and must not outlive * cx->fp() (otherwise owned fields may become invalid). */ static void PopulateReportBlame(JSContext* cx, JSErrorReport* report) { JS::Realm* realm = cx->realm(); if (!realm) { return; } /* * Walk stack until we find a frame that is associated with a non-builtin * rather than a builtin frame and which we're allowed to know about. */ NonBuiltinFrameIter iter(cx, realm->principals()); if (iter.done()) { return; } report->filename = iter.filename(); if (iter.hasScript()) { report->sourceId = iter.script()->scriptSource()->id(); } uint32_t column; report->lineno = iter.computeLine(&column); report->column = FixupColumnForDisplay(column); report->isMuted = iter.mutedErrors(); } class MOZ_RAII AutoMessageArgs { size_t totalLength_; /* only {0} thru {9} supported */ mozilla::Array args_; mozilla::Array lengths_; uint16_t count_; bool allocatedElements_ : 1; public: AutoMessageArgs() : totalLength_(0), count_(0), allocatedElements_(false) { PodArrayZero(args_); } ~AutoMessageArgs() { /* free the arguments only if we allocated them */ if (allocatedElements_) { uint16_t i = 0; while (i < count_) { if (args_[i]) { js_free((void*)args_[i]); } i++; } } } const char* args(size_t i) const { MOZ_ASSERT(i < count_); return args_[i]; } size_t totalLength() const { return totalLength_; } size_t lengths(size_t i) const { MOZ_ASSERT(i < count_); return lengths_[i]; } uint16_t count() const { return count_; } /* Gather the arguments into an array, and accumulate their sizes. * * We could template on the type of argsArg, but we're already trusting people * to do the right thing with varargs, so might as well trust them on this * part too. Upstream consumers do assert that it's the right thing. Also, * if argsArg were strongly typed we'd still need casting below for this to * compile, because typeArg is not known at compile-time here. */ template bool init(Allocator* alloc, void* argsArg, uint16_t countArg, ErrorArgumentsType typeArg, va_list ap) { MOZ_ASSERT(countArg > 0); count_ = countArg; for (uint16_t i = 0; i < count_; i++) { switch (typeArg) { case ArgumentsAreASCII: case ArgumentsAreUTF8: { const char* c = argsArg ? static_cast(argsArg)[i] : va_arg(ap, const char*); args_[i] = c; MOZ_ASSERT_IF(typeArg == ArgumentsAreASCII, JS::StringIsASCII(args_[i])); lengths_[i] = strlen(args_[i]); break; } case ArgumentsAreLatin1: { MOZ_ASSERT(!argsArg); const Latin1Char* latin1 = va_arg(ap, Latin1Char*); size_t len = strlen(reinterpret_cast(latin1)); mozilla::Range range(latin1, len); char* utf8 = JS::CharsToNewUTF8CharsZ(alloc, range).c_str(); if (!utf8) { return false; } args_[i] = utf8; lengths_[i] = strlen(utf8); allocatedElements_ = true; break; } case ArgumentsAreUnicode: { const char16_t* uc = argsArg ? static_cast(argsArg)[i] : va_arg(ap, const char16_t*); size_t len = js_strlen(uc); mozilla::Range range(uc, len); char* utf8 = JS::CharsToNewUTF8CharsZ(alloc, range).c_str(); if (!utf8) { return false; } args_[i] = utf8; lengths_[i] = strlen(utf8); allocatedElements_ = true; break; } } totalLength_ += lengths_[i]; } return true; } }; /* * The arguments from ap need to be packaged up into an array and stored * into the report struct. * * The format string addressed by the error number may contain operands * identified by the format {N}, where N is a decimal digit. Each of these * is to be replaced by the Nth argument from the va_list. The complete * message is placed into reportp->message_. * * Returns true if the expansion succeeds (can fail if out of memory). * * messageArgs is a `const char**` or a `const char16_t**` but templating on * that is not worth it here because AutoMessageArgs takes a void* anyway, and * using void* here simplifies our callers a bit. */ template static bool ExpandErrorArgumentsHelper(FrontendContext* fc, JSErrorCallback callback, void* userRef, const unsigned errorNumber, void* messageArgs, ErrorArgumentsType argumentsType, T* reportp, va_list ap) { const JSErrorFormatString* efs; if (!callback) { callback = GetErrorMessage; } efs = fc->gcSafeCallback(callback, userRef, errorNumber); if (efs) { if constexpr (std::is_same_v) { reportp->exnType = efs->exnType; } MOZ_ASSERT(reportp->errorNumber == errorNumber); reportp->errorMessageName = efs->name; MOZ_ASSERT_IF(argumentsType == ArgumentsAreASCII, JS::StringIsASCII(efs->format)); uint16_t argCount = efs->argCount; MOZ_RELEASE_ASSERT(argCount <= JS::MaxNumErrorArguments); if (argCount > 0) { /* * Parse the error format, substituting the argument X * for {X} in the format. */ if (efs->format) { const char* fmt; char* out; #ifdef DEBUG int expandedArgs = 0; #endif size_t expandedLength; size_t len = strlen(efs->format); AutoMessageArgs args; if (!args.init(fc->getAllocator(), messageArgs, argCount, argumentsType, ap)) { return false; } expandedLength = len - (3 * args.count()) /* exclude the {n} */ + args.totalLength(); /* * Note - the above calculation assumes that each argument * is used once and only once in the expansion !!! */ char* utf8 = out = fc->getAllocator()->pod_malloc(expandedLength + 1); if (!out) { return false; } fmt = efs->format; while (*fmt) { if (*fmt == '{') { if (mozilla::IsAsciiDigit(fmt[1])) { int d = AsciiDigitToNumber(fmt[1]); MOZ_RELEASE_ASSERT(d < args.count()); strncpy(out, args.args(d), args.lengths(d)); out += args.lengths(d); fmt += 3; #ifdef DEBUG expandedArgs++; #endif continue; } } *out++ = *fmt++; } MOZ_ASSERT(expandedArgs == args.count()); *out = 0; reportp->initOwnedMessage(utf8); } } else { /* Non-null messageArgs should have at least one non-null arg. */ MOZ_ASSERT(!messageArgs); /* * Zero arguments: the format string (if it exists) is the * entire message. */ if (efs->format) { reportp->initBorrowedMessage(efs->format); } } } if (!reportp->message()) { /* where's the right place for this ??? */ const char* defaultErrorMessage = "No error message available for error number %d"; size_t nbytes = strlen(defaultErrorMessage) + 16; char* message = fc->getAllocator()->pod_malloc(nbytes); if (!message) { return false; } snprintf(message, nbytes, defaultErrorMessage, errorNumber); reportp->initOwnedMessage(message); } return true; } bool js::ExpandErrorArgumentsVA(FrontendContext* fc, JSErrorCallback callback, void* userRef, const unsigned errorNumber, const char16_t** messageArgs, ErrorArgumentsType argumentsType, JSErrorReport* reportp, va_list ap) { MOZ_ASSERT(argumentsType == ArgumentsAreUnicode); return ExpandErrorArgumentsHelper(fc, callback, userRef, errorNumber, messageArgs, argumentsType, reportp, ap); } bool js::ExpandErrorArgumentsVA(FrontendContext* fc, JSErrorCallback callback, void* userRef, const unsigned errorNumber, const char** messageArgs, ErrorArgumentsType argumentsType, JSErrorReport* reportp, va_list ap) { MOZ_ASSERT(argumentsType != ArgumentsAreUnicode); return ExpandErrorArgumentsHelper(fc, callback, userRef, errorNumber, messageArgs, argumentsType, reportp, ap); } bool js::ExpandErrorArgumentsVA(FrontendContext* fc, JSErrorCallback callback, void* userRef, const unsigned errorNumber, ErrorArgumentsType argumentsType, JSErrorReport* reportp, va_list ap) { return ExpandErrorArgumentsHelper(fc, callback, userRef, errorNumber, nullptr, argumentsType, reportp, ap); } bool js::ExpandErrorArgumentsVA(FrontendContext* fc, JSErrorCallback callback, void* userRef, const unsigned errorNumber, const char16_t** messageArgs, ErrorArgumentsType argumentsType, JSErrorNotes::Note* notep, va_list ap) { return ExpandErrorArgumentsHelper(fc, callback, userRef, errorNumber, messageArgs, argumentsType, notep, ap); } bool js::ReportErrorNumberVA(JSContext* cx, IsWarning isWarning, JSErrorCallback callback, void* userRef, const unsigned errorNumber, ErrorArgumentsType argumentsType, va_list ap) { JSErrorReport report; report.isWarning_ = isWarning == IsWarning::Yes; report.errorNumber = errorNumber; PopulateReportBlame(cx, &report); AutoReportFrontendContext fc(cx); if (!ExpandErrorArgumentsVA(&fc, callback, userRef, errorNumber, argumentsType, &report, ap)) { return false; } if (!ReportError(cx, &report, callback, userRef)) { return false; } return report.isWarning(); } template static bool ExpandErrorArguments(FrontendContext* fc, JSErrorCallback callback, void* userRef, const unsigned errorNumber, const CharT** messageArgs, js::ErrorArgumentsType argumentsType, JSErrorReport* reportp, ...) { va_list ap; va_start(ap, reportp); bool expanded = js::ExpandErrorArgumentsVA(fc, callback, userRef, errorNumber, messageArgs, argumentsType, reportp, ap); va_end(ap); return expanded; } template static bool ReportErrorNumberArray(JSContext* cx, IsWarning isWarning, JSErrorCallback callback, void* userRef, const unsigned errorNumber, const CharT** args) { static_assert( (argType == ArgumentsAreUnicode && std::is_same_v) || (argType != ArgumentsAreUnicode && std::is_same_v), "Mismatch between character type and argument type"); JSErrorReport report; report.isWarning_ = isWarning == IsWarning::Yes; report.errorNumber = errorNumber; PopulateReportBlame(cx, &report); AutoReportFrontendContext fc(cx); if (!ExpandErrorArguments(&fc, callback, userRef, errorNumber, args, argType, &report)) { return false; } if (!ReportError(cx, &report, callback, userRef)) { return false; } return report.isWarning(); } bool js::ReportErrorNumberUCArray(JSContext* cx, IsWarning isWarning, JSErrorCallback callback, void* userRef, const unsigned errorNumber, const char16_t** args) { return ReportErrorNumberArray( cx, isWarning, callback, userRef, errorNumber, args); } bool js::ReportErrorNumberUTF8Array(JSContext* cx, IsWarning isWarning, JSErrorCallback callback, void* userRef, const unsigned errorNumber, const char** args) { return ReportErrorNumberArray(cx, isWarning, callback, userRef, errorNumber, args); } bool js::ReportErrorVA(JSContext* cx, IsWarning isWarning, const char* format, js::ErrorArgumentsType argumentsType, va_list ap) { JSErrorReport report; UniqueChars message(JS_vsmprintf(format, ap)); if (!message) { ReportOutOfMemory(cx); return false; } MOZ_ASSERT_IF(argumentsType == ArgumentsAreASCII, JS::StringIsASCII(message.get())); report.isWarning_ = isWarning == IsWarning::Yes; report.errorNumber = JSMSG_USER_DEFINED_ERROR; if (argumentsType == ArgumentsAreASCII || argumentsType == ArgumentsAreUTF8) { report.initOwnedMessage(message.release()); } else { MOZ_ASSERT(argumentsType == ArgumentsAreLatin1); JS::Latin1Chars latin1(message.get(), strlen(message.get())); JS::UTF8CharsZ utf8(JS::CharsToNewUTF8CharsZ(cx, latin1)); if (!utf8) { return false; } report.initOwnedMessage(reinterpret_cast(utf8.get())); } PopulateReportBlame(cx, &report); if (!ReportError(cx, &report, nullptr, nullptr)) { return false; } return report.isWarning(); } void js::MaybePrintAndClearPendingException(JSContext* cx) { if (!cx->isExceptionPending()) { return; } AutoClearPendingException acpe(cx); JS::ExceptionStack exnStack(cx); if (!JS::StealPendingExceptionStack(cx, &exnStack)) { fprintf(stderr, "error getting pending exception\n"); return; } JS::ErrorReportBuilder report(cx); if (!report.init(cx, exnStack, JS::ErrorReportBuilder::WithSideEffects)) { fprintf(stderr, "out of memory initializing JS::ErrorReportBuilder\n"); return; } MOZ_ASSERT(!report.report()->isWarning()); JS::PrintError(stderr, report, true); }