Skip to content

error: never return an empty JSValue from computeErrorInfoWrapperToJSValue - #34104

Merged
Jarred-Sumner merged 2 commits into
mainfrom
farm/fdf23f49/error-instance-getownpropslot-exception
Jul 14, 2026
Merged

error: never return an empty JSValue from computeErrorInfoWrapperToJSValue#34104
Jarred-Sumner merged 2 commits into
mainfrom
farm/fdf23f49/error-instance-getownpropslot-exception

Conversation

@robobun

@robobun robobun commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

computeErrorInfoWrapperToJSValue is Bun's vm.onComputeErrorInfoJSValue hook, called from ErrorInstance::materializeErrorInfoIfNeeded when a lazy error property (stack/line/column/sourceURL) is first read. When the hook throws before the default stack string has been computed (e.g. a throwing .message getter while Error.prepareStackTrace is set), computeErrorInfoToJSValue returns {}. materializeErrorInfoIfNeeded then putDirects that empty value into the error's stack slot, and the next read segfaults:

Error.prepareStackTrace = (e, s) => "custom";
const e = new Error("x");
Object.defineProperty(e, "message", { get() { throw new TypeError("boom"); } });
e.stack;   // Segmentation fault at address 0x5 (release), UBSan null deref (debug)

Fall back to jsUndefined() so the stored value is always valid. The .message throw still propagates to the caller.

About the getOwnPropertyDescriptor assertion in #34095

ErrorInstance::getOwnPropertySlot in WebKit does not check for an exception after materializeErrorInfoIfNeeded (unlike its siblings defineOwnProperty/put, which do). When the hook leaves one pending, JSObject::getOwnPropertyDescriptor trips:

ASSERTION FAILED: !scope.exception() || !result
vendor/WebKit/Source/JavaScriptCore/runtime/JSObject.cpp(3936)

An earlier revision of this PR wrapped the hook in DeferTerminationForAWhile to keep a TerminationException from reaching that assertion, but that scope would have covered the profiledCall into user Error.prepareStackTrace, making an infinite loop there uninterruptible by worker.terminate(). Dropped in 3dd57f6. A proper fix for the termination case is a RETURN_IF_EXCEPTION in ErrorInstance::getOwnPropertySlot on the WebKit side.

Related

Verification

New test in test/js/node/v8/capture-stack-trace.test.js spawns the .message-throws repro and asserts the first .stack read throws msg-boom and a subsequent read returns undefined instead of crashing. Segfaults on the unfixed build, passes with this change. The full test file (41 tests, including the existing e.stack-throws and prepareStackTrace-propagation tests) passes.

Refs #34095


[stamp-90s] gate passed · iteration 0 · 2 files touched

fails on main (without fix)
ASAN without fix: 1 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" "test/js/node/v8/capture-stack-trace.test.js"
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (3dd57f69d)

test/js/node/v8/capture-stack-trace.test.js:
(pass) Regular .stack [12.67ms]
(pass) throw inside Error.prepareStackTrace doesnt crash [6.81ms]
(pass) capture stack trace [6.22ms]
(pass) capture stack trace with message [6.94ms]
(pass) capture stack trace with constructor [4.93ms]
(pass) capture stack trace limit [21.63ms]
(pass) prepare stack trace [10.24ms]
(pass) capture stack trace second argument [17.05ms]
(pass) capture stack trace edge cases [11.07ms]
(pass) prepare stack trace call sites [12.77ms]
(pass) sanity check [13.28ms]
(pass) CallFrame isEval works as expected [6.69ms]
(pass) CallFrame isTopLevel returns false for Function constructor [7.85ms]
(pass) CallFrame.p.getThisgetFunction: strict/slopp
... (truncated)

release without fix: all passed
bun test v1.4.0-canary.1 (3dd57f69d)

