From 9037a395be76744205ca3148f0c020a1dc61050e Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Tue, 13 Jan 2026 11:27:09 +0900 Subject: [PATCH 1/3] Fix async context preservation across await for Bun When an async function awaits a promise, Bun's AsyncLocalStorage context was being lost. This happened because the upstream WebKit change moved the Microtask handling to C++, and the InternalMicrotask::AsyncFunctionResume case was not restoring Bun's async context. The fix: 1. In JSPromise::resolveWithInternalMicrotaskForAsyncAwait: Capture the current async context and wrap it together with the generator in an InternalFieldTuple. 2. In runInternalMicrotask (AsyncFunctionResume case): Extract the async context from the InternalFieldTuple and restore it during the microtask execution. This mirrors the existing async context handling in PromiseReactionJob and ensures that AsyncLocalStorage.getStore() returns the correct context after await points. Fixes: oven-sh/bun#25958 --- Source/JavaScriptCore/runtime/JSMicrotask.cpp | 350 +++++++++++++++++- Source/JavaScriptCore/runtime/JSPromise.cpp | 91 ++++- .../runtime/JSPromisePrototype.cpp | 18 + 3 files changed, 439 insertions(+), 20 deletions(-) diff --git a/Source/JavaScriptCore/runtime/JSMicrotask.cpp b/Source/JavaScriptCore/runtime/JSMicrotask.cpp index c0a1af6cd296..a86adb9c6a1b 100644 --- a/Source/JavaScriptCore/runtime/JSMicrotask.cpp +++ b/Source/JavaScriptCore/runtime/JSMicrotask.cpp @@ -54,6 +54,9 @@ #include "ObjectConstructor.h" #include "ThrowScope.h" #include "VMTrapsInlines.h" +#if USE(BUN_JSC_ADDITIONS) +#include "InternalFieldTuple.h" +#endif #include WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN @@ -795,12 +798,34 @@ void runInternalMicrotask(JSGlobalObject* globalObject, InternalMicrotask task, case InternalMicrotask::PromiseResolveThenableJobFast: { auto* promise = jsCast(arguments[0]); auto* promiseToResolve = jsCast(arguments[1]); +#if USE(BUN_JSC_ADDITIONS) + JSValue asyncContext = arguments[2]; +#endif if (!promiseSpeciesWatchpointIsValid(vm, promise)) [[unlikely]] RELEASE_AND_RETURN(scope, promiseResolveThenableJobFastSlow(globalObject, promise, promiseToResolve)); +#if USE(BUN_JSC_ADDITIONS) + // Set up async context for promise resolution + InternalFieldTuple* asyncContextData = nullptr; + JSValue restoreAsyncContext; + if (!asyncContext.isUndefined()) { + asyncContextData = globalObject->m_asyncContextData.get(); + if (asyncContextData) { + restoreAsyncContext = asyncContextData->getInternalField(0); + asyncContextData->putInternalField(vm, 0, asyncContext); + } + } +#endif + scope.release(); promise->performPromiseThenWithInternalMicrotask(vm, globalObject, InternalMicrotask::PromiseResolveWithoutHandlerJob, promiseToResolve, jsUndefined()); + +#if USE(BUN_JSC_ADDITIONS) + // Restore async context + if (asyncContextData) + asyncContextData->putInternalField(vm, 0, restoreAsyncContext); +#endif return; } @@ -839,8 +864,30 @@ void runInternalMicrotask(JSGlobalObject* globalObject, InternalMicrotask task, JSValue promise = arguments[0]; JSValue then = arguments[1]; JSPromise* promiseToResolve = jsCast(arguments[2]); +#if USE(BUN_JSC_ADDITIONS) + JSValue asyncContext = arguments[3]; + + // Set up async context for thenable resolution + InternalFieldTuple* asyncContextData = nullptr; + JSValue restoreAsyncContext; + if (!asyncContext.isUndefined()) { + asyncContextData = globalObject->m_asyncContextData.get(); + if (asyncContextData) { + restoreAsyncContext = asyncContextData->getInternalField(0); + asyncContextData->putInternalField(vm, 0, asyncContext); + } + } +#endif + auto [resolve, reject] = promiseToResolve->createResolvingFunctions(vm, globalObject); - RELEASE_AND_RETURN(scope, promiseResolveThenableJob(globalObject, promise, then, resolve, reject)); + promiseResolveThenableJob(globalObject, promise, then, resolve, reject); + +#if USE(BUN_JSC_ADDITIONS) + // Restore async context after calling thenable's then method + if (asyncContextData) + asyncContextData->putInternalField(vm, 0, restoreAsyncContext); +#endif + return; } case InternalMicrotask::PromiseResolveThenableJobWithInternalMicrotask: { @@ -848,8 +895,32 @@ void runInternalMicrotask(JSGlobalObject* globalObject, InternalMicrotask task, JSValue promise = arguments[0]; JSValue then = arguments[1]; JSValue context = arguments[2]; + +#if USE(BUN_JSC_ADDITIONS) + // Extract async context from the context tuple and set it up before calling thenable's then method + InternalFieldTuple* asyncContextData = nullptr; + JSValue restoreAsyncContext; + if (auto* tuple = jsDynamicCast(context)) { + JSValue asyncContext = tuple->getInternalField(1); + if (!asyncContext.isUndefined()) { + asyncContextData = globalObject->m_asyncContextData.get(); + if (asyncContextData) { + restoreAsyncContext = asyncContextData->getInternalField(0); + asyncContextData->putInternalField(vm, 0, asyncContext); + } + } + } +#endif + auto [resolve, reject] = JSPromise::createResolvingFunctionsWithInternalMicrotask(vm, globalObject, task, context); - RELEASE_AND_RETURN(scope, promiseResolveThenableJob(globalObject, promise, then, resolve, reject)); + promiseResolveThenableJob(globalObject, promise, then, resolve, reject); + +#if USE(BUN_JSC_ADDITIONS) + // Restore async context after calling thenable's then method + if (asyncContextData) + asyncContextData->putInternalField(vm, 0, restoreAsyncContext); +#endif + return; } case InternalMicrotask::PromiseResolveWithoutHandlerJob: { @@ -893,7 +964,34 @@ void runInternalMicrotask(JSGlobalObject* globalObject, InternalMicrotask task, JSValue promiseOrCapability = arguments[0]; JSValue handler = arguments[1]; #if USE(BUN_JSC_ADDITIONS) - JSValue context = arguments[3]; + // Extract userContext and asyncContext from arguments[3] + // If it's an InternalFieldTuple: [userContext, asyncContext] + // Otherwise: it's userContext directly (legacy behavior) + JSValue contextArg = arguments[3]; + JSValue userContext = jsUndefined(); + JSValue asyncContext = jsUndefined(); + + if (!contextArg.isEmpty() && contextArg.isCell()) { + if (auto* tuple = jsDynamicCast(contextArg)) { + userContext = tuple->getInternalField(0); + asyncContext = tuple->getInternalField(1); + } else { + userContext = contextArg; + } + } else if (!contextArg.isEmpty() && !contextArg.isUndefinedOrNull()) { + userContext = contextArg; + } + + // Set up async context before calling handler + InternalFieldTuple* asyncContextData = nullptr; + JSValue restoreAsyncContext; + if (!asyncContext.isUndefined()) { + asyncContextData = globalObject->m_asyncContextData.get(); + if (asyncContextData) { + restoreAsyncContext = asyncContextData->getInternalField(0); + asyncContextData->putInternalField(vm, 0, asyncContext); + } + } #endif JSValue result; @@ -901,37 +999,59 @@ void runInternalMicrotask(JSGlobalObject* globalObject, InternalMicrotask task, { auto catchScope = DECLARE_CATCH_SCOPE(vm); #if USE(BUN_JSC_ADDITIONS) - // Pass context if present, allowing handlers to receive extra context - // When context is defined (not empty, undefined, or null), pass 2 arguments and use context as the cell context - // When context is empty/undefined/null, pass 1 argument only - if (context.isEmpty() || context.isUndefinedOrNull()) + // Pass userContext if present, allowing handlers to receive extra context + // When userContext is defined (not empty, undefined, or null), pass 2 arguments and use userContext as the cell context + // When userContext is empty/undefined/null, pass 1 argument only + if (userContext.isEmpty() || userContext.isUndefinedOrNull()) result = callMicrotask(globalObject, handler, jsUndefined(), dynamicCastToCell(handler), "handler is not a function"_s, arguments[2]); else - result = callMicrotask(globalObject, handler, jsUndefined(), dynamicCastToCell(context), "handler is not a function"_s, arguments[2], context); + result = callMicrotask(globalObject, handler, jsUndefined(), dynamicCastToCell(userContext), "handler is not a function"_s, arguments[2], userContext); #else result = callMicrotask(globalObject, handler, jsUndefined(), dynamicCastToCell(handler), "handler is not a function"_s, arguments[2]); #endif if (catchScope.exception()) { if (promiseOrCapability.isUndefinedOrNull()) { +#if USE(BUN_JSC_ADDITIONS) + if (asyncContextData) + asyncContextData->putInternalField(vm, 0, restoreAsyncContext); +#endif scope.release(); return; } error = catchScope.exception()->value(); if (!catchScope.clearExceptionExceptTermination()) [[unlikely]] { +#if USE(BUN_JSC_ADDITIONS) + if (asyncContextData) + asyncContextData->putInternalField(vm, 0, restoreAsyncContext); +#endif scope.release(); return; } } if (promiseOrCapability.isUndefinedOrNull()) { +#if USE(BUN_JSC_ADDITIONS) + if (asyncContextData) + asyncContextData->putInternalField(vm, 0, restoreAsyncContext); +#endif scope.release(); return; } } + // Note: Keep async context active during resolvePromise/rejectPromise + // so that any thenables returned from the handler can capture the correct async context + if (error) { - if (auto* promise = jsDynamicCast(promiseOrCapability)) - RELEASE_AND_RETURN(scope, promise->rejectPromise(vm, globalObject, error)); + if (auto* promise = jsDynamicCast(promiseOrCapability)) { + scope.release(); + promise->rejectPromise(vm, globalObject, error); +#if USE(BUN_JSC_ADDITIONS) + if (asyncContextData) + asyncContextData->putInternalField(vm, 0, restoreAsyncContext); +#endif + return; + } JSValue reject = promiseOrCapability.get(globalObject, vm.propertyNames->reject); RETURN_IF_EXCEPTION(scope, void()); @@ -941,11 +1061,22 @@ void runInternalMicrotask(JSGlobalObject* globalObject, InternalMicrotask task, ASSERT(!arguments.hasOverflowed()); scope.release(); call(globalObject, reject, jsUndefined(), arguments, "reject is not a function"_s); +#if USE(BUN_JSC_ADDITIONS) + if (asyncContextData) + asyncContextData->putInternalField(vm, 0, restoreAsyncContext); +#endif return; } - if (auto* promise = jsDynamicCast(promiseOrCapability)) - RELEASE_AND_RETURN(scope, promise->resolvePromise(globalObject, result)); + if (auto* promise = jsDynamicCast(promiseOrCapability)) { + scope.release(); + promise->resolvePromise(globalObject, result); +#if USE(BUN_JSC_ADDITIONS) + if (asyncContextData) + asyncContextData->putInternalField(vm, 0, restoreAsyncContext); +#endif + return; + } JSValue resolve = promiseOrCapability.get(globalObject, vm.propertyNames->resolve); RETURN_IF_EXCEPTION(scope, void()); @@ -955,6 +1086,10 @@ void runInternalMicrotask(JSGlobalObject* globalObject, InternalMicrotask task, ASSERT(!arguments.hasOverflowed()); scope.release(); call(globalObject, resolve, jsUndefined(), arguments, "resolve is not a function"_s); +#if USE(BUN_JSC_ADDITIONS) + if (asyncContextData) + asyncContextData->putInternalField(vm, 0, restoreAsyncContext); +#endif return; } @@ -967,7 +1102,34 @@ void runInternalMicrotask(JSGlobalObject* globalObject, InternalMicrotask task, case InternalMicrotask::AsyncFunctionResume: { JSValue resolution = arguments[1]; - auto* generator = jsCast(arguments[2]); + JSValue contextArg = arguments[2]; + +#if USE(BUN_JSC_ADDITIONS) + // Extract generator and async context from InternalFieldTuple if wrapped + JSGenerator* generator; + JSValue asyncContext; + if (auto* tuple = jsDynamicCast(contextArg)) { + generator = jsCast(tuple->getInternalField(0)); + asyncContext = tuple->getInternalField(1); + } else { + generator = jsCast(contextArg); + asyncContext = jsUndefined(); + } + + // Set up Bun's async context before resuming the async function + InternalFieldTuple* asyncContextData = nullptr; + JSValue restoreAsyncContext; + if (!asyncContext.isUndefined()) { + asyncContextData = globalObject->m_asyncContextData.get(); + if (asyncContextData) { + restoreAsyncContext = asyncContextData->getInternalField(0); + asyncContextData->putInternalField(vm, 0, asyncContext); + } + } +#else + auto* generator = jsCast(contextArg); +#endif + JSGenerator::ResumeMode resumeMode = JSGenerator::ResumeMode::NormalMode; switch (static_cast(payload)) { case JSPromise::Status::Pending: { @@ -998,6 +1160,11 @@ void runInternalMicrotask(JSGlobalObject* globalObject, InternalMicrotask task, if (catchScope.exception()) { error = catchScope.exception()->value(); if (!catchScope.clearExceptionExceptTermination()) [[unlikely]] { +#if USE(BUN_JSC_ADDITIONS) + // Restore async context before returning + if (asyncContextData) + asyncContextData->putInternalField(vm, 0, restoreAsyncContext); +#endif scope.release(); return; } @@ -1006,6 +1173,10 @@ void runInternalMicrotask(JSGlobalObject* globalObject, InternalMicrotask task, if (error) { auto* promise = jsCast(generator->context()); +#if USE(BUN_JSC_ADDITIONS) + if (asyncContextData) + asyncContextData->putInternalField(vm, 0, restoreAsyncContext); +#endif scope.release(); promise->reject(vm, globalObject, error); return; @@ -1013,6 +1184,10 @@ void runInternalMicrotask(JSGlobalObject* globalObject, InternalMicrotask task, if (generator->state() == static_cast(JSGenerator::State::Executing)) { auto* promise = jsCast(generator->context()); +#if USE(BUN_JSC_ADDITIONS) + if (asyncContextData) + asyncContextData->putInternalField(vm, 0, restoreAsyncContext); +#endif scope.release(); promise->resolve(globalObject, value); return; @@ -1020,27 +1195,133 @@ void runInternalMicrotask(JSGlobalObject* globalObject, InternalMicrotask task, scope.release(); JSPromise::resolveWithInternalMicrotaskForAsyncAwait(globalObject, value, InternalMicrotask::AsyncFunctionResume, generator); +#if USE(BUN_JSC_ADDITIONS) + // Restore async context after capturing it for the next await iteration + if (asyncContextData) + asyncContextData->putInternalField(vm, 0, restoreAsyncContext); +#endif return; } case InternalMicrotask::AsyncFromSyncIteratorContinue: - case InternalMicrotask::AsyncFromSyncIteratorDone: + case InternalMicrotask::AsyncFromSyncIteratorDone: { +#if USE(BUN_JSC_ADDITIONS) + // Extract context from InternalFieldTuple if wrapped + JSValue contextArg = arguments[2]; + if (auto* tuple = jsDynamicCast(contextArg)) + contextArg = tuple->getInternalField(0); + RELEASE_AND_RETURN(scope, asyncFromSyncIteratorContinueOrDone(globalObject, vm, contextArg, arguments[1], static_cast(payload), task == InternalMicrotask::AsyncFromSyncIteratorDone)); +#else RELEASE_AND_RETURN(scope, asyncFromSyncIteratorContinueOrDone(globalObject, vm, arguments[2], arguments[1], static_cast(payload), task == InternalMicrotask::AsyncFromSyncIteratorDone)); +#endif + } case InternalMicrotask::AsyncGeneratorYieldAwaited: { +#if USE(BUN_JSC_ADDITIONS) + // Extract generator and async context from InternalFieldTuple if wrapped + JSValue contextArg = arguments[2]; + InternalFieldTuple* asyncContextData = nullptr; + JSValue restoreAsyncContext; + if (auto* tuple = jsDynamicCast(contextArg)) { + contextArg = tuple->getInternalField(0); + JSValue asyncContext = tuple->getInternalField(1); + if (!asyncContext.isUndefined()) { + asyncContextData = globalObject->m_asyncContextData.get(); + if (asyncContextData) { + restoreAsyncContext = asyncContextData->getInternalField(0); + asyncContextData->putInternalField(vm, 0, asyncContext); + } + } + } + scope.release(); + asyncGeneratorYieldAwaited(globalObject, jsCast(contextArg), arguments[1], static_cast(payload)); + if (asyncContextData) + asyncContextData->putInternalField(vm, 0, restoreAsyncContext); + return; +#else RELEASE_AND_RETURN(scope, asyncGeneratorYieldAwaited(globalObject, jsCast(arguments[2]), arguments[1], static_cast(payload))); +#endif } case InternalMicrotask::AsyncGeneratorBodyCallNormal: { +#if USE(BUN_JSC_ADDITIONS) + // Extract generator and async context from InternalFieldTuple if wrapped + JSValue contextArg = arguments[2]; + InternalFieldTuple* asyncContextData = nullptr; + JSValue restoreAsyncContext; + if (auto* tuple = jsDynamicCast(contextArg)) { + contextArg = tuple->getInternalField(0); + JSValue asyncContext = tuple->getInternalField(1); + if (!asyncContext.isUndefined()) { + asyncContextData = globalObject->m_asyncContextData.get(); + if (asyncContextData) { + restoreAsyncContext = asyncContextData->getInternalField(0); + asyncContextData->putInternalField(vm, 0, asyncContext); + } + } + } + scope.release(); + asyncGeneratorBodyCallNormal(globalObject, jsCast(contextArg), arguments[1], static_cast(payload)); + if (asyncContextData) + asyncContextData->putInternalField(vm, 0, restoreAsyncContext); + return; +#else RELEASE_AND_RETURN(scope, asyncGeneratorBodyCallNormal(globalObject, jsCast(arguments[2]), arguments[1], static_cast(payload))); +#endif } case InternalMicrotask::AsyncGeneratorBodyCallReturn: { +#if USE(BUN_JSC_ADDITIONS) + // Extract generator and async context from InternalFieldTuple if wrapped + JSValue contextArg = arguments[2]; + InternalFieldTuple* asyncContextData = nullptr; + JSValue restoreAsyncContext; + if (auto* tuple = jsDynamicCast(contextArg)) { + contextArg = tuple->getInternalField(0); + JSValue asyncContext = tuple->getInternalField(1); + if (!asyncContext.isUndefined()) { + asyncContextData = globalObject->m_asyncContextData.get(); + if (asyncContextData) { + restoreAsyncContext = asyncContextData->getInternalField(0); + asyncContextData->putInternalField(vm, 0, asyncContext); + } + } + } + scope.release(); + asyncGeneratorBodyCallReturn(globalObject, jsCast(contextArg), arguments[1], static_cast(payload)); + if (asyncContextData) + asyncContextData->putInternalField(vm, 0, restoreAsyncContext); + return; +#else RELEASE_AND_RETURN(scope, asyncGeneratorBodyCallReturn(globalObject, jsCast(arguments[2]), arguments[1], static_cast(payload))); +#endif } case InternalMicrotask::AsyncGeneratorResumeNext: { +#if USE(BUN_JSC_ADDITIONS) + // Extract generator and async context from InternalFieldTuple if wrapped + JSValue contextArg = arguments[2]; + InternalFieldTuple* asyncContextData = nullptr; + JSValue restoreAsyncContext; + if (auto* tuple = jsDynamicCast(contextArg)) { + contextArg = tuple->getInternalField(0); + JSValue asyncContext = tuple->getInternalField(1); + if (!asyncContext.isUndefined()) { + asyncContextData = globalObject->m_asyncContextData.get(); + if (asyncContextData) { + restoreAsyncContext = asyncContextData->getInternalField(0); + asyncContextData->putInternalField(vm, 0, asyncContext); + } + } + } + scope.release(); + asyncGeneratorResumeNextReturn(globalObject, jsCast(contextArg), arguments[1], static_cast(payload)); + if (asyncContextData) + asyncContextData->putInternalField(vm, 0, restoreAsyncContext); + return; +#else RELEASE_AND_RETURN(scope, asyncGeneratorResumeNextReturn(globalObject, jsCast(arguments[2]), arguments[1], static_cast(payload))); +#endif } case InternalMicrotask::PromiseFinallyReactionJob: { @@ -1048,13 +1329,54 @@ void runInternalMicrotask(JSGlobalObject* globalObject, InternalMicrotask task, // arguments[0] = resultPromise // arguments[1] = value/reason from original promise // arguments[2] = context (JSPromiseCombinatorsGlobalContext: promise=resultPromise, values=onFinally) + // OR InternalFieldTuple: [context, asyncContext] when Bun async context is present // payload = Fulfilled/Rejected status +#if USE(BUN_JSC_ADDITIONS) + // Extract context and async context from InternalFieldTuple if wrapped + JSValue contextArg = arguments[2]; + JSPromiseCombinatorsGlobalContext* context; + JSValue asyncContext = jsUndefined(); + + if (contextArg.isCell()) { + if (auto* tuple = jsDynamicCast(contextArg)) { + context = jsCast(tuple->getInternalField(0)); + asyncContext = tuple->getInternalField(1); + } else { + context = jsCast(contextArg); + } + } else { + context = jsCast(contextArg); + } + + // Set up async context before calling onFinally + InternalFieldTuple* asyncContextData = nullptr; + JSValue restoreAsyncContext; + if (!asyncContext.isUndefined()) { + asyncContextData = globalObject->m_asyncContextData.get(); + if (asyncContextData) { + restoreAsyncContext = asyncContextData->getInternalField(0); + asyncContextData->putInternalField(vm, 0, asyncContext); + } + } + + scope.release(); + promiseFinallyReactionJob(globalObject, vm, + jsCast(arguments[0]), + arguments[1], + context, + static_cast(payload)); + + // Restore async context + if (asyncContextData) + asyncContextData->putInternalField(vm, 0, restoreAsyncContext); +#else scope.release(); promiseFinallyReactionJob(globalObject, vm, jsCast(arguments[0]), arguments[1], jsCast(arguments[2]), static_cast(payload)); +#endif return; } diff --git a/Source/JavaScriptCore/runtime/JSPromise.cpp b/Source/JavaScriptCore/runtime/JSPromise.cpp index 67efc33f99fb..0436ecb43709 100644 --- a/Source/JavaScriptCore/runtime/JSPromise.cpp +++ b/Source/JavaScriptCore/runtime/JSPromise.cpp @@ -44,6 +44,9 @@ #include "JSPromiseReaction.h" #include "Microtask.h" #include "ObjectConstructor.h" +#if USE(BUN_JSC_ADDITIONS) +#include "InternalFieldTuple.h" +#endif namespace JSC { @@ -271,21 +274,48 @@ void JSPromise::performPromiseThen(VM& vm, JSGlobalObject* globalObject, JSValue if (!onRejected.isCallable()) onRejected = globalObject->promiseEmptyOnRejectedFunction(); +#if USE(BUN_JSC_ADDITIONS) + // Capture async context for promise reaction + // Wrap in InternalFieldTuple: [userContext (undefined), asyncContext] + JSValue context = jsUndefined(); + if (auto* asyncContextData = globalObject->m_asyncContextData.get()) { + JSValue asyncContext = asyncContextData->getInternalField(0); + if (!asyncContext.isUndefined()) { + auto* tuple = InternalFieldTuple::create(vm, globalObject->internalFieldTupleStructure()); + tuple->putInternalField(vm, 0, jsUndefined()); // userContext + tuple->putInternalField(vm, 1, asyncContext); // asyncContext + context = tuple; + } + } +#endif + JSValue reactionsOrResult = this->reactionsOrResult(); switch (status()) { case JSPromise::Status::Pending: { +#if USE(BUN_JSC_ADDITIONS) + auto* reaction = JSPromiseReaction::create(vm, promiseOrCapability, onFulfilled, onRejected, context, jsDynamicCast(reactionsOrResult)); +#else auto* reaction = JSPromiseReaction::create(vm, promiseOrCapability, onFulfilled, onRejected, jsUndefined(), jsDynamicCast(reactionsOrResult)); +#endif setReactionsOrResult(vm, reaction); break; } case JSPromise::Status::Rejected: { if (!isHandled()) globalObject->globalObjectMethodTable()->promiseRejectionTracker(globalObject, this, JSPromiseRejectionOperation::Handle); +#if USE(BUN_JSC_ADDITIONS) + globalObject->queueMicrotask(InternalMicrotask::PromiseReactionJob, static_cast(Status::Rejected), promiseOrCapability, onRejected, reactionsOrResult, context); +#else globalObject->queueMicrotask(InternalMicrotask::PromiseReactionJob, static_cast(Status::Rejected), promiseOrCapability, onRejected, reactionsOrResult); +#endif break; } case JSPromise::Status::Fulfilled: { +#if USE(BUN_JSC_ADDITIONS) + globalObject->queueMicrotask(InternalMicrotask::PromiseReactionJob, static_cast(Status::Fulfilled), promiseOrCapability, onFulfilled, reactionsOrResult, context); +#else globalObject->queueMicrotask(InternalMicrotask::PromiseReactionJob, static_cast(Status::Fulfilled), promiseOrCapability, onFulfilled, reactionsOrResult); +#endif break; } } @@ -293,7 +323,7 @@ void JSPromise::performPromiseThen(VM& vm, JSGlobalObject* globalObject, JSValue } #if USE(BUN_JSC_ADDITIONS) -void JSPromise::performPromiseThenWithContext(VM& vm, JSGlobalObject* globalObject, JSValue onFulfilled, JSValue onRejected, JSValue promiseOrCapability, JSValue context) +void JSPromise::performPromiseThenWithContext(VM& vm, JSGlobalObject* globalObject, JSValue onFulfilled, JSValue onRejected, JSValue promiseOrCapability, JSValue userContext) { if (!onFulfilled.isCallable()) onFulfilled = globalObject->promiseEmptyOnFulfilledFunction(); @@ -301,6 +331,19 @@ void JSPromise::performPromiseThenWithContext(VM& vm, JSGlobalObject* globalObje if (!onRejected.isCallable()) onRejected = globalObject->promiseEmptyOnRejectedFunction(); + // Wrap userContext and asyncContext in InternalFieldTuple: [userContext, asyncContext] + JSValue context = userContext; + if (auto* asyncContextData = globalObject->m_asyncContextData.get()) { + JSValue asyncContext = asyncContextData->getInternalField(0); + // Always create a tuple if there's a user context or async context + if (!userContext.isUndefinedOrNull() || !asyncContext.isUndefined()) { + auto* tuple = InternalFieldTuple::create(vm, globalObject->internalFieldTupleStructure()); + tuple->putInternalField(vm, 0, userContext); // userContext + tuple->putInternalField(vm, 1, asyncContext); // asyncContext + context = tuple; + } + } + JSValue reactionsOrResult = this->reactionsOrResult(); switch (status()) { case JSPromise::Status::Pending: { @@ -412,8 +455,17 @@ void JSPromise::resolvePromise(JSGlobalObject* globalObject, JSValue resolution) auto* resolutionObject = asObject(resolution); if (resolutionObject->inherits()) { auto* promise = jsCast(resolutionObject); - if (promise->isThenFastAndNonObservable()) + if (promise->isThenFastAndNonObservable()) { +#if USE(BUN_JSC_ADDITIONS) + // Capture async context for thenable resolution + JSValue asyncContext = jsUndefined(); + if (auto* asyncContextData = globalObject->m_asyncContextData.get()) + asyncContext = asyncContextData->getInternalField(0); + return globalObject->queueMicrotask(InternalMicrotask::PromiseResolveThenableJobFast, 0, resolutionObject, this, asyncContext); +#else return globalObject->queueMicrotask(InternalMicrotask::PromiseResolveThenableJobFast, 0, resolutionObject, this, jsUndefined()); +#endif + } } if (isDefinitelyNonThenable(resolutionObject, globalObject)) @@ -436,7 +488,15 @@ void JSPromise::resolvePromise(JSGlobalObject* globalObject, JSValue resolution) if (!then.isCallable()) [[likely]] return fulfillPromise(vm, globalObject, resolutionObject); +#if USE(BUN_JSC_ADDITIONS) + // Capture async context for thenable resolution + JSValue asyncContext = jsUndefined(); + if (auto* asyncContextData = globalObject->m_asyncContextData.get()) + asyncContext = asyncContextData->getInternalField(0); + return globalObject->queueMicrotask(InternalMicrotask::PromiseResolveThenableJob, 0, resolutionObject, then, this, asyncContext); +#else return globalObject->queueMicrotask(InternalMicrotask::PromiseResolveThenableJob, 0, resolutionObject, then, this); +#endif } JSC_DEFINE_HOST_FUNCTION(promiseResolvingFunctionResolve, (JSGlobalObject* globalObject, CallFrame* callFrame)) @@ -645,10 +705,27 @@ void JSPromise::resolveWithInternalMicrotaskForAsyncAwait(JSGlobalObject* global { VM& vm = globalObject->vm(); +#if USE(BUN_JSC_ADDITIONS) + // Capture Bun's async context at the point of await and wrap it with the generator context. + // This allows AsyncFunctionResume and related microtasks to restore the async context when + // resuming the async function. + JSValue wrappedContext = context; + if (auto* asyncContextData = globalObject->m_asyncContextData.get()) { + JSValue asyncContext = asyncContextData->getInternalField(0); + if (!asyncContext.isUndefined()) { + auto* tuple = InternalFieldTuple::create(vm, globalObject->internalFieldTupleStructure(), context, asyncContext); + wrappedContext = tuple; + } + } +#define BUN_CONTEXT wrappedContext +#else +#define BUN_CONTEXT context +#endif + if (resolution.inherits()) { auto* promise = jsCast(resolution); if (promiseSpeciesWatchpointIsValid(vm, promise)) [[likely]] - return promise->performPromiseThenWithInternalMicrotask(vm, globalObject, task, jsUndefined(), context); + return promise->performPromiseThenWithInternalMicrotask(vm, globalObject, task, jsUndefined(), BUN_CONTEXT); JSValue constructor; JSValue error; @@ -665,17 +742,19 @@ void JSPromise::resolveWithInternalMicrotaskForAsyncAwait(JSGlobalObject* global std::array arguments { { jsUndefined(), error, - context, + BUN_CONTEXT, } }; runInternalMicrotask(globalObject, task, static_cast(JSPromise::Status::Rejected), arguments); return; } if (constructor == globalObject->promiseConstructor() || constructor == globalObject->internalPromiseConstructor()) - return promise->performPromiseThenWithInternalMicrotask(vm, globalObject, task, jsUndefined(), context); + return promise->performPromiseThenWithInternalMicrotask(vm, globalObject, task, jsUndefined(), BUN_CONTEXT); } - resolveWithInternalMicrotask(globalObject, resolution, task, context); + resolveWithInternalMicrotask(globalObject, resolution, task, BUN_CONTEXT); + +#undef BUN_CONTEXT } void JSPromise::resolveWithInternalMicrotask(JSGlobalObject* globalObject, JSValue resolution, InternalMicrotask task, JSValue context) diff --git a/Source/JavaScriptCore/runtime/JSPromisePrototype.cpp b/Source/JavaScriptCore/runtime/JSPromisePrototype.cpp index 3ed7e72ea79e..008865d84c01 100644 --- a/Source/JavaScriptCore/runtime/JSPromisePrototype.cpp +++ b/Source/JavaScriptCore/runtime/JSPromisePrototype.cpp @@ -33,6 +33,9 @@ #include "JSInternalPromise.h" #include "JSPromise.h" #include "JSPromiseCombinatorsGlobalContext.h" +#if USE(BUN_JSC_ADDITIONS) +#include "InternalFieldTuple.h" +#endif namespace JSC { @@ -271,7 +274,22 @@ JSC_DEFINE_HOST_FUNCTION(promiseProtoFuncFinally, (JSGlobalObject* globalObject, if (promise && promise->isThenFastAndNonObservable() && promiseSpeciesWatchpointIsValid(vm, promise)) [[likely]] { JSPromise* resultPromise = JSPromise::create(vm, globalObject->promiseStructure()); auto* context = JSPromiseCombinatorsGlobalContext::create(vm, resultPromise, onFinally, jsUndefined()); +#if USE(BUN_JSC_ADDITIONS) + // Wrap context with async context in InternalFieldTuple: [context, asyncContext] + JSValue contextValue = context; + if (auto* asyncContextData = globalObject->m_asyncContextData.get()) { + JSValue asyncContext = asyncContextData->getInternalField(0); + if (!asyncContext.isUndefined()) { + auto* tuple = InternalFieldTuple::create(vm, globalObject->internalFieldTupleStructure()); + tuple->putInternalField(vm, 0, context); + tuple->putInternalField(vm, 1, asyncContext); + contextValue = tuple; + } + } + promise->performPromiseThenWithInternalMicrotask(vm, globalObject, InternalMicrotask::PromiseFinallyReactionJob, resultPromise, contextValue); +#else promise->performPromiseThenWithInternalMicrotask(vm, globalObject, InternalMicrotask::PromiseFinallyReactionJob, resultPromise, context); +#endif return JSValue::encode(resultPromise); } From 8cbd8edf26d10225f23d14803ef3f343690eeba9 Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Tue, 13 Jan 2026 21:57:19 +0900 Subject: [PATCH 2/3] Fix Promise.resolve(InternalPromise) to create new Promise When Promise.resolve() is called with an InternalPromise argument, it should create a new regular Promise instead of returning the same InternalPromise. This ensures user-facing APIs return regular Promises that pass instanceof Promise checks. The issue was that the C++ promiseResolve implementation used promiseSpeciesWatchpointIsValid() which returns true for InternalPromise, causing it to bypass the constructor check. The old JavaScript implementation explicitly checked value.constructor === constructor before returning the same promise. This fix adds a check for InternalPromise: if the target constructor is not internalPromiseConstructor, we fall through to create a new Promise. --- Source/JavaScriptCore/runtime/JSPromise.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Source/JavaScriptCore/runtime/JSPromise.cpp b/Source/JavaScriptCore/runtime/JSPromise.cpp index 0436ecb43709..fd6e02b24ae6 100644 --- a/Source/JavaScriptCore/runtime/JSPromise.cpp +++ b/Source/JavaScriptCore/runtime/JSPromise.cpp @@ -913,8 +913,16 @@ JSObject* JSPromise::promiseResolve(JSGlobalObject* globalObject, JSObject* cons if (argument.inherits()) { auto* promise = jsCast(argument); - if (promiseSpeciesWatchpointIsValid(vm, promise)) [[likely]] + if (promiseSpeciesWatchpointIsValid(vm, promise)) [[likely]] { + // For InternalPromise, we can only return the same promise if the constructor + // matches. This preserves the behavior where Promise.resolve(InternalPromise) + // creates a new regular Promise, ensuring user-facing APIs return regular Promises. + if (promise->structure()->classInfoForCells() == JSInternalPromise::info()) { + if (constructor != globalObject->internalPromiseConstructor()) + goto createNewPromise; + } return promise; + } auto property = promise->get(globalObject, vm.propertyNames->constructor); RETURN_IF_EXCEPTION(scope, { }); @@ -922,6 +930,7 @@ JSObject* JSPromise::promiseResolve(JSGlobalObject* globalObject, JSObject* cons if (property == constructor) return promise; } +createNewPromise: if (constructor == globalObject->promiseConstructor()) [[likely]] { JSPromise* promise = JSPromise::create(vm, globalObject->promiseStructure()); From d2c9cd9e9aebc8acd207009ce431b53e5db3beff Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Wed, 14 Jan 2026 13:40:37 +0900 Subject: [PATCH 3/3] Use internal microtask when resolving Promise with InternalPromise When Promise.resolve(InternalPromise) is called, instead of using the regular resolve() which triggers thenable resolution and calls user-visible .then(), use performPromiseThenWithInternalMicrotask to adopt the state internally. This preserves InternalPromise isolation while returning a regular Promise for instanceof checks. --- Source/JavaScriptCore/runtime/JSPromise.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Source/JavaScriptCore/runtime/JSPromise.cpp b/Source/JavaScriptCore/runtime/JSPromise.cpp index fd6e02b24ae6..c2d76e5b9c85 100644 --- a/Source/JavaScriptCore/runtime/JSPromise.cpp +++ b/Source/JavaScriptCore/runtime/JSPromise.cpp @@ -935,6 +935,17 @@ JSObject* JSPromise::promiseResolve(JSGlobalObject* globalObject, JSObject* cons if (constructor == globalObject->promiseConstructor()) [[likely]] { JSPromise* promise = JSPromise::create(vm, globalObject->promiseStructure()); scope.release(); +#if USE(BUN_JSC_ADDITIONS) + // For InternalPromise, use internal microtask to adopt the state without + // calling user-visible .then(). This preserves InternalPromise isolation + // while returning a regular Promise for instanceof checks. + if (argument.inherits()) { + auto* internalPromise = jsCast(argument); + internalPromise->performPromiseThenWithInternalMicrotask(vm, globalObject, + InternalMicrotask::PromiseResolveWithoutHandlerJob, promise, jsUndefined()); + return promise; + } +#endif promise->resolve(globalObject, argument); return promise; }