// Copyright (c) 2017-2022 Cloudflare, Inc. // Licensed under the Apache 2.0 license found in the LICENSE file or at: // https://opensource.org/licenses/Apache-2.0 #include "dom-exception.h" #include "jsg.h" // can't include util.h directly due to weird cyclic dependency... #include "ser.h" #include "setup.h" #include #include #include #include #include #include #include #if !_WIN32 #include #endif namespace workerd::jsg { bool getCaptureThrowsAsRejections(v8::Isolate* isolate) { auto& jsgIsolate = *reinterpret_cast(isolate->GetData(SET_DATA_ISOLATE_BASE)); return jsgIsolate.getCaptureThrowsAsRejections(); } bool getShouldSetToStringTag(v8::Isolate* isolate) { auto& jsgIsolate = *reinterpret_cast(isolate->GetData(SET_DATA_ISOLATE_BASE)); return jsgIsolate.shouldSetToStringTag(); } bool getShouldSetImmutablePrototype(v8::Isolate* isolate) { auto& jsgIsolate = *reinterpret_cast(isolate->GetData(SET_DATA_ISOLATE_BASE)); return jsgIsolate.shouldSetImmutablePrototype(); } bool getSpecCompliantPropertyAttributes(v8::Isolate* isolate) { auto& jsgIsolate = *reinterpret_cast(isolate->GetData(SET_DATA_ISOLATE_BASE)); return jsgIsolate.shouldUseSpecCompliantPropertyAttributes(); } #if _WIN32 kj::String fullyQualifiedTypeName(const std::type_info& type) { // type.name() returns a human-readable name on Windows: // https://learn.microsoft.com/en-us/cpp/cpp/type-info-class?view=msvc-170 kj::StringPtr name = type.name(); // Remove struct prefix if (name.startsWith("struct ")) { name = name.slice(7); } // Remove class prefix if (name.startsWith("class ")) { name = name.slice(6); } kj::String result = kj::str(name); // Replace instances of `anonymous namespace' with (anonymous namespace) for (auto& c: result.asArray()) { if (c == '`') c = '('; else if (c == '\'') c = ')'; } return kj::mv(result); } #else kj::String fullyQualifiedTypeName(const std::type_info& type) { int status; char* buf = abi::__cxa_demangle(type.name(), nullptr, nullptr, &status); kj::String result = kj::str(buf == nullptr ? type.name() : buf); free(buf); return kj::mv(result); } #endif kj::String typeName(const std::type_info& type) { auto result = fullyQualifiedTypeName(type); // Strip namespace, if any. KJ_IF_SOME(pos, result.findLast(':')) { result = kj::str(result.slice(pos + 1)); } // Strip template args, if any. // // TODO(someday): Maybe just strip namespaces from each arg? KJ_IF_SOME(pos, result.findFirst('<')) { result = kj::str(result.first(pos)); } return kj::mv(result); } namespace { // For internal errors, we generate an ID to include when rendering user-facing "internal error" // exceptions and writing internal exception logs, to make it easier to search for logs // corresponding to "internal error" exceptions reported by users. // // We'll use an ID of 24 base-32 encoded characters, just because its relatively simple to // generate from random bytes. This should give us a value with 120 bits of uniqueness, which is // about as good as a UUID. // // (We're not using base-64 encoding to avoid issues with case insensitive search, as well as // ensuring that the id is easy to select and copy via double-clicking.) using InternalErrorId = kj::FixedArray; constexpr char BASE32_DIGITS[] = "0123456789abcdefghijklmnopqrstuv"; InternalErrorId makeInternalErrorId() { InternalErrorId id; if (isPredictableModeForTest()) { // In testing mode, use content that generates a "0123456789abcdefghijklm" ID: for (auto i: kj::indices(id)) { id[i] = i; } } else { getEntropy(kj::asBytes(id)); } for (auto i: kj::indices(id)) { id[i] = BASE32_DIGITS[static_cast(id[i]) % 32]; } return id; } kj::String renderInternalError(InternalErrorId& internalErrorId) { return kj::str("internal error; reference = ", internalErrorId); } } // namespace v8::Local makeInternalError(v8::Isolate* isolate, kj::StringPtr internalMessage) { auto wdErrId = makeInternalErrorId(); KJ_LOG(ERROR, internalMessage, wdErrId); return v8::Exception::Error(v8Str(isolate, renderInternalError(wdErrId))); } namespace { kj::StringPtr trimErrorMessage(kj::StringPtr errorString) { // For strings beginning with ':' OWS, returns everything after the OWS. Otherwise returns the // empty string. if (errorString.startsWith(":")) { errorString = errorString.slice(1); while (errorString.startsWith(" ")) { errorString = errorString.slice(1); } return errorString; } return ""; } bool setRemoteError(v8::Isolate* isolate, v8::Local& exception) { // If an exception was tunneled, we add a property `.remote` to the Javascript error. KJ_ASSERT(exception->IsObject()); auto obj = exception.As(); return jsg::check(obj->Set( isolate->GetCurrentContext(), jsg::v8StrIntern(isolate, "remote"_kj), v8::True(isolate))); } bool setRetryableError(v8::Isolate* isolate, v8::Local& exception) { KJ_ASSERT(exception->IsObject()); auto obj = exception.As(); return jsg::check(obj->Set( isolate->GetCurrentContext(), jsg::v8StrIntern(isolate, "retryable"_kj), v8::True(isolate))); } bool setOverloadedError(v8::Isolate* isolate, v8::Local& exception) { KJ_ASSERT(exception->IsObject()); auto obj = exception.As(); return jsg::check(obj->Set( isolate->GetCurrentContext(), jsg::v8StrIntern(isolate, "overloaded"_kj), v8::True(isolate))); } bool setDurableObjectResetError(v8::Isolate* isolate, v8::Local& exception) { KJ_ASSERT(exception->IsObject()); auto obj = exception.As(); return jsg::check(obj->Set(isolate->GetCurrentContext(), jsg::v8StrIntern(isolate, "durableObjectReset"_kj), v8::True(isolate))); } struct DecodedException { v8::Local handle; bool isInternal; bool isFromRemote; bool isDurableObjectReset; // TODO(cleanup): Maybe<> is redundant with isInternal flag field? kj::Maybe internalErrorId; bool isDisconnection; bool isDoNotLogException; }; DecodedException decodeTunneledException( v8::Isolate* isolate, const kj::Exception& exception, const ExceptionToJsOptions& options) { // We currently support tunneling the following error types: // // - Error: While the Web IDL spec claims this is reserved for use by program authors, this // is broadly useful as a general-purpose error type. // - RangeError: Commonly thrown by web API implementations. // - TypeError: Commonly thrown by web API implementations. // - SyntaxError: Especially from JSON parsing. // - ReferenceError: Not thrown by our APIs, but could be tunneled from user code. // - DOMException: Commonly thrown by web API implementations. // // https://heycam.github.io/webidl/#idl-exceptions // // TODO(someday): Support arbitrary user-defined error types, not just Error? auto tunneledInfo = tunneledErrorType(exception.getDescription()); DecodedException result; result.isDisconnection = false; result.isDoNotLogException = tunneledInfo.isDoNotLogException; auto errorType = tunneledInfo.message; auto appMessage = [&](kj::StringPtr errorString) -> kj::String { if (tunneledInfo.isInternal) { result.internalErrorId = makeInternalErrorId(); return renderInternalError(KJ_ASSERT_NONNULL(result.internalErrorId)); } else { return kj::str(trimErrorMessage(errorString)); } }; result.isInternal = tunneledInfo.isInternal; result.isFromRemote = tunneledInfo.isFromRemote; result.isDurableObjectReset = tunneledInfo.isDurableObjectReset; auto addAdditionalInfo = [isolate, &result, &exception]() { if (!result.handle->IsObject()) return; // Note that if the error was deserialized from the TUNNELED_EXCEPTION_DETAIL_ID detail, // these operations may overwrite properties that were already set on the serialized // error object. That is fine, we want the metadata captured in the kj::Exception // description to take precedence. // TODO(someday): Maybe consider making this configurable when a deserialized // error is used? if (result.isFromRemote) { setRemoteError(isolate, result.handle); } if (exception.getType() == kj::Exception::Type::DISCONNECTED) { setRetryableError(isolate, result.handle); } else if (exception.getType() == kj::Exception::Type::OVERLOADED) { setOverloadedError(isolate, result.handle); } if (result.isDurableObjectReset) { setDurableObjectResetError(isolate, result.handle); } }; if (tunneledInfo.isJsgError) { // DOMExceptions require a parenthesized error name argument, like DOMException(SyntaxError). // TODO(someday): We always handle DOMException specifically here rather than decoding from // the serialized detail because using the detail breaks some tests that expect a specific // error details. We'll need to investigate further to see if we can make this more consistent. if (errorType.startsWith("DOMException(")) { errorType = errorType.slice(strlen("DOMException(")); // Check for closing brace KJ_IF_SOME(closeParen, errorType.findFirst(')')) { auto& js = Lock::from(isolate); auto errorName = kj::str(errorType.first(closeParen)); auto message = appMessage(errorType.slice(1 + closeParen)); auto exception = js.domException(kj::mv(errorName), kj::mv(message)); result.handle = KJ_ASSERT_NONNULL(exception.tryGetHandle(js)); addAdditionalInfo(); return result; } } auto& isolateBase = IsolateBase::from(isolate); if ((options.trusted || isolateBase.getUsingEnhancedErrorSerialization()) && !options.ignoreDetail) { // If the error was originally converted from a JS error, then we likely have // serialized the original error object as a detail, if so, let's try to use // that, otherwise, we'll fall back to constructing a new error object. If // the ignoreDetail optiom is set, we skip trying to deserialize. KJ_IF_SOME(serializedJsError, exception.getDetail(jsg::TUNNELED_EXCEPTION_DETAIL_ID)) { kj::Maybe deserialized; v8::TryCatch tryCatch(isolate); try { auto& js = Lock::from(isolate); jsg::Deserializer deser(js, serializedJsError, kj::none, kj::none, jsg::Deserializer::Options{ // By default, we do not preserve stacks in deserialized errors because // of concerns sharing stack details over potentially untrusted boundaries. // However, if the caller has explicitly indicated that the scope it trusted, // we will preserve the stack in the deserialized error. .preserveStackInErrors = options.trusted, }); result.handle = deser.readValue(js); // If the result came from a serialized JS detail, it might not be an object! // If that's the case, and allowNonObjects is false (the default), we will ignore // the deserialized data and fallback to the normal decoding below. if (result.handle->IsObject() || options.allowNonObjects) { addAdditionalInfo(); return result; } } catch (jsg::JsExceptionThrown&) { if (!tryCatch.CanContinue()) { tryCatch.ReThrow(); throw; } // Failed to deserialize, we'll ignore the error and continue with the original // decoding below. For debugging purposes, when verbose logging is enabled, we // will at least log the error. if (!tryCatch.Exception().IsEmpty()) { KJ_LOG(INFO, "Failed to deserialize tunneled JS error detail", tryCatch.Exception()); } else { KJ_LOG(INFO, "Failed to deserialize tunneled JS error detail (unknown error)"); } } } } // It's neither a DOMException nor are we using a serialized detail, so it must be // one of the standard JS error types (or we will treat it as such). #define HANDLE_AND_RETURN_V8_ERROR(error_name, error_type) \ if (errorType.startsWith(error_name)) { \ auto message = appMessage(errorType.slice(strlen(error_name))); \ result.handle = v8::Exception::error_type(v8Str(isolate, message)); \ addAdditionalInfo(); \ return result; \ } JS_ERROR_TYPES(HANDLE_AND_RETURN_V8_ERROR) #undef HANDLE_AND_RETURN_V8_ERROR } // It's not a tunneled JavaScript error type that we recognize. // Return an internal error. result.isInternal = true; // For disconnection errors, we ignore any tunneled error info and just return // a generic "Network connection lost" error. One thing to keep in mind is that // DOMExceptions with the AbortError name are also DISCONNECTED errors, but those // are handled above as tunneled JS errors. It is important that we preserve the // ordering of these checks, so keep this if block after the tunneled JS error // handling above. if (exception.getType() == kj::Exception::Type::DISCONNECTED) { result.isDisconnection = true; result.handle = v8::Exception::Error(v8StrIntern(isolate, "Network connection lost."_kj)); if (tunneledInfo.isFromRemote) { setRemoteError(isolate, result.handle); } // DISCONNECTED exceptions are considered retryable setRetryableError(isolate, result.handle); if (tunneledInfo.isDurableObjectReset) { setDurableObjectResetError(isolate, result.handle); } } else { // For everything return a generic error with an internal error id. result.internalErrorId = makeInternalErrorId(); result.handle = v8::Exception::Error( v8Str(isolate, renderInternalError(KJ_ASSERT_NONNULL(result.internalErrorId)))); addAdditionalInfo(); } return result; } } // namespace kj::StringPtr extractTunneledExceptionDescription(kj::StringPtr message) { auto tunneledError = tunneledErrorType(message); if (tunneledError.isInternal) { // TODO(soon): Include an internal error ID in message, and also return the id. return "Error: internal error"; } else { return tunneledError.message; } } v8::Local exceptionToJs( v8::Isolate* isolate, kj::Exception&& exception, ExceptionToJsOptions options) { // TODO(cleanup): decodeTunneledException is currently only used here, consider // inlining it back into this function. auto tunneledException = decodeTunneledException(isolate, exception, options); if (tunneledException.isInternal) { // Don't log exceptions that have been explicitly marked with worker_do_not_log or are // DISCONNECTED exceptions as these are unlikely to represent bugs worth tracking. bool shouldLogWithInternalId = !tunneledException.isDisconnection && !tunneledException.isDoNotLogException; auto& observer = IsolateBase::from(isolate).getObserver(); observer.reportInternalException(exception, { .isInternal = tunneledException.isInternal, .isFromRemote = tunneledException.isFromRemote, .isDurableObjectReset = tunneledException.isDurableObjectReset, .internalErrorId = shouldLogWithInternalId ? tunneledException.internalErrorId : kj::none, }); if (shouldLogWithInternalId) { // LOG_EXCEPTION("jsgInternalError", ...), but with internal error ID: auto& e = exception; constexpr auto sentryErrorContext = "jsgInternalError"; auto& wdErrId = KJ_ASSERT_NONNULL(tunneledException.internalErrorId); KJ_LOG(ERROR, e, sentryErrorContext, wdErrId); } else { KJ_LOG(INFO, exception); // Run with --verbose to see exception logs. } } return tunneledException.handle; } Value Lock::exceptionToJs(kj::Exception&& exception, ExceptionToJsOptions options) { return withinHandleScope( [&] { return Value(v8Isolate, jsg::exceptionToJs(v8Isolate, kj::mv(exception), options)); }); } JsRef Lock::exceptionToJsValue(kj::Exception&& exception, ExceptionToJsOptions options) { return withinHandleScope([&] { JsValue val = JsValue(jsg::exceptionToJs(v8Isolate, kj::mv(exception), options)); return val.addRef(*this); }); } void Lock::throwException(Value&& exception) { withinHandleScope([&] { v8Isolate->ThrowException(exception.getHandle(*this)); }); throw JsExceptionThrown(); } void Lock::throwException(const JsValue& exception) { withinHandleScope([&] { v8Isolate->ThrowException(exception); }); throw JsExceptionThrown(); } void throwInternalError(v8::Isolate* isolate, kj::StringPtr internalMessage) { isolate->ThrowException(makeInternalError(isolate, internalMessage)); } void throwInternalError( v8::Isolate* isolate, kj::Exception&& exception, ExceptionToJsOptions options) { KJ_IF_SOME(renderingError, kj::runCatchingExceptions([&]() { isolate->ThrowException(exceptionToJs(isolate, kj::mv(exception), options)); })) { KJ_LOG(ERROR, "error rendering exception", renderingError); KJ_LOG(ERROR, exception); throwInternalError(isolate, "error rendering exception"); } } void addExceptionDetail(Lock& js, kj::Exception& exception, v8::Local handle) { v8::TryCatch tryCatch(js.v8Isolate); try { Serializer ser(js, {// Make sure we don't break compatibility if V8 introduces a new version. This value can // be bumped to match the new version once all of production is updated to understand it. .version = 15}); ser.write(js, JsValue(handle)); exception.setDetail(TUNNELED_EXCEPTION_DETAIL_ID, ser.release().data); } catch (JsExceptionThrown&) { // Either: // a. The exception is not serializable, and we caught the exception. We will just ignore it // and proceed without annotating. // b. The isolate's execution is being terminated, and so tryCatch.CanContinue() is false. In // this case we cannot serialize the exception, but again we'll just move on without the // annotation. } } void addJsExceptionMetadata(Lock& js, kj::Exception& exception, v8::Local handle) { // Extract JavaScript error type and stack trace if (!handle->IsObject()) { return; // Not an error object, nothing to extract } auto errorObj = jsg::JsObject(handle.As()); // Build Cap'n Proto message capnp::MallocMessageBuilder message; auto metadata = message.initRoot(); // Limit for user-controlled fields (4KB) constexpr size_t MAX_FIELD_SIZE = 4096; // Extract error name (e.g., "Error", "TypeError", "RangeError") auto nameProp = errorObj.get(js, "name"_kj); if (nameProp.isString()) { auto errorType = nameProp.toString(js); // Truncate to 4KB if needed if (errorType.size() > MAX_FIELD_SIZE) { errorType = kj::str(errorType.slice(0, MAX_FIELD_SIZE)); } metadata.setErrorType(errorType); } // Extract stack trace string auto stackProp = errorObj.get(js, "stack"_kj); if (stackProp.isString()) { auto stackTrace = stackProp.toString(js); // Truncate to 4KB if needed if (stackTrace.size() > MAX_FIELD_SIZE) { stackTrace = kj::str(stackTrace.slice(0, MAX_FIELD_SIZE)); } metadata.setStackTrace(stackTrace); } // Serialize to bytes using Cap'n Proto auto words = capnp::messageToFlatArray(message); exception.setDetail(JS_EXCEPTION_METADATA_DETAIL_ID, kj::heapArray(words.asBytes())); } static kj::String typeErrorMessage(TypeErrorContext c, const char* expectedType) { kj::String type; KJ_IF_SOME(t, c.type) { type = typeName(t); } switch (c.kind) { case TypeErrorContext::METHOD_ARGUMENT: return kj::str("Failed to execute '", c.memberName, "' on '", type, "': parameter ", c.argumentIndex + 1, " is not of type '", expectedType, "'."); case TypeErrorContext::CONSTRUCTOR_ARGUMENT: return kj::str("Failed to construct '", type, "': constructor parameter ", c.argumentIndex + 1, " is not of type '", expectedType, "'."); case TypeErrorContext::SETTER_ARGUMENT: return kj::str("Failed to set the '", c.memberName, "' property on '", type, "': the provided value is not of type '", expectedType, "'."); case TypeErrorContext::STRUCT_FIELD: return kj::str("Incorrect type for the '", c.memberName, "' field on '", type, "': the provided value is not of type '", expectedType, "'."); case TypeErrorContext::ARRAY_ELEMENT: return kj::str("Incorrect type for array element ", c.argumentIndex, ": the provided value is not of type '", expectedType, "'."); case TypeErrorContext::CALLBACK_ARGUMENT: return kj::str("Failed to execute function: parameter ", c.argumentIndex + 1, " is not of type '", expectedType, "'."); case TypeErrorContext::CALLBACK_RETURN: return kj::str("Callback returned incorrect type; expected '", expectedType, "'"); case TypeErrorContext::DICT_KEY: return kj::str("Incorrect type for map entry '", c.memberName, "': the provided key is not of type '", expectedType, "'."); case TypeErrorContext::DICT_FIELD: return kj::str("Incorrect type for map entry '", c.memberName, "': the provided value is not of type '", expectedType, "'."); case TypeErrorContext::PROMISE_RESOLUTION: return kj::str( "Incorrect type for Promise: the Promise did not resolve to '", expectedType, "'."); case TypeErrorContext::OTHER: return kj::str("Incorrect type: the provided value is not of type '", expectedType, "'."); }; KJ_UNREACHABLE; } static kj::String unimplementedErrorMessage(TypeErrorContext c) { kj::String type; KJ_IF_SOME(t, c.type) { type = typeName(t); } switch (c.kind) { case TypeErrorContext::METHOD_ARGUMENT: return kj::str("Failed to execute '", c.memberName, "' on '", type, "': parameter ", c.argumentIndex + 1, " is not implemented."); case TypeErrorContext::CONSTRUCTOR_ARGUMENT: return kj::str("Failed to construct '", type, "': constructor parameter ", c.argumentIndex + 1, " is not implemented."); case TypeErrorContext::SETTER_ARGUMENT: return kj::str("Failed to set the '", c.memberName, "' property on '", type, "': the ability to set this property is not implemented."); case TypeErrorContext::STRUCT_FIELD: return kj::str("The '", c.memberName, "' field on '", type, "' is not implemented."); case TypeErrorContext::ARRAY_ELEMENT: KJ_UNREACHABLE; case TypeErrorContext::CALLBACK_ARGUMENT: return kj::str( "Failed to execute function: parameter ", c.argumentIndex + 1, " is not implemented."); case TypeErrorContext::CALLBACK_RETURN: KJ_UNREACHABLE; case TypeErrorContext::DICT_KEY: KJ_UNREACHABLE; case TypeErrorContext::DICT_FIELD: KJ_UNREACHABLE; case TypeErrorContext::PROMISE_RESOLUTION: KJ_UNREACHABLE; case TypeErrorContext::OTHER: KJ_UNREACHABLE; }; KJ_UNREACHABLE; } void throwTypeError(v8::Isolate* isolate, kj::StringPtr message) { isolate->ThrowException(v8::Exception::TypeError(v8Str(isolate, message))); throw JsExceptionThrown(); } void throwTypeError(v8::Isolate* isolate, TypeErrorContext errorContext, kj::String expectedType) { kj::String message = typeErrorMessage(errorContext, expectedType.cStr()); throwTypeError(isolate, message); } void throwTypeError(v8::Isolate* isolate, TypeErrorContext errorContext, const char* expectedType) { kj::String message = typeErrorMessage(errorContext, expectedType); throwTypeError(isolate, message); } void throwTypeError( v8::Isolate* isolate, TypeErrorContext errorContext, const std::type_info& expectedType) { if (expectedType == typeid(Unimplemented)) { isolate->ThrowError(v8StrIntern(isolate, unimplementedErrorMessage(errorContext))); throw JsExceptionThrown(); } else { throwTypeError(isolate, errorContext, typeName(expectedType).cStr()); } } static constexpr auto kIllegalConstructorMessage = "Illegal constructor"; void throwIllegalConstructor(const v8::FunctionCallbackInfo& args) { auto isolate = args.GetIsolate(); isolate->ThrowException( v8::Exception::TypeError(v8StrIntern(isolate, kIllegalConstructorMessage))); } void throwTunneledException(v8::Isolate* isolate, v8::Local exception) { kj::throwFatalException(createTunneledException(isolate, exception)); } kj::Exception createTunneledException(v8::Isolate* isolate, v8::Local exception) { auto& jsgIsolate = *reinterpret_cast(isolate->GetData(SET_DATA_ISOLATE_BASE)); auto& js = Lock::from(isolate); return jsgIsolate.unwrapException(js, isolate->GetCurrentContext(), exception); } kj::Exception Lock::exceptionToKj(Value&& exception) { return withinHandleScope( [&] { return createTunneledException(v8Isolate, exception.getHandle(*this)); }); } kj::Exception Lock::exceptionToKj(const JsValue& exception) { return withinHandleScope([&] { return createTunneledException(v8Isolate, exception); }); } static kj::byte DUMMY = 0; static kj::Array getEmptyArray() { // An older version of asBytes(), when given an empty ArrayBuffer, would often return an array // with zero size but non-empty start address. Meanwhile, it turns out that some code, // particularly in BoringSSL, does not like receiving a null pointer even when the length is // zero -- it will spuriously produce an error. We could carefully find all the places where // this is an issue and adjust the specific calls to avoid passing null pointers, but it is // easier to change `asBytes()` so that it never produces a null start address in the first // place. return kj::Array(&DUMMY, 0, kj::NullArrayDisposer::instance); } kj::Array asBytes(v8::Local arrayBuffer) { auto backing = arrayBuffer->GetBackingStore(); kj::ArrayPtr bytes(static_cast(backing->Data()), backing->ByteLength()); if (bytes == nullptr) { return getEmptyArray(); } else { return bytes.attach(kj::mv(backing)); } } kj::Array asBytes(v8::Local arrayBufferView) { auto backing = arrayBufferView->Buffer()->GetBackingStore(); kj::ArrayPtr buffer(static_cast(backing->Data()), backing->ByteLength()); auto sliceStart = arrayBufferView->ByteOffset(); auto sliceEnd = sliceStart + arrayBufferView->ByteLength(); KJ_ASSERT(buffer.size() >= sliceEnd); auto bytes = buffer.slice(sliceStart, sliceEnd); if (bytes == nullptr) { return getEmptyArray(); } else { return bytes.attach(kj::mv(backing)); } } // TODO(soon): If the returned kj::Array is used outside of the isolate lock, // we'll need to ensure it works correctly once MPK (Memory Protection Keys) enforcement // is fully in place. kj::Array asBytes(v8::Local sharedArrayBuffer) { auto backing = sharedArrayBuffer->GetBackingStore(); kj::ArrayPtr bytes(static_cast(backing->Data()), backing->ByteLength()); if (bytes == nullptr) { return getEmptyArray(); } else { return bytes.attach(kj::mv(backing)); } } void recursivelyFreeze(v8::Local context, v8::Local value) { if (value->IsArray()) { // Optimize array freezing (Array is a subclass of Object, but we can iterate it faster). v8::HandleScope scope(v8::Isolate::GetCurrent()); auto arr = value.As(); for (auto i: kj::zeroTo(arr->Length())) { recursivelyFreeze(context, check(arr->Get(context, i))); } check(arr->SetIntegrityLevel(context, v8::IntegrityLevel::kFrozen)); } else if (value->IsObject()) { v8::HandleScope scope(v8::Isolate::GetCurrent()); auto obj = value.As(); auto names = check(obj->GetPropertyNames(context, v8::KeyCollectionMode::kOwnOnly, v8::ALL_PROPERTIES, v8::IndexFilter::kIncludeIndices)); for (auto i: kj::zeroTo(names->Length())) { recursivelyFreeze(context, check(obj->Get(context, check(names->Get(context, i))))); } check(obj->SetIntegrityLevel(context, v8::IntegrityLevel::kFrozen)); } else { // Primitive type, nothing to do. } } v8::Local deepClone(v8::Local context, v8::Local value) { // This is implemented in the classic JSON restringification way. auto serialized = check(v8::JSON::Stringify(context, value)); return check(v8::JSON::Parse(context, serialized)); } namespace { v8::MaybeLocal makeRejectedPromise( v8::Isolate* isolate, v8::Local exception) { v8::Local resolver; auto context = isolate->GetCurrentContext(); if (!v8::Promise::Resolver::New(context).ToLocal(&resolver) || resolver->Reject(context, exception).IsNothing()) { return v8::MaybeLocal(); } return resolver->GetPromise(); }; void returnRejectedPromiseImpl(auto info, v8::Local exception, v8::TryCatch& tryCatch) { v8::Local promise; if (!makeRejectedPromise(info.GetIsolate(), exception).ToLocal(&promise)) { // If makeRejectedPromise fails, the tryCatch should have caught the error. // Let's rethrow it if it isn't terminal. if (tryCatch.CanContinue()) tryCatch.ReThrow(); } info.GetReturnValue().Set(promise); } } // namespace void returnRejectedPromise(const v8::FunctionCallbackInfo& info, v8::Local exception, v8::TryCatch& tryCatch) { returnRejectedPromiseImpl&>(info, exception, tryCatch); } void returnRejectedPromise(const v8::PropertyCallbackInfo& info, v8::Local exception, v8::TryCatch& tryCatch) { returnRejectedPromiseImpl&>(info, exception, tryCatch); } static ExternalStringAllocator& getAllocatorForIsolate(v8::Isolate* isolate) { return IsolateBase::from(isolate).getExternalStringAllocator(); } // Default allocator that uses standard new/delete. // We typically don't use the new/delete operators directly, // but in this case we have to because V8's ExternalStringResource may default to `delete this` // if not overridden, and we are allocating raw byte arrays for placement new. class DefaultExternalStringAllocator final: public ExternalStringAllocator { public: void* allocate(size_t size) override { return operator new(size); } void deallocate(void* ptr) override { operator delete(ptr); } }; kj::Own defaultExternalStringAllocator() { static DefaultExternalStringAllocator allocator; return kj::Own(&allocator, kj::NullDisposer::instance); } // ====================================================================================== template class ExternString: public Type { // The implementation of ExternString here is very closely after the implementation of the same // class in Node.js, with modifications to fit our conventions. It is distributed under the // same MIT license that Node.js uses. The appropriate copyright attribution is included here: // // Copyright Node.js contributors. All rights reserved. // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. public: inline const Data* data() const override { return buf.begin(); } inline size_t length() const override { return buf.size(); } inline uint64_t byteLength() const { return length() * sizeof(Data); } // Override Dispose() so that V8 properly deallocates through the configured // ExternalStringAllocator rather than using `delete this` (the default). void Dispose() override { auto& allocator = getAllocatorForIsolate(isolate); this->~ExternString(); allocator.deallocate(this); } static v8::MaybeLocal createExtern( v8::Isolate* isolate, kj::ArrayPtr& buf) { if (buf.size() == 0) { return v8::String::Empty(isolate); } // TODO(now): In Node.js impl, we check to see if length is less than a specified // minimum. If it is, it's likely more efficient to just copy and use a regular // heap allocated string than an external. We're not doing that here currently, but // we might? auto& allocator = getAllocatorForIsolate(isolate); auto mem = allocator.allocate(sizeof(ExternString)); if (mem == nullptr) { isolate->ThrowException(v8::Exception::Error( v8::String::NewFromUtf8Literal(isolate, "String allocation failed"))); return v8::MaybeLocal(); } auto resource = new (mem) ExternString(isolate, buf); v8::MaybeLocal str; if constexpr (kj::isSameType()) { str = v8::String::NewExternalOneByte(isolate, resource); } else { // resource here must be a v8::String::ExternalStringResource. str = v8::String::NewExternalTwoByte(isolate, resource); } if (str.IsEmpty()) { // This should happen only if the string is too long resource->~ExternString(); allocator.deallocate(mem); isolate->ThrowException(v8::Exception::Error( v8::String::NewFromUtf8Literal(isolate, "String allocation failed"))); return v8::MaybeLocal(); } return str; } private: v8::Isolate* isolate; kj::ArrayPtr buf; inline ExternString(v8::Isolate* isolate, kj::ArrayPtr& buf) : isolate(isolate), buf(buf) {} }; using ExternOneByteString = ExternString; using ExternTwoByteString = ExternString; v8::Local newExternalOneByteString(Lock& js, kj::ArrayPtr buf) { return check(ExternOneByteString::createExtern(js.v8Isolate, buf)); } v8::Local newExternalTwoByteString(Lock& js, kj::ArrayPtr buf) { return check(ExternTwoByteString::createExtern(js.v8Isolate, buf)); } // ====================================================================================== // Module utilities JsObject createMutableModuleExports(Lock& js, JsObject moduleNamespace) { auto result = js.objNoProto(); auto names = moduleNamespace.getPropertyNames(js, OWN_ONLY, ALL_PROPERTIES, INCLUDE_INDICES); for (uint32_t i = 0; i < names.size(); i++) { auto name = names.get(js, i); result.set(js, name, moduleNamespace.get(js, name)); } return result; } // ====================================================================================== // Node.js Compat namespace { // This list must be kept in sync with the list of builtins from Node.js. // It should be unlikely that anything is ever removed from this list, and // adding items to it is considered a semver-major change in Node.js. static const std::set NODEJS_BUILTINS{"_http_agent"_kj, "_http_client"_kj, "_http_common"_kj, "_http_incoming"_kj, "_http_outgoing"_kj, "_http_server"_kj, "_stream_duplex"_kj, "_stream_passthrough"_kj, "_stream_readable"_kj, "_stream_transform"_kj, "_stream_wrap"_kj, "_stream_writable"_kj, "_tls_common"_kj, "_tls_wrap"_kj, "assert"_kj, "assert/strict"_kj, "async_hooks"_kj, "buffer"_kj, "child_process"_kj, "cluster"_kj, "console"_kj, "constants"_kj, "crypto"_kj, "dgram"_kj, "diagnostics_channel"_kj, "dns"_kj, "dns/promises"_kj, "domain"_kj, "events"_kj, "fs"_kj, "fs/promises"_kj, "http"_kj, "http2"_kj, "https"_kj, "inspector"_kj, "inspector/promises"_kj, "module"_kj, "net"_kj, "os"_kj, "path"_kj, "path/posix"_kj, "path/win32"_kj, "perf_hooks"_kj, "process"_kj, "punycode"_kj, "querystring"_kj, "readline"_kj, "readline/promises"_kj, "repl"_kj, "sqlite"_kj, "stream"_kj, "stream/consumers"_kj, "stream/promises"_kj, "stream/web"_kj, "string_decoder"_kj, "sys"_kj, "timers"_kj, "timers/promises"_kj, "tls"_kj, "trace_events"_kj, "tty"_kj, "url"_kj, "util"_kj, "util/types"_kj, "v8"_kj, "vm"_kj, "wasi"_kj, "worker_threads"_kj, "zlib"_kj}; } // namespace kj::Maybe checkNodeSpecifier(kj::StringPtr specifier) { // The sys module was renamed to 'util'. This shim remains to keep old programs // working. `sys` is deprecated and shouldn't be used. // Note to maintainers: Although this module has been deprecated for a while // Node.js do not plan to remove it. // See: https://github.com/nodejs/node/pull/35407#issuecomment-700693439 if (specifier == "sys" || specifier == "node:sys") [[unlikely]] { return kj::str("node:util"); } if (NODEJS_BUILTINS.contains(specifier)) { return kj::str("node:", specifier); } else if (specifier.startsWith("node:")) { return kj::str(specifier); } return kj::none; } bool isNodeJsCompatEnabled(jsg::Lock& js) { return IsolateBase::from(js.v8Isolate).isNodeJsCompatEnabled(); } bool isNodeJsProcessV2Enabled(jsg::Lock& js) { return IsolateBase::from(js.v8Isolate).isNodeJsProcessV2Enabled(); } bool isRequireReturnsDefaultExportEnabled(jsg::Lock& js) { return IsolateBase::from(js.v8Isolate).isRequireReturnsDefaultExportEnabled(); } } // namespace workerd::jsg