Skip to content

addErrorInfo: keep a stack hook exception from escaping ParserError::toErrorObject - #535

Open
robobun wants to merge 1 commit into
mainfrom
robobun/add-error-info-hook-exception
Open

addErrorInfo: keep a stack hook exception from escaping ParserError::toErrorObject#535
robobun wants to merge 1 commit into
mainfrom
robobun/add-error-info-hook-exception

Conversation

@robobun

@robobun robobun commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • With validateExceptionChecks=1, any new Function(source) whose source has a syntax error aborts the process:
    ERROR: Unchecked JS exception:
        This scope can throw a JS exception: computeErrorInfoToJSValueWithoutSkipping @ src/jsc/bindings/FormatStackTraceForJS.cpp:535
        But the exception was unchecked as of this scope: constructFunctionSkippingEvalEnabledCheck @ runtime/FunctionConstructor.cpp:220
    
    GeneratorFunction, AsyncFunction and Bun's vm.SourceTextModule hit the same abort. eval("{") does not: an eval parse error is ParserError::EvalError, which never calls addErrorInfo.
  • Cause: ParserError::toErrorObject() calls addErrorInfo() (Error.cpp:243), which materializes the error info at once. In the Bun fork that runs VM::onComputeErrorInfoJSValue, and Bun's hook calls a user Error.prepareStackTrace, so it declares a ThrowScope and can throw. Upstream's computeErrorInfo cannot throw, so every toErrorObject() caller throws the returned error with no exception check in between (FunctionConstructor.cpp:226, ModuleProgramExecutable.cpp:68, ScriptExecutable.cpp:320, UnlinkedFunctionExecutable.cpp:238).

Fix

  • addErrorInfo() materializes under a TopExceptionScope and clears a hook exception there. clearExceptionExceptTermination() leaves a termination pending, and VM::throwException keeps a pending termination over the parse error.
  • This keeps toErrorObject() non-throwing, which is the contract all of its callers rely on. The parse error is what gets thrown. That is what Node does too: V8 does not run prepareStackTrace while it builds a SyntaxError.
  • Behavior in release builds does not change. There the hook exception was left pending and VM::throwException(parseError) replaced it. Bun's node:vm callers of toErrorObject() already clear that exception by hand (NodeVM.cpp, NodeVMScript.cpp); this covers the callers inside JSC.
  • Verified: the validator's rules in ThrowScope.cpp and TopExceptionScope.cpp (a TopExceptionScope destructor does not simulate a throw, so the caller's throwException sees no pending check). I could not build JSC in this environment, so CI is the build check. The Bun side is oven-sh/bun (test in test/js/bun/jsc/exception-checks.test.ts), which pins the preview build of this PR.

Background

  • validateExceptionChecks=1 makes every ThrowScope destructor mark VM::m_needExceptionCheck. The next ThrowScope constructor, or a throwException of a non-Exception value, asserts if the bit is still set. VM::exception() and clearException() clear it.
  • TopExceptionScope (the old CatchScope) only verifies in its destructor. Code that must not propagate an exception uses it, as createTypeErrorCopy in the same file does.
  • ErrorInstance computes stack, line, column and sourceURL lazily. materializeErrorInfoIfNeeded builds them from the captured frames. The Bun fork lets the embedder do that through VM::onComputeErrorInfoJSValue, which is where Error.prepareStackTrace runs.

…toErrorObject

Bun's VM::onComputeErrorInfoJSValue hook runs a user Error.prepareStackTrace
when addErrorInfo materializes the error info of a new parse error. Callers
of ParserError::toErrorObject (constructFunctionSkippingEvalEnabledCheck,
ModuleProgramExecutable::getUnlinkedCodeBlock, ScriptExecutable::newCodeBlockFor)
treat it as non-throwing and throw the returned error without an exception
check. With validateExceptionChecks=1 that aborts on every `new Function`
whose source has a syntax error.

Materialize under a TopExceptionScope and clear a hook exception there. The
parse error is what gets thrown. A termination exception stays pending.

@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 reviewed this and didn't find any bugs. Because it changes exception-scope handling in the JSC runtime error path (CODEOWNERS routes Source/JavaScriptCore to jsc-reviewers) and the author notes it wasn't built locally, a human familiar with the exception-scope validator should still take a look.

What was reviewed:

  • #if/#else restructure — the non-Bun path still just calls materializeErrorInfoIfNeeded(vm) and falls through to the putDirect block, semantically identical to before.
  • DECLARE_TOP_EXCEPTION_SCOPE + clearExceptionExceptTermination() matches the existing pattern at createTypeErrorCopy (Error.cpp:293) and the definition in TopExceptionScope.h; termination stays pending as described.
  • Bun path still returns errorInstance early, so the trailing putDirect(line/sourceURL) remains skipped as before.
