Unwrap async context tuple in AsyncModuleExecutionResume dispatch#252
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
WalkthroughThis PR extends JavaScriptCore's async module execution handler to support Bun's async-context preservation. When handling ChangesAsync module execution context preservation
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Warning Review ran into problems🔥 ProblemsGit: Failed to clone repository. Please run the Comment |
There was a problem hiding this comment.
I didn't find any issues — the unwrap/restore exactly mirrors the existing AsyncFunctionResume / AsyncGeneratorYieldAwaited cases and the producer in JSPromise.cpp confirms the tuple wrapping — but since this is JSC microtask dispatch touching module evaluation and async-context propagation semantics, it's worth a quick human sign-off.
Extended reasoning...
Overview
This PR adds tuple-unwrapping to the InternalMicrotask::AsyncModuleExecutionResume dispatch case in JSMicrotask.cpp, guarded entirely by #if USE(BUN_JSC_ADDITIONS). Under Bun's additions, JSPromise::resolveWithInternalMicrotaskForAsyncAwait (JSPromise.cpp:984-997) wraps the context argument in an InternalFieldTuple(context, asyncContext) whenever an AsyncLocalStorage context is active. The AsyncFunctionResume, AsyncFromSyncIterator*, and AsyncGeneratorYieldAwaited cases already unwrap this tuple; AsyncModuleExecutionResume did not, so it passed the tuple straight into uncheckedDowncast<JSModuleRecord> — a type confusion that crashes deterministically on any top-level-await module with an active ALS context.
The fix is a near-verbatim copy of the pattern at lines 1902-1923 / 1993-1996 (AsyncFunctionResume) and 2016-2037 (AsyncGeneratorYieldAwaited): dynamicDowncast<InternalFieldTuple>, extract module from field 0 and asyncContext from field 1, swap the global m_asyncContextData[0] around the resumption, and restore it afterward. The restore is correctly placed after asyncModuleExecutionResume returns, so the nested resolveWithInternalMicrotaskForAsyncAwait at JSMicrotask.cpp:882 captures the module's context for the next await iteration — matching the comment and the AsyncFunctionResume ordering at line 1992-1996. The non-Bun #else branch is byte-identical to the original code.
Security risks
The bug being fixed is itself a memory-safety issue (type-confused uncheckedDowncast followed by module->evaluate writing through a misinterpreted pointer). The fix removes that hazard and introduces no new untrusted input handling, no auth/crypto/permissions surface, and no new allocation or pointer arithmetic. I see no new security risk.
Level of scrutiny
This is JSC runtime microtask dispatch — production-critical, hot-path code where ordering and context-restore placement have subtle semantic consequences (e.g., whether ALS context propagates to the next TLA await). Even though the change is mechanical and pattern-matched against three sibling cases in the same switch, I'd want a human who owns Bun's async-context plumbing to confirm the restore-after-capture ordering is the intended semantics for TLA (it appears to be, per the PR description's Node-parity claim and the AsyncFunctionResume precedent).
Other factors
- Single function, ~30 lines, fully
#if-guarded; upstream WebKit path unchanged. - PR description includes a 4-line deterministic repro, debug+ASAN verification, the full upstream Bun issue reproduction, and passing
test/js/node/async_hooks/suites. - No bugs flagged by the automated review.
- No prior reviewer comments to address.
Preview Builds
|
|
Note: #253 is this same fix cherry-picked onto 6d586e2 (the commit oven-sh/bun currently pins) as a draft, only to produce preview artifacts compatible with bun main for oven-sh/bun#32184. This PR remains the mergeable fix for main. |
…me fix Temporary pin to autobuild-preview-pr-253-7eb8e17e: the currently pinned commit (6d586e293f) plus the one-function fix from oven-sh/WebKit#252, which unwraps the InternalFieldTuple-wrapped async context in asyncModuleExecutionResume. Swap to an oven-sh/WebKit main autobuild SHA containing the fix before merging.
A top-level-await module that awaits while an AsyncLocalStorage context is active segfaulted since 1.3.14 (worked in 1.3.13). The fix is in the WebKit fork (oven-sh/WebKit#252): asyncModuleExecutionResume received an InternalFieldTuple-wrapped context and downcast it as a JSModuleRecord. The test passes once WEBKIT_VERSION picks up a build containing that fix.
…me fix Temporary pin to autobuild-preview-pr-253-7eb8e17e: the currently pinned commit (6d586e293f) plus the one-function fix from oven-sh/WebKit#252, which unwraps the InternalFieldTuple-wrapped async context in asyncModuleExecutionResume. Swap to an oven-sh/WebKit main autobuild SHA containing the fix before merging.
3d2a7a9 to
6861a73
Compare
There was a problem hiding this comment.
I didn't find any issues — the unwrap/restore mirrors the existing AsyncFunctionResume case exactly — but since this touches JSC's async module resumption path (and the original bug was a memory-safety type confusion), it's worth a human sign-off.
Extended reasoning...
Overview
This PR modifies one switch case (InternalMicrotask::AsyncModuleExecutionResume) in Source/JavaScriptCore/runtime/JSMicrotask.cpp. Under USE(BUN_JSC_ADDITIONS), it now detects when arguments[2] is an InternalFieldTuple (wrapped by resolveWithInternalMicrotaskForAsyncAwait when an AsyncLocalStorage context is active), unwraps it to recover the real JSModuleRecord*, and installs/restores the captured async context around asyncModuleExecutionResume. The non-Bun branch is byte-for-byte unchanged. The new code is a near-verbatim copy of the existing AsyncFunctionResume handling at lines ~1898–1997 in the same file.
Security risks
The bug being fixed is itself a memory-safety issue: an uncheckedDowncast<JSModuleRecord> on an InternalFieldTuple cell, leading to type confusion and OOB access. The fix uses dynamicDowncast to discriminate, then uncheckedDowncast on the unwrapped field — identical to the proven AsyncFunctionResume pattern. I don't see new attack surface introduced; the change strictly narrows the set of values that reach the unchecked cast. The async-context save/restore writes only to globalObject->m_asyncContextData field 0, also matching the existing pattern.
Level of scrutiny
This is JavaScriptCore runtime code on the module-evaluation / microtask dispatch path — a critical, memory-safety-sensitive code path in the engine. Even though the change is small, mechanical, and mirrors an established sibling case, engine-internal changes that involve unchecked downcasts and fix a type-confusion crash warrant a human reviewer who knows the Bun JSC additions.
Other factors
The PR description includes a deterministic 4-line repro, ASAN-verified results, and passing test/js/node/async_hooks/ suites in Bun. The bug-hunting system found no issues, and CodeRabbit had no actionable comments. I confirmed the restore placement (after asyncModuleExecutionResume, which internally re-calls resolveWithInternalMicrotaskForAsyncAwait) matches the AsyncFunctionResume case. Given all that, I'd expect this to be a quick approval — I'm deferring purely on the "critical engine path" criterion rather than any concern with the diff itself.
A top-level-await module that awaits while an AsyncLocalStorage context is active segfaulted since 1.3.14 (worked in 1.3.13). The fix is in the WebKit fork (oven-sh/WebKit#252): asyncModuleExecutionResume received an InternalFieldTuple-wrapped context and downcast it as a JSModuleRecord. The test passes once WEBKIT_VERSION picks up a build containing that fix.
…me fix Temporary pin to autobuild-preview-pr-253-7eb8e17e: the currently pinned commit (6d586e293f) plus the one-function fix from oven-sh/WebKit#252, which unwraps the InternalFieldTuple-wrapped async context in asyncModuleExecutionResume. Swap to an oven-sh/WebKit main autobuild SHA containing the fix before merging.
main bumped WEBKIT_VERSION to 09f04cd5 (786 commits past the prior base, adding CommonIdentifiers::globalThis). The old preview (9cb85a0716 base) no longer compiles against main's bindings, so oven-sh/WebKit#252 was rebased onto 09f04cd5 (commit 6861a738, JSMicrotask.cpp unchanged in that range) and its fresh preview is pinned here.
resolveWithInternalMicrotaskForAsyncAwait wraps the microtask context in an InternalFieldTuple(context, asyncContext) when Bun's async context is active at await time. AsyncFunctionResume unwraps it; AsyncModuleExecutionResume did not, and downcast the tuple as a JSModuleRecord (type confusion). Unwrap it and restore the async context around the resumption, mirroring AsyncFunctionResume. Fixes oven-sh/bun#32178.
6861a73 to
83144fc
Compare
There was a problem hiding this comment.
I didn't find any issues — the unwrap/restore mirrors the existing AsyncFunctionResume and AsyncGenerator* cases line-for-line and is fully gated behind USE(BUN_JSC_ADDITIONS). Deferring to a human only because this sits in JSC's microtask dispatch with uncheckedDowncast on the module-evaluation path.
Extended reasoning...
Overview
This PR modifies a single switch case (InternalMicrotask::AsyncModuleExecutionResume) in Source/JavaScriptCore/runtime/JSMicrotask.cpp. Under USE(BUN_JSC_ADDITIONS), resolveWithInternalMicrotaskForAsyncAwait may wrap the context argument in an InternalFieldTuple(realContext, asyncContext) when Bun's AsyncLocalStorage is active. The existing code passed that tuple straight to uncheckedDowncast<JSModuleRecord>, producing a type confusion that crashes deterministically on any top-level await inside an ALS context. The fix detects the tuple via dynamicDowncast, extracts the real JSModuleRecord* and the captured async context, installs the context into globalObject->m_asyncContextData for the duration of asyncModuleExecutionResume, and restores it afterward. The non-Bun #else branch is byte-identical to the previous behavior.
Security risks
The bug being fixed is itself a memory-safety issue (unchecked downcast of the wrong cell type → reads/writes through a confused pointer). The fix removes that hazard and introduces no new untrusted input handling — it only adds a dynamicDowncast type check and a save/restore of an internal field. I don't see new injection, auth, or data-exposure surface. The remaining uncheckedDowncast<JSModuleRecord> on tuple->getInternalField(0) is safe by construction: the only producer of this tuple shape (resolveWithInternalMicrotaskForAsyncAwait) always stores the original context in field 0.
Level of scrutiny
The change is mechanically simple and copies an idiom already used four other times in the same function (AsyncFunctionResume at L1915–2010, AsyncFromSyncIteratorContinue at L2016, AsyncGeneratorYieldAwaited at L2029, AsyncGeneratorBodyCallNormal at L2058). That said, it lives in JavaScriptCore's microtask dispatch and module-evaluation path, where mistakes manifest as memory corruption rather than exceptions. Per the approval guidelines for critical / security-sensitive code paths, I'm deferring to a human reviewer rather than auto-approving, even though I believe the change is correct.
Other factors
The PR description reports verification under debug+ASAN against the pinned Bun commit, the 4-line repro, the upstream alchemy reproduction, and the full test/js/node/async_hooks/ suite. The bug-hunting system found no issues. No prior human review comments are outstanding.
Temporary pin to autobuild-preview-pr-252-83144fc3: main's WebKit cd821fec plus the one-function asyncModuleExecutionResume fix from oven-sh/WebKit#252, rebased onto cd821fec. Swap to a non-preview oven-sh/WebKit main autobuild SHA containing the fix before merging.
What
resolveWithInternalMicrotaskForAsyncAwait(BUN_JSC_ADDITIONS) wraps the microtask context in anInternalFieldTuple(context, asyncContext)whenever Bun's async context (AsyncLocalStorage) is active at await time. TheAsyncFunctionResumedispatch unwraps that tuple; theAsyncModuleExecutionResumedispatch did not. It passed the tuple straight touncheckedDowncast<JSModuleRecord>, a type confusion that reads InternalFieldTuple memory as a JSModuleRecord.In asserting builds this trips
reportZappedCellAndCrashin the downcast; in release builds it segfaults at garbage addresses (0x600010071,0x0,0x10), andmodule->evaluateon the confused pointer can write outside the 2-field tuple cell.Repro (4 lines, against Bun with this fork)
Any top-level-await module that awaits while an ALS context is active crashes 100% deterministically. This is the root cause of oven-sh/bun#32178 (regression in Bun 1.3.14, which picked up the internal-microtask async module resumption; works in 1.3.13). Real-world trigger: the alchemy IaC framework, which runs its module graph inside AsyncLocalStorage.
Fix
Unwrap the tuple in the
AsyncModuleExecutionResumecase and set/restore the async context around the resumption, exactly mirroring the existingAsyncFunctionResumecase (including restoring after the follow-upresolveWithInternalMicrotaskForAsyncAwaitso the next await captures the module's context). This also makes AsyncLocalStorage context survive across top-level await, matching Node.AsyncModuleExecutionDoneis not affected: its only producer (performPromiseThenWithInternalMicrotask) never wraps the context.Verification
Built Bun against this fork at the currently pinned commit (6d586e2, same one-function fix shape) on linux x64 debug+ASAN:
okinstead of crashingtest/js/node/async_hooks/suites in oven-sh/bun all pass