Skip to content

JSC: tolerate pending TerminationException in property-lookup assertions#286

Open
Jarred-Sumner wants to merge 2 commits into
mainfrom
jsc/tolerate-termination-in-property-asserts
Open

JSC: tolerate pending TerminationException in property-lookup assertions#286
Jarred-Sumner wants to merge 2 commits into
mainfrom
jsc/tolerate-termination-in-property-asserts

Conversation

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

Assertion builds abort when worker.terminate() races script execution: DeferTermination throws the TerminationException from its destructor, i.e. after a property-slot lookup already succeeded, tripping ASSERTION FAILED: !scope.exception() || !result.

JSObject::get already tolerates this via hasPendingTerminationException() in its assertion (upstream pattern). This applies the same tolerance to the three sibling sites observed aborting in CI stress runs:

  • JSValue::get (both overloads in JSCJSValuePropertyInlines.h) — hit via LLInt get_by_id during module evaluation
  • JSObject::getOwnPropertyDescriptor (JSObject.cpp:3936) — hit via Object.defineProperty
  • JSModuleLoader::continueDynamicImportassertNoException()assertNoExceptionExceptTermination() (rejectWithCaughtException's TRY_CLEAR_EXCEPTION cannot clear a termination, so it stays pending by design)

No exception is ever cleared — only the sticky termination is tolerated and RETURN_IF_EXCEPTION lets callers unwind.

Verification: worker terminate() stress loop (320 runs under load, BUN_JSC_validateExceptionChecks=1): 3/320 aborts before, 0/320 after, plus 0/240 on the original failing node test. Other EXCEPTION_ASSERT(!scope.exception() || !success) sites (ProxyObject, JSONObject, …) are latently susceptible to the same race; this fixes the three empirically observed, matching the JSObject::get precedent.

DeferTermination throws the TerminationException from its destructor, after
a property-slot lookup has already succeeded. JSObject::get already tolerates
this (hasPendingTerminationException in its assertion); three sibling sites
did not and abort assertion builds when terminate() races script execution:

- JSValue::get (both overloads): same tolerance + RETURN_IF_EXCEPTION, hit
  via LLInt get_by_id during module evaluation
- JSObject::getOwnPropertyDescriptor: same, hit via Object.defineProperty
- JSModuleLoader::continueDynamicImport: assertNoException ->
  assertNoExceptionExceptTermination (rejectWithCaughtException cannot clear
  a termination, so it stays pending by design)

No exception is ever cleared; callers' existing checks unwind as usual.
Reproduced with a worker terminate() stress loop: 3/320 aborts before,
0/320 + 0/240 after.
@coderabbitai

coderabbitai Bot commented Jul 13, 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: f9a6ebbb-8743-4b0f-a048-e6c13aec1a7a

📥 Commits

Reviewing files that changed from the base of the PR and between 234d8b3 and bed16a3.

📒 Files selected for processing (3)
  • Source/JavaScriptCore/runtime/JSCJSValuePropertyInlines.h
  • Source/JavaScriptCore/runtime/JSModuleLoader.cpp
  • Source/JavaScriptCore/runtime/JSObject.cpp

Walkthrough

Changes

Termination exception handling

Layer / File(s) Summary
Property lookup termination handling
Source/JavaScriptCore/runtime/JSCJSValuePropertyInlines.h, Source/JavaScriptCore/runtime/JSObject.cpp
Property lookup assertions now allow pending termination exceptions, and affected paths explicitly return undefined or false while unwinding.
Dynamic import abrupt completion
Source/JavaScriptCore/runtime/JSModuleLoader.cpp
Dynamic import rejection handling now permits a pending TerminationException during abrupt completion.

Suggested reviewers: robobun

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning It describes the bug, affected sites, and verification, but it does not follow the required PR template or include the Bugzilla/commit-message sections. Rewrite the description using the repository template, including the Bugzilla link, Reviewed by line, bug explanation, and changed-file bullets.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main change: relaxing property-lookup assertions to tolerate pending TerminationException.
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.

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.

I didn't find any issues — the change mirrors the existing JSObject::get pattern in JSObjectInlines.h:137-138 exactly, and assertNoExceptionExceptTermination() is the standard idiom for this. Deferring only because JSValue::get and JSObject::getOwnPropertyDescriptor are core hot paths and the added RETURN_IF_EXCEPTION is a (benign-looking) release-build change; a JSC maintainer should give it a quick glance.

What was reviewed:

  • Confirmed the precedent: JSObjectInlines.h already uses EXCEPTION_ASSERT(... || vm.hasPendingTerminationException() || ...) + RETURN_IF_EXCEPTION for both JSObject::get overloads.
  • EXCEPTION_ASSERT_UNUSEDEXCEPTION_ASSERT in getOwnPropertyDescriptor is correct since scope is now used by RETURN_IF_EXCEPTION.
  • The new RETURN_IF_EXCEPTION doesn't change the non-termination path (invariant already guaranteed !hasSlotjsUndefined()); only the termination-race case now returns early instead of proceeding into slot.getValue().
Extended reasoning...

Overview

This PR relaxes four debug-only exception assertions to tolerate a pending TerminationException, matching the established pattern already present in JSObject::get (JSObjectInlines.h:137,153). The affected sites are:

  • JSValue::get(JSGlobalObject*, PropertyName, PropertySlot&) and the unsigned overload in JSCJSValuePropertyInlines.h
  • JSObject::getOwnPropertyDescriptor in JSObject.cpp
  • JSModuleLoader::continueDynamicImport in JSModuleLoader.cpp (assertNoExceptionassertNoExceptionExceptTermination)

Each site also gains a RETURN_IF_EXCEPTION so that when the race does occur, the function unwinds cleanly rather than calling into slot.getValue() with a pending exception.

Security risks

None. The change only weakens debug assertions to admit a case that already occurs at runtime (termination racing a successful slot lookup), and adds an early-return-on-exception which is strictly safer than proceeding. No exceptions are cleared or swallowed — RETURN_IF_EXCEPTION propagates and the sticky termination remains pending for the caller.

Level of scrutiny

Medium-high. The change itself is mechanical and precedented — I verified JSObjectInlines.h:137-138 and :153-154 use the identical construct, and assertNoExceptionExceptTermination() is used in 6 other runtime files. However, JSValue::get is one of the most central C++ property-access helpers in JSC, and getOwnPropertyDescriptor backs Object.defineProperty/Object.getOwnPropertyDescriptor. Adding a RETURN_IF_EXCEPTION to JSValue::get is a release-build change on a very widely-called path. The change should be a no-op for the non-exception case (one predicted-not-taken branch) and for ordinary exceptions (the old invariant guaranteed !hasSlot, so both old and new code return jsUndefined()), but a JSC maintainer should confirm.

Other factors

  • The EXCEPTION_ASSERT_UNUSEDEXCEPTION_ASSERT switch in getOwnPropertyDescriptor is correct: scope is now used by the new RETURN_IF_EXCEPTION, so the _UNUSED marker is no longer needed.
  • The second JSValue::get overload calls getVM(globalObject) inline rather than caching it in a local like the first overload; harmless (getVM is a trivial pointer chase) but slightly inconsistent.
  • The PR description notes other EXCEPTION_ASSERT(!scope.exception() || !success) sites (ProxyObject, JSONObject) are latently susceptible; a maintainer may want to weigh in on whether those should be batched in.
  • Empirically verified per the description (320-run stress loop, 3→0 aborts).
  • No prior reviews or comments on the PR.

@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 — mechanical application of the existing JSObject::get termination-tolerance pattern to three sibling sites.

What was reviewed:

  • Confirmed JSObjectInlines.h:137/153 already uses the identical hasPendingTerminationException() + RETURN_IF_EXCEPTION shape being copied here.
  • assertNoExceptionExceptTermination() is an existing helper (ExceptionScope.h:63/101); the module-loader change is a straight swap.
  • EXCEPTION_ASSERT_UNUSEDEXCEPTION_ASSERT in getOwnPropertyDescriptor is correct now that scope is used by the new RETURN_IF_EXCEPTION.
  • The added early returns only fire with an exception already pending, so callers' own RETURN_IF_EXCEPTION checks discard the sentinel — no observable behavior change in the non-termination path.
Extended reasoning...

Overview

The PR relaxes four debug-only assertion sites to tolerate a pending TerminationException and adds RETURN_IF_EXCEPTION early-outs at three of them. Touches JSValue::get (two overloads), JSObject::getOwnPropertyDescriptor, and one assertNoException()assertNoExceptionExceptTermination() swap in JSModuleLoader::continueDynamicImport. The motivation is a worker.terminate() race where DeferTermination's destructor throws after a slot lookup already succeeded, tripping !scope.exception() || !result in assertion builds.

Security risks

None. The change loosens debug assertions by one specific disjunct (vm.hasPendingTerminationException()) and adds early returns that only trigger when an exception is already set. No exception is cleared, no new state is exposed, and release-build semantics change only in that a pending-exception path now short-circuits rather than proceeding into slot.getValue() / descriptor.setPropertySlot() — which is strictly more defensive.

Level of scrutiny

Low-to-medium. Although JSValue::get is an extremely hot inlined path, this is a verbatim copy of the pattern already present in JSObject::get (JSObjectInlines.h:137, :153) — same assertion shape, same RETURN_IF_EXCEPTION(scope, jsUndefined()). The EXCEPTION_ASSERT compiles out entirely in release; the only release-visible addition is the RETURN_IF_EXCEPTION branch, which is standard JSC exception hygiene and mirrors the sibling function exactly. The getOwnPropertyDescriptor change correctly drops the _UNUSED macro variant now that scope is consumed by RETURN_IF_EXCEPTION. The module-loader change uses an existing purpose-built helper.

Other factors

The PR description documents empirical verification: 3/320 → 0/320 aborts under a terminate() stress loop with validateExceptionChecks=1, plus 0/240 on the original failing test. The fix is narrowly scoped to the three empirically observed abort sites rather than a blanket sweep, and explicitly notes other latently-susceptible sites are left for later — a conservative approach. No design decisions are being made here; this is CI-stability plumbing following upstream precedent.

@github-actions

Copy link
Copy Markdown

Preview Builds

Commit Release Date
98e25e08 autobuild-preview-pr-286-98e25e08 2026-07-17 04:07:43 UTC

robobun added a commit to oven-sh/bun that referenced this pull request Jul 18, 2026
The previous preview was based on WebKit main, which pulled in the
JSMicrotask/JSPromise refactors (oven-sh/WebKit#295/#301) and nudged the
node-net mimalloc-page-count leak test past its threshold on aarch64.
The minimal branch carries only the termination fixes the new stress
test needs (plus Jarred's oven-sh/WebKit#286 for the property-lookup
assertions the same sweep surfaces).
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