Extended reasoning...

Overview

The PR modifies a single function, addErrorInfo(VM&, JSObject*, int, const SourceCode&) in Source/JavaScriptCore/runtime/Error.cpp. Inside the USE(BUN_JSC_ADDITIONS) branch it now wraps errorInstance->materializeErrorInfoIfNeeded(vm) in a DECLARE_TOP_EXCEPTION_SCOPE and calls scope.clearExceptionExceptTermination() afterward, so that an exception thrown by Bun's Error.prepareStackTrace hook (via VM::onComputeErrorInfoJSValue) is swallowed and ParserError::toErrorObject() remains effectively non-throwing for its callers. The #if/#endif blocks are collapsed into a single #if/#else/#endif; the #else arm preserves the original unguarded materializeErrorInfoIfNeeded call for non-Bun builds.

Security risks

None identified. The change does not introduce new inputs, parsing, allocation, or trust boundaries. It clears a user-hook exception rather than propagating it, which is a hardening/correctness fix under validateExceptionChecks=1. clearExceptionExceptTermination() explicitly preserves a pending termination exception (verified in TopExceptionScope.h), so termination semantics are unchanged.

Level of scrutiny

Moderate-to-high. The diff is small and follows an established idiom already used a few lines below in createTypeErrorCopy, and I confirmed the non-Bun preprocessor path is byte-for-byte equivalent in effect. However, JSC's exception-scope validator has subtle destructor/needExceptionCheck rules, Source/JavaScriptCore is CODEOWNER-routed to jsc-reviewers, and the author states they could not build JSC locally and are relying on CI. Those factors together warrant a human reviewer with JSC exception-scope experience signing off rather than an auto-approval.

Other factors

The TopExceptionScope header is already used in this translation unit, so no new include is needed. The Bun-side test lives in the companion oven-sh/bun PR per the description; there is no test change in this repo. Release-build behavior is claimed unchanged (the hook exception was previously overwritten by the subsequent throwException), which is consistent with what I read, but I could not execute a build here to confirm the validator is satisfied.

@coderabbitai

coderabbitai Bot commented Aug 29, 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: 124a3409-6eb5-4c5b-8195-02c3547b5cb1

📥 Commits

Reviewing files that changed from the base of the PR and between ceb9f90 and e6ad39a.

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

Included review availability: Your plan provides up to 5 included reviews per hour; 1 remains after this review.


Walkthrough

Changes

Error info handling

Layer / File(s) Summary
Bun error materialization
Source/JavaScriptCore/runtime/Error.cpp
Bun builds wrap materializeErrorInfoIfNeeded in a top exception scope. Non-termination exceptions from Error.prepareStackTrace are cleared, and the original error remains thrown. Non-Bun builds retain direct materialization.

Merge Risk: ⚪ Minimal · up to e6ad3

This localized change prevents stack-trace hook exceptions from escaping while preserving the original parse error; no actionable merge-blocking risk remains after normal checks and review.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: preventing stack-hook exceptions from escaping ParserError::toErrorObject().
Description check ✅ Passed The description clearly explains the problem, cause, fix, affected callers, behavior, and verification status. It does not include the required Bugzilla link or explicit changed-file summary, but the …
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.
Full details: Description check

Explanation

The description clearly explains the problem, cause, fix, affected callers, behavior, and verification status. It does not include the required Bugzilla link or explicit changed-file summary, but the technical content is substantially complete.

Warning

Git: CodeRabbit could not clone the repository, so clone-backed analysis was skipped and this review may be incomplete. Verify repository clone access, such as SSH credentials, before requesting another full review. If clone access is intentionally unavailable, use path_filters to narrow the review scope.


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

@github-actions

Copy link
Copy Markdown

Preview Builds

Commit Release Date
e6ad39a7 autobuild-preview-pr-535-e6ad39a7 2026-08-29 03:36:59 UTC

@robobun

robobun commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator Author

Bun side: oven-sh/bun#40866 pins the preview build autobuild-preview-pr-535-e6ad39a7 and adds the tests. The six new snippets abort on the current pin and pass on the preview build; test/js/node/vm/vm.test.ts, test/js/node/v8/capture-stack-trace.test.js, test/js/bun/test/stack.test.ts and test/js/web/workers/structured-clone.test.ts pass under BUN_JSC_validateExceptionChecks=1 with it.

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.

1 participant