test/js/node/v8/capture-stack-trace.test.js:
(pass) Regular .stack [0.39ms]
(pass) throw inside Error.prepareStackTrace doesnt crash [0.11ms]
(pass) capture stack trace [0.07ms]
(pass) capture stack trace with message [0.09ms]
(pass) capture stack trace with constructor [0.06ms]
(pass) capture stack trace limit [0.22ms]
(pass) prepare stack trace [0.12ms]
(pass) capture stack trace second argument [0.18ms]
(pass) capture stack trace edge cases [0.10ms]
(pass) prepare stack trace call sites [0.11ms]
(pass) sanity check [0.11ms]
(pass) CallFrame isEval works as expected [0.14ms]
(pass) CallFrame isTopLevel returns false for Function constructor [0.12ms]
(pass) CallFrame.p.getThisgetFunction: strict/sloppy mode interaction [0.11ms]
(pass) CallFrame.p.isConstructor [0.04ms]
(pass) CallFrame.p.isNative [0.04ms]
(pass) return non-strings from Error.prepareStackTrace [0.03ms]
(pass) CallFrame.p.toString [0.03ms]
(pass) err.stack should invoke prepareStackTrace [0.30ms]
(pass) Error.prepareStackTrace inside a node:vm works [4.77ms]
(pass) Error.captureStackTrace inside error constructor works [0.10ms]
(pass) Error.prepareStackTrace has 
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" "test/js/node/v8/capture-stack-trace.test.js"
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (3dd57f69d)

test/js/node/v8/capture-stack-trace.test.js:
(pass) Regular .stack [12.34ms]
(pass) throw inside Error.prepareStackTrace doesnt crash [6.56ms]
(pass) capture stack trace [6.06ms]
(pass) capture stack trace with message [6.90ms]
(pass) capture stack trace with constructor [4.93ms]
(pass) capture stack trace limit [21.98ms]
(pass) prepare stack trace [10.25ms]
(pass) capture stack trace second argument [17.39ms]
(pass) capture stack trace edge cases [11.23ms]
(pass) prepare stack trace call sites [12.74ms]
(pass) sanity check [12.78ms]
(pass) CallFrame isEval works as expected [6.62ms]
(pass) CallFrame isTopLevel returns false for Function constructor [7.84ms]
(pass) CallFrame.p.getThisgetFunction: strict/slopp
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
[configured] bun-profile → bun (stripped) in 643ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/26] gen generated_host_exports.rs
generated_host_exports.rs: 91 exports (host=3, lazy=10, generic=78, rust=0); 243 extern-C blocks audited
[2/26] gen cpp.rs (cppbind)
[3/26] gen JS modules (bundle-modules)
Preprocess modules (6586ms)
Bundle modules (26ms)
Postprocesss modules (26ms)
Bundle Functions (664ms)
Generate Code (74ms)

[7.39s] Bundled "src/js" for production
  1912 kb
  162 internal modules
  12 native modules
  90 internal functions across 19 files
[3/17] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: component rust
... (truncated)
diff hotspot
src/jsc/bindings/FormatStackTraceForJS.cpp  |  4 ++++
 test/js/node/v8/capture-stack-trace.test.js | 24 ++++++++++++++++++++++++
 2 files changed, 28 insertions(+)

gate history · 2 passed · 0 rejected · iteration 0

evidence per changed file
file                                         reads  edits  tests
src/jsc/bindings/FormatStackTraceForJS.cpp       9      9      0
test/js/node/v8/capture-stack-trace.test.js      2      3      0

…WrapperToJSValue

ErrorInstance::getOwnPropertySlot does not check for an exception after
materializeErrorInfoIfNeeded calls vm.onComputeErrorInfoJSValue, so a
TerminationException raised inside the hook trips
JSObject::getOwnPropertyDescriptor's EXCEPTION_ASSERT(!scope.exception()
|| !result). Defer termination across the hook so the request is handled
at the next trap checkpoint after the slot is filled.

Separately, when the hook throws before the default stack string is
computed (e.g. a throwing .message getter), it returned an empty JSValue
which materializeErrorInfoIfNeeded putDirect()s into the error's stack
slot; the next read of e.stack then dereferences JSValue() and
segfaults. Fall back to jsUndefined() so the stored value is always
valid.

Refs #34095
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

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

Next review available in: 5 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 83ab3a90-4ec2-409b-91af-c896758664d4

📥 Commits

Reviewing files that changed from the base of the PR and between 73b6c14 and 3dd57f6.

📒 Files selected for processing (2)
  • src/jsc/bindings/FormatStackTraceForJS.cpp
  • test/js/node/v8/capture-stack-trace.test.js

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

@robobun

robobun commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 8:05 PM PT - Jul 13th, 2026

@robobun, your commit 3dd57f6 is building: #72623

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. error: swallow Error.prepareStackTrace throws in computeErrorInfoWrapperToJSValue #30823 - Also fixes exception handling in computeErrorInfoWrapperToJSValue, using an alternative approach (tryClearException() vs DeferTerminationForAWhile)

🤖 Generated with Claude Code

@robobun

robobun commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator Author

