/* * Copyright (C) 2008-2021 Apple Inc. All Rights Reserved. * Copyright (C) 2011, 2012 Google Inc. All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "config.h" #include "WorkerOrWorkletScriptController.h" #include "DedicatedWorkerGlobalScope.h" #include "EventLoop.h" #include "JSAudioWorkletGlobalScope.h" #include "JSDOMBinding.h" #include "JSDedicatedWorkerGlobalScope.h" #include "JSEventTarget.h" #include "JSExecState.h" #include "JSPaintWorkletGlobalScope.h" #include "JSServiceWorkerGlobalScope.h" #include "ModuleFetchFailureKind.h" #include "ModuleFetchParameters.h" #include "ScriptSourceCode.h" #include "WebCoreJSClientData.h" #include "WorkerConsoleClient.h" #include "WorkerModuleScriptLoader.h" #include "WorkerScriptFetcher.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace WebCore { using namespace JSC; WorkerOrWorkletScriptController::WorkerOrWorkletScriptController(WorkerThreadType type, Ref&& vm, WorkerOrWorkletGlobalScope* globalScope) : m_vm(WTFMove(vm)) , m_globalScope(globalScope) , m_globalScopeWrapper(*m_vm) { m_vm->heap.acquireAccess(); // It's not clear that we have good discipline for heap access, so turn it on permanently. { JSLockHolder lock(m_vm.get()); m_vm->ensureTerminationException(); } JSVMClientData::initNormalWorld(m_vm.get(), type); } WorkerOrWorkletScriptController::WorkerOrWorkletScriptController(WorkerThreadType type, WorkerOrWorkletGlobalScope* globalScope) : WorkerOrWorkletScriptController(type, JSC::VM::create(), globalScope) { } WorkerOrWorkletScriptController::~WorkerOrWorkletScriptController() { JSLockHolder lock(vm()); if (m_globalScopeWrapper) { m_globalScopeWrapper->clearDOMGuardedObjects(); m_globalScopeWrapper->setConsoleClient(nullptr); } m_globalScopeWrapper.clear(); m_vm = nullptr; } void WorkerOrWorkletScriptController::attachDebugger(JSC::Debugger* debugger) { initScriptIfNeeded(); debugger->attach(m_globalScopeWrapper.get()); } void WorkerOrWorkletScriptController::detachDebugger(JSC::Debugger* debugger) { debugger->detach(m_globalScopeWrapper.get(), JSC::Debugger::TerminatingDebuggingSession); } void WorkerOrWorkletScriptController::forbidExecution() { ASSERT(m_globalScope->isContextThread()); m_vm->setExecutionForbidden(); } bool WorkerOrWorkletScriptController::isExecutionForbidden() const { ASSERT(m_globalScope->isContextThread()); return m_vm->executionForbidden(); } void WorkerOrWorkletScriptController::scheduleExecutionTermination() { { // The mutex provides a memory barrier to ensure that once // termination is scheduled, isTerminatingExecution() will // accurately reflect that lexicalGlobalObject when called from another thread. Locker locker { m_scheduledTerminationLock }; if (m_isTerminatingExecution) return; m_isTerminatingExecution = true; } m_vm->notifyNeedTermination(); } bool WorkerOrWorkletScriptController::isTerminatingExecution() const { // See comments in scheduleExecutionTermination regarding mutex usage. Locker locker { m_scheduledTerminationLock }; return m_isTerminatingExecution; } void WorkerOrWorkletScriptController::releaseHeapAccess() { m_vm->heap.releaseAccess(); } void WorkerOrWorkletScriptController::acquireHeapAccess() { m_vm->heap.acquireAccess(); } void WorkerOrWorkletScriptController::addTimerSetNotification(JSC::JSRunLoopTimer::TimerNotificationCallback callback) { auto processTimer = [&] (JSRunLoopTimer* timer) { if (!timer) return; timer->addTimerSetNotification(callback); }; processTimer(m_vm->heap.fullActivityCallback()); processTimer(m_vm->heap.edenActivityCallback()); processTimer(m_vm->deferredWorkTimer.ptr()); } void WorkerOrWorkletScriptController::removeTimerSetNotification(JSC::JSRunLoopTimer::TimerNotificationCallback callback) { auto processTimer = [&] (JSRunLoopTimer* timer) { if (!timer) return; timer->removeTimerSetNotification(callback); }; processTimer(m_vm->heap.fullActivityCallback()); processTimer(m_vm->heap.edenActivityCallback()); processTimer(m_vm->deferredWorkTimer.ptr()); } void WorkerOrWorkletScriptController::setException(JSC::Exception* exception) { JSC::JSGlobalObject* lexicalGlobalObject = m_globalScopeWrapper.get(); VM& vm = lexicalGlobalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); throwException(lexicalGlobalObject, scope, exception); } void WorkerOrWorkletScriptController::disableEval(const String& errorMessage) { initScriptIfNeeded(); JSLockHolder lock { vm() }; m_globalScopeWrapper->setEvalEnabled(false, errorMessage); } void WorkerOrWorkletScriptController::disableWebAssembly(const String& errorMessage) { initScriptIfNeeded(); JSLockHolder lock { vm() }; m_globalScopeWrapper->setWebAssemblyEnabled(false, errorMessage); } void WorkerOrWorkletScriptController::evaluate(const ScriptSourceCode& sourceCode, String* returnedExceptionMessage) { if (isExecutionForbidden()) return; VM& vm = this->vm(); NakedPtr uncaughtException; evaluate(sourceCode, uncaughtException, returnedExceptionMessage); if ((uncaughtException && vm.isTerminationException(uncaughtException)) || isTerminatingExecution()) { forbidExecution(); return; } if (uncaughtException) { JSLockHolder lock(vm); reportException(m_globalScopeWrapper.get(), uncaughtException); } } void WorkerOrWorkletScriptController::evaluate(const ScriptSourceCode& sourceCode, NakedPtr& returnedException, String* returnedExceptionMessage) { if (isExecutionForbidden()) return; initScriptIfNeeded(); auto& globalObject = *m_globalScopeWrapper.get(); VM& vm = globalObject.vm(); JSLockHolder lock { vm }; JSExecState::profiledEvaluate(&globalObject, JSC::ProfilingReason::Other, sourceCode.jsSourceCode(), m_globalScopeWrapper->globalThis(), returnedException); if ((returnedException && vm.isTerminationException(returnedException)) || isTerminatingExecution()) { forbidExecution(); return; } if (returnedException) { if (m_globalScope->canIncludeErrorDetails(sourceCode.cachedScript(), sourceCode.url().string())) { // FIXME: It's not great that this can run arbitrary code to string-ify the value of the exception. // Do we need to do anything to handle that properly, if it, say, raises another exception? if (returnedExceptionMessage) *returnedExceptionMessage = returnedException->value().toWTFString(&globalObject); } else { // Overwrite the detailed error with a generic error. String genericErrorMessage { "Script error."_s }; if (returnedExceptionMessage) *returnedExceptionMessage = genericErrorMessage; returnedException = JSC::Exception::create(vm, createError(&globalObject, genericErrorMessage)); } } } static Identifier jsValueToModuleKey(JSGlobalObject* lexicalGlobalObject, JSValue value) { if (value.isSymbol()) return Identifier::fromUid(jsCast(value)->privateName()); ASSERT(value.isString()); return asString(value)->toIdentifier(lexicalGlobalObject); } JSC::JSValue WorkerOrWorkletScriptController::evaluateModule(JSC::JSModuleRecord& moduleRecord, JSC::JSValue awaitedValue, JSC::JSValue resumeMode) { auto& globalObject = *m_globalScopeWrapper.get(); VM& vm = globalObject.vm(); JSLockHolder lock { vm }; return moduleRecord.evaluate(&globalObject, awaitedValue, resumeMode); } MessageQueueWaitResult WorkerOrWorkletScriptController::loadModuleSynchronously(WorkerScriptFetcher& scriptFetcher, const ScriptSourceCode& sourceCode) { if (isExecutionForbidden()) return MessageQueueTerminated; initScriptIfNeeded(); auto& globalObject = *m_globalScopeWrapper.get(); VM& vm = globalObject.vm(); JSLockHolder lock { vm }; auto protector = makeRef(scriptFetcher); { auto& promise = JSExecState::loadModule(globalObject, sourceCode.jsSourceCode(), JSC::JSScriptFetcher::create(vm, { &scriptFetcher })); auto& fulfillHandler = *JSNativeStdFunction::create(vm, &globalObject, 1, String(), [protector](JSGlobalObject* globalObject, CallFrame* callFrame) -> JSC::EncodedJSValue { VM& vm = globalObject->vm(); JSLockHolder lock { vm }; auto scope = DECLARE_THROW_SCOPE(vm); Identifier moduleKey = jsValueToModuleKey(globalObject, callFrame->argument(0)); RETURN_IF_EXCEPTION(scope, { }); protector->notifyLoadCompleted(*moduleKey.impl()); return JSValue::encode(jsUndefined()); }); auto& rejectHandler = *JSNativeStdFunction::create(vm, &globalObject, 1, String(), [protector](JSGlobalObject* globalObject, CallFrame* callFrame) { VM& vm = globalObject->vm(); JSLockHolder lock { vm }; JSValue errorValue = callFrame->argument(0); if (errorValue.isObject()) { auto* object = JSC::asObject(errorValue); if (JSValue failureKindValue = object->getDirect(vm, static_cast(*vm.clientData).builtinNames().failureKindPrivateName())) { // This is host propagated error in the module loader pipeline. switch (static_cast(failureKindValue.asInt32())) { case ModuleFetchFailureKind::WasErrored: protector->notifyLoadFailed(LoadableScript::Error { LoadableScript::ErrorType::CachedScript, std::nullopt }); break; case ModuleFetchFailureKind::WasCanceled: protector->notifyLoadWasCanceled(); break; } return JSValue::encode(jsUndefined()); } } auto scope = DECLARE_CATCH_SCOPE(vm); protector->notifyLoadFailed(LoadableScript::Error { LoadableScript::ErrorType::CachedScript, LoadableScript::ConsoleMessage { MessageSource::JS, MessageLevel::Error, retrieveErrorMessage(*globalObject, vm, errorValue, scope), } }); return JSValue::encode(jsUndefined()); }); promise.then(&globalObject, &fulfillHandler, &rejectHandler); } m_globalScope->eventLoop().performMicrotaskCheckpoint(); // Drive RunLoop until we get either of "Worker is terminated", "Loading is done", or "Loading is failed". WorkerRunLoop& runLoop = m_globalScope->workerOrWorkletThread()->runLoop(); // We do not want to receive messages that are not related to asynchronous resource loading. // Otherwise, a worker discards some messages from the main thread here in a racy way. // For example, the main thread can postMessage just after creating a Worker. In that case, postMessage's // task is queued in WorkerRunLoop before start running module scripts. This task should not be discarded // in the following driving of the RunLoop which mainly attempt to collect initial load of module scripts. String taskMode = WorkerModuleScriptLoader::taskMode(); MessageQueueWaitResult result = MessageQueueMessageReceived; while ((!protector->isLoaded() && !protector->wasCanceled()) && result != MessageQueueTerminated) { result = runLoop.runInMode(m_globalScope, taskMode); if (result != MessageQueueTerminated) m_globalScope->eventLoop().performMicrotaskCheckpoint(); } return result; } void WorkerOrWorkletScriptController::linkAndEvaluateModule(WorkerScriptFetcher& scriptFetcher, const ScriptSourceCode& sourceCode, String* returnedExceptionMessage) { if (isExecutionForbidden()) return; initScriptIfNeeded(); auto& globalObject = *m_globalScopeWrapper.get(); VM& vm = globalObject.vm(); JSLockHolder lock { vm }; NakedPtr returnedException; JSExecState::linkAndEvaluateModule(globalObject, Identifier::fromUid(vm, scriptFetcher.moduleKey()), jsUndefined(), returnedException); if ((returnedException && vm.isTerminationException(returnedException)) || isTerminatingExecution()) { forbidExecution(); return; } if (returnedException) { if (m_globalScope->canIncludeErrorDetails(sourceCode.cachedScript(), sourceCode.url().string())) { // FIXME: It's not great that this can run arbitrary code to string-ify the value of the exception. // Do we need to do anything to handle that properly, if it, say, raises another exception? if (returnedExceptionMessage) *returnedExceptionMessage = returnedException->value().toWTFString(&globalObject); } else { String genericErrorMessage { "Script error."_s }; if (returnedExceptionMessage) *returnedExceptionMessage = genericErrorMessage; } } } void WorkerOrWorkletScriptController::loadAndEvaluateModule(const URL& moduleURL, FetchOptions::Credentials credentials, CompletionHandler&&)>&& completionHandler) { if (isExecutionForbidden()) { completionHandler(Exception { NotAllowedError }); return; } initScriptIfNeeded(); auto& globalObject = *m_globalScopeWrapper.get(); VM& vm = globalObject.vm(); JSLockHolder lock { vm }; auto scriptFetcher = WorkerScriptFetcher::create(credentials, globalScope()->destination(), globalScope()->referrerPolicy()); { auto& promise = JSExecState::loadModule(globalObject, moduleURL.string(), JSC::JSScriptFetchParameters::create(vm, makeRef(scriptFetcher->parameters())), JSC::JSScriptFetcher::create(vm, { scriptFetcher.ptr() })); auto task = createSharedTask&&)>([completionHandler = WTFMove(completionHandler)](std::optional&& exception) mutable { completionHandler(WTFMove(exception)); }); auto& fulfillHandler = *JSNativeStdFunction::create(vm, &globalObject, 1, String(), [task, scriptFetcher](JSGlobalObject* globalObject, CallFrame* callFrame) -> JSC::EncodedJSValue { // task->run(std::nullopt); VM& vm = globalObject->vm(); JSLockHolder lock { vm }; auto scope = DECLARE_THROW_SCOPE(vm); Identifier moduleKey = jsValueToModuleKey(globalObject, callFrame->argument(0)); RETURN_IF_EXCEPTION(scope, { }); scriptFetcher->notifyLoadCompleted(*moduleKey.impl()); auto* context = downcast(jsCast(globalObject)->scriptExecutionContext()); if (!context || !context->script()) { task->run(std::nullopt); return JSValue::encode(jsUndefined()); } NakedPtr returnedException; JSExecState::linkAndEvaluateModule(*globalObject, moduleKey, jsUndefined(), returnedException); if ((returnedException && vm.isTerminationException(returnedException)) || context->script()->isTerminatingExecution()) { if (context->script()) context->script()->forbidExecution(); task->run(std::nullopt); return JSValue::encode(jsUndefined()); } if (returnedException) { String message; if (context->canIncludeErrorDetails(nullptr, moduleKey.string())) { // FIXME: It's not great that this can run arbitrary code to string-ify the value of the exception. // Do we need to do anything to handle that properly, if it, say, raises another exception? message = returnedException->value().toWTFString(globalObject); } else message = "Script error."_s; context->reportException(message, { }, { }, { }, { }, { }); } task->run(std::nullopt); return JSValue::encode(jsUndefined()); }); auto& rejectHandler = *JSNativeStdFunction::create(vm, &globalObject, 1, String(), [task](JSGlobalObject* globalObject, CallFrame* callFrame) { VM& vm = globalObject->vm(); JSLockHolder lock { vm }; JSValue errorValue = callFrame->argument(0); if (errorValue.isObject()) { auto* object = JSC::asObject(errorValue); if (object->getDirect(vm, static_cast(*vm.clientData).builtinNames().failureKindPrivateName())) { auto catchScope = DECLARE_CATCH_SCOPE(vm); String message = retrieveErrorMessageWithoutName(*globalObject, vm, object, catchScope); task->run(Exception { AbortError, message }); return JSValue::encode(jsUndefined()); } if (object->inherits(vm)) { auto* error = jsCast(object); switch (error->errorType()) { case ErrorType::TypeError: { auto catchScope = DECLARE_CATCH_SCOPE(vm); String message = retrieveErrorMessageWithoutName(*globalObject, vm, error, catchScope); task->run(Exception { TypeError, message }); return JSValue::encode(jsUndefined()); } case ErrorType::SyntaxError: { auto catchScope = DECLARE_CATCH_SCOPE(vm); String message = retrieveErrorMessageWithoutName(*globalObject, vm, error, catchScope); task->run(Exception { JSSyntaxError, message }); return JSValue::encode(jsUndefined()); } default: break; } } } auto catchScope = DECLARE_CATCH_SCOPE(vm); String message = retrieveErrorMessageWithoutName(*globalObject, vm, errorValue, catchScope); task->run(Exception { AbortError, message }); return JSValue::encode(jsUndefined()); }); promise.then(&globalObject, &fulfillHandler, &rejectHandler); } m_globalScope->eventLoop().performMicrotaskCheckpoint(); } template void WorkerOrWorkletScriptController::initScriptWithSubclass() { ASSERT(!m_globalScopeWrapper); JSLockHolder lock { vm() }; // Explicitly protect the global object's prototype so it isn't collected // when we allocate the global object. (Once the global object is fully // constructed, it can mark its own prototype.) Structure* contextPrototypeStructure = JSGlobalScopePrototype::createStructure(*m_vm, nullptr, jsNull()); auto* contextPrototype = JSGlobalScopePrototype::create(*m_vm, nullptr, contextPrototypeStructure); Structure* structure = JSGlobalScope::createStructure(*m_vm, nullptr, contextPrototype); auto* proxyStructure = JSProxy::createStructure(*m_vm, nullptr, jsNull()); auto* proxy = JSProxy::create(*m_vm, proxyStructure); m_globalScopeWrapper.set(*m_vm, JSGlobalScope::create(*m_vm, structure, static_cast(*m_globalScope), proxy)); contextPrototypeStructure->setGlobalObject(*m_vm, m_globalScopeWrapper.get()); ASSERT(structure->globalObject() == m_globalScopeWrapper); ASSERT(m_globalScopeWrapper->structure(*m_vm)->globalObject() == m_globalScopeWrapper); contextPrototype->structure(*m_vm)->setGlobalObject(*m_vm, m_globalScopeWrapper.get()); auto* globalScopePrototype = JSGlobalScope::prototype(*m_vm, *m_globalScopeWrapper.get()); globalScopePrototype->didBecomePrototype(); contextPrototype->structure(*m_vm)->setPrototypeWithoutTransition(*m_vm, globalScopePrototype); proxy->setTarget(*m_vm, m_globalScopeWrapper.get()); proxy->structure(*m_vm)->setGlobalObject(*m_vm, m_globalScopeWrapper.get()); ASSERT(m_globalScopeWrapper->globalObject() == m_globalScopeWrapper); ASSERT(asObject(m_globalScopeWrapper->getPrototypeDirect(*m_vm))->globalObject() == m_globalScopeWrapper); m_consoleClient = makeUnique(*m_globalScope); m_globalScopeWrapper->setConsoleClient(makeWeakPtr(*m_consoleClient)); } void WorkerOrWorkletScriptController::initScript() { ASSERT(m_vm.get()); JSC::DeferTermination deferTermination(*m_vm.get()); if (is(m_globalScope)) { initScriptWithSubclass(); return; } #if ENABLE(SERVICE_WORKER) if (is(m_globalScope)) { initScriptWithSubclass(); return; } #endif #if ENABLE(CSS_PAINTING_API) if (is(m_globalScope)) { initScriptWithSubclass(); return; } #endif #if ENABLE(WEB_AUDIO) if (is(m_globalScope)) { initScriptWithSubclass(); return; } #endif ASSERT_NOT_REACHED(); } } // namespace WebCore