Skip to content

getAsyncStackTrace: unwrap InternalFieldTuple in reaction context - #347

Merged
Jarred-Sumner merged 1 commit into
mainfrom
farm/e9e0550c/unwrap-als-tuple-in-async-stack
Jul 25, 2026
Merged

getAsyncStackTrace: unwrap InternalFieldTuple in reaction context#347
Jarred-Sumner merged 1 commit into
mainfrom
farm/e9e0550c/unwrap-als-tuple-in-async-stack

Conversation

@robobun

@robobun robobun commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

Under USE(BUN_JSC_ADDITIONS), JSPromise::resolveWithInternalMicrotaskForAsyncAwait wraps the awaiting generator in InternalFieldTuple(generator, asyncContext) when an AsyncLocalStorage store is active, so the context can be restored when the microtask resumes.

Interpreter::getAsyncStackTrace's getParentGenerator reads that context via asyncStackTraceContext() and casts it with dynamicDowncast<JSAsyncFunctionGenerator>, which fails on the tuple, so every at async frame is dropped from error.stack under ALS. Unwrapping in getContextValueFromPromise covers the direct-await, JSPromiseCombinatorsGlobalContext, and Promise.race branches that all flow through it.

Bun-side test: oven-sh/bun#35772 (test/regression/issue/24003.test.ts), which bumps WEBKIT_VERSION to this branch's preview autobuild.

Under USE(BUN_JSC_ADDITIONS), JSPromise::resolveWithInternalMicrotaskForAsyncAwait
wraps the awaiting generator in InternalFieldTuple(generator, asyncContext) when
an AsyncLocalStorage store is active. getParentGenerator reads that context and
casts it with dynamicDowncast<JSAsyncFunctionGenerator>, which fails on the
tuple, so every at-async frame is dropped from error.stack.

Unwrap the tuple in getContextValueFromPromise so the direct-await, combinator,
and Promise.race branches all see the underlying context.
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 7 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: aa9a5b00-9321-4a94-b790-38b65d774a06

📥 Commits

Reviewing files that changed from the base of the PR and between b11589f and 2a259d2.

📒 Files selected for processing (1)
  • Source/JavaScriptCore/interpreter/Interpreter.cpp

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

@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.

LGTM — small, guarded diagnostic fix.

What was reviewed:

  • Verified AsyncContextSwapScope::wrapWithCurrent stores [userContext, asyncContext], so getInternalField(0) reads the correct slot.
  • dynamicDowncast<InternalFieldTuple>(JSValue) matches the existing pattern in getParentGenerator and is null-safe under the if (context) guard.
  • Change is under AssertNoGC; getInternalField is a plain WriteBarrier read with no allocation.
  • Only affects async stack-trace output, not execution semantics; entirely gated by USE(BUN_JSC_ADDITIONS).
Extended reasoning...

Overview

This PR adds ~10 lines to Interpreter::getAsyncStackTrace's getContextValueFromPromise lambda in Interpreter.cpp, plus a conditional #include "InternalFieldTuple.h". When the promise reaction context is an InternalFieldTuple (which AsyncContextSwapScope::wrapWithCurrent creates to bundle the awaiting generator with the captured ALS context), the code now unwraps slot 0 to recover the original generator instead of failing the subsequent dynamicDowncast<JSAsyncFunctionGenerator> and dropping the async frame.

Security risks

None. The change is read-only, uses dynamicDowncast (type-checked, returns nullptr on mismatch), and only influences the contents of error.stack. It runs inside an AssertNoGC region and getInternalField(0) is a simple WriteBarrier<Unknown>::get() with no allocation or side effects. No auth, crypto, or memory-safety-sensitive paths are touched.

Level of scrutiny

Low-to-moderate. While this is JSC interpreter code, the change is (a) fully gated behind #if USE(BUN_JSC_ADDITIONS), (b) confined to a diagnostic path that builds stack-trace strings, and (c) defensive — if the context is not an InternalFieldTuple, behavior is byte-for-byte identical to before. I cross-checked AsyncContextSwapScope.h and confirmed the tuple layout is [userContext, asyncContext], so index 0 is correct. InternalFieldTuple is documented as used exclusively for ALS bookkeeping, so there is no ambiguity about other tuple producers colliding here; even if one did, the downstream dynamicDowncast<JSAsyncFunctionGenerator> would simply return nullptr (same as pre-patch behavior).

Other factors

Placing the unwrap in getContextValueFromPromise is the right spot — it covers all three call sites (direct await, JSPromiseCombinatorsGlobalContext, and the Promise.race chain) without duplication. A Bun-side regression test (test/regression/issue/24003.test.ts in oven-sh/bun#35772) exercises the fix. No outstanding reviewer comments.

robobun added a commit to oven-sh/bun that referenced this pull request Jul 25, 2026
The hook is now registered the first time AsyncLocalStorage turns tracking on,
so code that never touches ALS pays nothing at error-capture time. Also point
at oven-sh/WebKit#347, which unwraps the tuple inside getAsyncStackTrace and
will make this hook unnecessary once WEBKIT_VERSION moves past it.
robobun added a commit to oven-sh/bun that referenced this pull request Jul 25, 2026
The hook is now registered the first time AsyncLocalStorage turns tracking on,
so code that never touches ALS pays nothing at error-capture time. Also point
at oven-sh/WebKit#347, which unwraps the tuple inside getAsyncStackTrace and
will make this hook unnecessary once WEBKIT_VERSION moves past it.
robobun added a commit to oven-sh/bun that referenced this pull request Jul 25, 2026
The hook is now registered the first time AsyncLocalStorage turns tracking on,
so code that never touches ALS pays nothing at error-capture time. Also point
at oven-sh/WebKit#347, which unwraps the tuple inside getAsyncStackTrace and
will make this hook unnecessary once WEBKIT_VERSION moves past it.
@github-actions

Copy link
Copy Markdown

Preview Builds

Commit Release Date
2a259d26 autobuild-preview-pr-347-2a259d26 2026-07-25 20:52:25 UTC

@Jarred-Sumner
Jarred-Sumner merged commit d103ebd into main Jul 25, 2026
43 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