Related but not a duplicate: #30823 uses tryClearException(), which does not clear a TerminationException (see ExceptionScope::tryClearException returning false for it), so the worker-terminate path in #34095 would still trip the assertion. This PR uses DeferTerminationForAWhile for that case and keeps e.stack propagating a throwing prepareStackTrace (the existing test at capture-stack-trace.test.js:22). Covered in the Related section of the description.

Comment thread src/jsc/bindings/FormatStackTraceForJS.cpp Outdated
The deferral wrapped the profiledCall into user Error.prepareStackTrace,
so a worker with an infinite loop there could no longer be interrupted
by worker.terminate(). The termination case for ErrorInstance
materialization needs a WebKit-side RETURN_IF_EXCEPTION in
getOwnPropertySlot instead; only the empty-return guard remains here.
@robobun robobun changed the title error: defer termination and never return empty from computeErrorInfoWrapperToJSValue error: never return an empty JSValue from computeErrorInfoWrapperToJSValue Jul 14, 2026

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — the DeferTerminationForAWhile from my earlier review was dropped in 3dd57f6; what remains is the 2-line guard swapping an empty JSValue for jsUndefined() before it reaches putDirect.

Checked: the guard only fires on the previously-crashing path (empty result when the hook throws); the pending exception from the .message getter still propagates unchanged. The new subprocess test asserts both the throw on the first .stack read and typeof e.stack === "undefined" (not a crash) on the second.
Nit: PR title/description still describe the dropped DeferTerminationForAWhile half.

Extended reasoning...

Overview

After 3dd57f6 addressed my earlier feedback (removing DeferTerminationForAWhile, which would have made a hanging prepareStackTrace uninterruptible by worker.terminate()), the PR is now a 4-line change to computeErrorInfoWrapperToJSValue in src/jsc/bindings/FormatStackTraceForJS.cpp plus a ~24-line spawned regression test in test/js/node/v8/capture-stack-trace.test.js.

The C++ change: when computeErrorInfoToJSValue returns {} (via any of its RETURN_IF_EXCEPTION(scope, {}) paths — e.g. a throwing .message getter while Error.prepareStackTrace is set), return jsUndefined() instead. ErrorInstance::materializeErrorInfoIfNeeded unconditionally putDirects this hook's return value into the stack slot, and an empty JSValue there segfaults the next property read.

Security risks

None. This is a defensive null-guard on an internal hook return value. It does not touch input parsing, auth, crypto, or resource limits. The only observable behavior change is that a scenario which previously crashed the process now stores undefined in .stack while the original exception continues to propagate.

Level of scrutiny

Low-to-moderate. JSC bindings are a sensitive area, but this specific change is a pure guard: the only input state it affects (result is empty) was previously undefined behavior (crash). It cannot alter the non-empty path. The exception state is untouched — the throw still surfaces to the caller, as the test verifies (first === "msg-boom") and as the existing throw inside Error.prepareStackTrace doesnt crash test at line 22 continues to cover.

Other factors

  • My prior inline concern is resolved and marked as such in the thread.
  • The gate evidence shows the test fails (segfault/UBSan) on ASAN without the fix and passes on both ASAN and release with it; the full 41-test file passes.
  • The test follows harness conventions: bunExe()/bunEnv, Bun.spawn with -e, Promise.all draining stdout/stderr/exited concurrently, asserts output/signalCode before exitCode.
  • The PR title and first section of the description still describe the dropped DeferTerminationForAWhile change; that's cosmetic and the timeline documents the reduction, so not blocking.
  • Overlaps with #30823 (different approach, tryClearException) — a maintainer will want to reconcile, but this change is strictly a crash fix and safe to land independently.

@Jarred-Sumner
Jarred-Sumner merged commit 16c5576 into main Jul 14, 2026
76 of 77 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/fdf23f49/error-instance-getownpropslot-exception branch July 14, 2026 02:30
robobun added a commit that referenced this pull request Jul 14, 2026
Reading the property can run a user getter. Before #34104 a pending
exception here meant computeErrorInfoToJSValue returned an empty JSValue,
which materializeErrorInfoIfNeeded putDirect'd into .stack and the process
died on a null JSCell deref (UBSan: "member call on null pointer of type
JSC::JSCell"). computeErrorInfoWrapperToJSValue now maps an empty return to
jsUndefined(), so propagate and let the wrapper handle it, which is what
Node does and what the existing #34095 test right above expects for the
.message-throws variant of the same shape.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants