Skip to content

Unwrap async context tuple in AsyncModuleExecutionResume dispatch#252

Merged
Jarred-Sumner merged 1 commit into
mainfrom
bun/fix-async-module-resume-async-context
Jun 18, 2026
Merged

Unwrap async context tuple in AsyncModuleExecutionResume dispatch#252
Jarred-Sumner merged 1 commit into
mainfrom
bun/fix-async-module-resume-async-context

Conversation

@robobun

@robobun robobun commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

What

resolveWithInternalMicrotaskForAsyncAwait (BUN_JSC_ADDITIONS) wraps the microtask context in an InternalFieldTuple(context, asyncContext) whenever Bun's async context (AsyncLocalStorage) is active at await time. The AsyncFunctionResume dispatch unwraps that tuple; the AsyncModuleExecutionResume dispatch did not. It passed the tuple straight to uncheckedDowncast<JSModuleRecord>, a type confusion that reads InternalFieldTuple memory as a JSModuleRecord.

In asserting builds this trips reportZappedCellAndCrash in the downcast; in release builds it segfaults at garbage addresses (0x600010071, 0x0, 0x10), and module->evaluate on the confused pointer can write outside the 2-field tuple cell.

Repro (4 lines, against Bun with this fork)

import { AsyncLocalStorage } from "node:async_hooks";
new AsyncLocalStorage().enterWith({ v: 1 });
await Promise.resolve(1);

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 AsyncModuleExecutionResume case and set/restore the async context around the resumption, exactly mirroring the existing AsyncFunctionResume case (including restoring after the follow-up resolveWithInternalMicrotaskForAsyncAwait so the next await captures the module's context). This also makes AsyncLocalStorage context survive across top-level await, matching Node.

AsyncModuleExecutionDone is 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:

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: dd618e66-36fd-48a6-a000-446e031ad769

📥 Commits

Reviewing files that changed from the base of the PR and between 9cb85a0 and 3d2a7a9.

📒 Files selected for processing (1)
  • Source/JavaScriptCore/runtime/JSMicrotask.cpp

Walkthrough

This PR extends JavaScriptCore's async module execution handler to support Bun's async-context preservation. When handling AsyncModuleExecutionResume, the code now conditionally unwraps Bun's InternalFieldTuple wrapper, extracts stored async-context state, installs it during the module resume call, and restores the prior context afterward. Non-Bun builds retain the prior direct JSModuleRecord* unwrapping behavior.

Changes

Async module execution context preservation

Layer / File(s) Summary
Async module execution context wrapping and restoration
Source/JavaScriptCore/runtime/JSMicrotask.cpp
InternalMicrotask::AsyncModuleExecutionResume case conditionally detects and unwraps Bun's InternalFieldTuple to extract JSModuleRecord* and async-context value, installs the context in globalObject->m_asyncContextData during asyncModuleExecutionResume execution, and restores the original context afterward when USE(BUN_JSC_ADDITIONS) is defined; otherwise preserves the prior direct JSModuleRecord* unwrapping and resume behavior.
🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is incomplete according to the WebKit template, which requires a Bugzilla bug reference, explicit "Reviewed by" line, and structured change list with functions/classes, but provides thorough technical explanation of the issue, fix, and verification. Add required Bugzilla bug reference (https://bugs.webkit.org/show_bug.cgi?id=#####), include "Reviewed by" line, and format the file changes section with functions/classes as per WebKit template structure.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: unwrapping an async context tuple in the AsyncModuleExecutionResume dispatch case.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Comment @coderabbitai help to get the list of available commands and usage tips.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown

Preview Builds

Commit Release Date
83144fc3 autobuild-preview-pr-252-83144fc3 2026-06-17 05:01:20 UTC
6861a738 autobuild-preview-pr-252-6861a738 2026-06-17 00:13:23 UTC
3d2a7a95 autobuild-preview-pr-252-3d2a7a95 2026-06-12 16:22:09 UTC

@robobun

robobun commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator Author

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.

robobun added a commit to oven-sh/bun that referenced this pull request Jun 12, 2026
…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.
robobun added a commit to oven-sh/bun that referenced this pull request Jun 16, 2026
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.
robobun added a commit to oven-sh/bun that referenced this pull request Jun 16, 2026
…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.
@robobun robobun force-pushed the bun/fix-async-module-resume-async-context branch from 3d2a7a9 to 6861a73 Compare June 16, 2026 22:56

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

robobun added a commit to oven-sh/bun that referenced this pull request Jun 17, 2026
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.
robobun added a commit to oven-sh/bun that referenced this pull request Jun 17, 2026
…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.
robobun added a commit to oven-sh/bun that referenced this pull request Jun 17, 2026
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.
@robobun robobun force-pushed the bun/fix-async-module-resume-async-context branch from 6861a73 to 83144fc Compare June 17, 2026 04:27

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

robobun added a commit to oven-sh/bun that referenced this pull request Jun 17, 2026
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.
@Jarred-Sumner Jarred-Sumner merged commit f72c015 into main Jun 18, 2026
3 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants