Skip to content

Fix assertion failure when inspecting an object whose lazy property initializer throws - #37234

Closed
robobun wants to merge 2 commits into
mainfrom
farm/e6fe688f/fix-lazy-prop-pending-exception
Closed

Fix assertion failure when inspecting an object whose lazy property initializer throws#37234
robobun wants to merge 2 commits into
mainfrom
farm/e6fe688f/fix-lazy-prop-pending-exception

Conversation

@robobun

@robobun robobun commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

Fixes a deterministic abort found by fuzzing (fingerprint a970dd8104b66865).

Repro

Symbol++; // global Symbol becomes NaN
Bun.inspect(Bun);

or, through error message construction:

Symbol++;
new CompressionStream(globalThis); // ERR_INVALID_ARG_VALUE renders globalThis with the inspector

On assert-enabled builds both abort with:

ASSERTION FAILED: Unexpected exception observed on thread ...
Error Exception: Symbol is not a function. (In 'Symbol("cwd")', 'Symbol' is NaN)
ExceptionScope.h(62) : void JSC::ExceptionScope::releaseAssertNoException()

What happens

Bun.$ and Bun.sql are static-table PropertyCallback entries whose initializers run JS (shell.ts, sql.ts). When the global Symbol has been overwritten, those initializers throw. JSC's setUpStaticFunctionSlot handles that by reporting the slot as not found with the exception left pending, and callers are expected to check.

The property walk in JSC__JSValue__forEachPropertyImpl (used by Bun.inspect, console.log, and node-style error messages that render the received value) did this:

if (!object->getPropertySlot(globalObject, property, slot))
    continue;               // skips the clear below
CLEAR_IF_EXCEPTION(scope);

so the pending exception survived into the next property's reification, which aborts debug builds and can surface the stale exception from unrelated code in release builds. The fix hoists the clear above the continue, matching how the walk already treats throwing getters and proxy traps: the property is skipped.

With that fixed, the same repro reached a second assert: the Bun.sql lazy getters had a debug-only block that called reportUncaughtExceptionAtEventLoop while the module evaluation exception was still pending. That re-enters process.get and JS with a pending exception, which trips a stale-structure assertion in JSObject::getPropertySlot (reification transitions the structure, then the pre-existing pending exception makes the lookup report not-found, and the cached Structure* no longer matches). The RETURN_IF_EXCEPTION right after already propagates the error to the caller, so the block is removed.

After the fix the repros run to completion: inspect output simply omits the properties whose initializers threw, and direct access like Bun.$ still throws the initializer's error to the caller as before.

Tests

Added two regression tests in test/js/bun/util/inspect.test.js. Both abort the unfixed debug build (SIGABRT) and pass with the fix. The assertion only exists in assert-enabled builds, so they pass on release builds either way.


no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/bun/util/inspect.test.js

A static-table PropertyCallback that runs JS (Bun.$, Bun.sql) can throw,
for example when the global Symbol has been overwritten. JSC reports the
slot as not found with the exception left pending, but the property walk
in JSC__JSValue__forEachPropertyImpl skipped its exception clear on that
branch, so the stale exception aborted assert-enabled builds at the next
property's reification and could leak into unrelated code in release
builds.

Also remove the debug-only uncaught exception report from the Bun.sql
lazy getters: it re-entered the uncaught exception machinery with the
exception still pending, tripping a stale-structure assertion inside
process.get. The exception already propagates to the caller.
@github-actions github-actions Bot added the claude label Aug 9, 2026
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b2e9c751-fac7-43cb-ac33-88c2fd2d10ea

📥 Commits

Reviewing files that changed from the base of the PR and between ead7111 and ae03357.

📒 Files selected for processing (1)
  • test/js/bun/util/inspect.test.js

Walkthrough

Changes

Lazy property exception handling

Layer / File(s) Summary
Exception-safe property enumeration
src/jsc/bindings/BunObject.cpp, src/jsc/bindings/bindings.cpp
Removed SQL lazy-property debug exception reporting. Property enumeration now clears lookup exceptions and skips missing properties.
Inspection regression coverage
test/js/bun/util/inspect.test.js
Added subprocess tests for corrupted Symbol inspection and invalid CompressionStream arguments. Both verify successful completion without signal termination.

Possibly related PRs

  • oven-sh/bun#37107: Updates the same property enumeration logic and adds related Bun.inspect regression tests.
  • oven-sh/bun#37175: Modifies the same exception handling and inspection test areas.
  • oven-sh/bun#37213: Contains overlapping changes in BunObject.cpp, bindings.cpp, and inspect.test.js.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the assertion failure fixed by the changes.
Description check ✅ Passed The description explains the bug, implementation, reproduction cases, and regression tests, although it does not use the template headings exactly.
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.

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/js/bun/util/inspect.test.js`:
- Around line 944-950: Update both subprocess test sites in
test/js/bun/util/inspect.test.js at lines 944-950 and 972-978 to consume
proc.stderr.text() concurrently with proc.stdout.text() and proc.exited in the
existing Promise.all calls. Apply the same change to both sites so every
Bun.spawn invocation drains stdout, stderr, and process completion without
altering the test assertions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b99d307c-b14c-47fe-9989-f0538cd25db3

📥 Commits

Reviewing files that changed from the base of the PR and between 9008ae7 and ead7111.

📒 Files selected for processing (3)
  • src/jsc/bindings/BunObject.cpp
  • src/jsc/bindings/bindings.cpp
  • test/js/bun/util/inspect.test.js
💤 Files with no reviewable changes (1)
  • src/jsc/bindings/BunObject.cpp

Comment thread test/js/bun/util/inspect.test.js Outdated
@robobun

robobun commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator Author

Good catch, both children do write diagnostics to stderr in debug builds (reported module eval errors with code frames), so an undrained pipe could block them. Both tests now read stderr in the same Promise.all as stdout and exit. Pushed in ae03357.

Comment on lines +5600 to +5601
// Ignore exceptions from "Get" proxy traps and throwing lazy
// property initializers (which report the slot as not found).

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.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

@robobun

robobun commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 2:07 AM PT - Aug 9th, 2026

@robobun, your commit ae03357 is still building in Build #90817, but has 2 failures so far (All Failures):

@robobun

robobun commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator Author

On the bindings.cpp comment note: that two line comment is not justifying a workaround, it records the JSC contract that makes the ordering matter (a throwing lazy property initializer reports the slot as not found while the exception stays pending, see setUpStaticFunctionSlot in Lookup.cpp). Without it the clear looks safe to move back below the continue, which is the exact bug this PR fixes. It also follows the same one line style already used in this function for getters and proxy traps, so I left it in place.

Comment thread test/js/bun/util/inspect.test.js Outdated
@robobun

robobun commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator Author

This finding was generated against the first commit. Both tests already drain stderr in the same Promise.all since ae03357, which addressed the identical CodeRabbit comment.

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Clear pending exceptions from lazy property getters during property enumeration #37213 - Contains both changes verbatim: the same bool hasProperty hoist so CLEAR_IF_EXCEPTION runs before the not-found continue in JSC__JSValue__forEachPropertyImpl, and the same removal of the two #if BUN_DEBUG reportUncaughtExceptionAtEventLoop blocks in BunObject.cpp.
  2. Fix stale pending exceptions in console.log property iteration #37202 - Same two hunks verbatim (identical forEachPropertyImpl hoist and the same two #if BUN_DEBUG deletions), on top of additional proxy/napi fixes.
  3. Fix crashes when inspecting objects whose property enumeration throws #37175 - Same two hunks verbatim, plus an extra util.inspect LazyProperty fallback.
  4. Fix crashes when a lazy property or util.inspect fails while a value is being inspected #37160 - Same #if BUN_DEBUG deletions and a superset of the forEachPropertyImpl fix (clears the exception before continue via a new getPropertySlotForEnumeration helper).
  5. Don't leak a pending exception when a property getter throws during inspect enumeration #37107 - The identical forEachPropertyImpl hoist for the same pending-exception-on-not-found-slot bug, with its regression test in the same test/js/bun/util/inspect.test.js.
  6. Fix Bun.inspect null deref with Proxy prototypes; stop reporting from inside the sql lazy-property builders #30245 - Carries the same forEachPropertyImpl hoist and also deletes the same two #if BUN_DEBUG blocks while fixing lazy-init null derefs.

🤖 Generated with Claude Code

@robobun

robobun commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator Author

Confirmed, this is a duplicate. The property walk fix here is identical to the one in #37107 (open since Aug 7, CI green) and #37213, and the Bun.sql debug report removal is already in #37160, #37175, and #37213. Several fuzzing sessions hit the same root cause through different crash signatures.

Closing in favor of the earlier PRs. One note for whichever lands: this signature reached the walk through error message construction, Symbol++ followed by new CompressionStream(globalThis) renders the received value with the inspector and aborted the same way, so that path is worth a regression test if not already covered.

@robobun robobun closed this Aug 9, 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. My earlier inline comment about draining stderr was stale — it was posted against the first commit and ae03357 already fixed both sites.

What was reviewed

  • The CLEAR_IF_EXCEPTION reorder in the slow path now matches the fast path at bindings.cpp:5532-5536, which already clears before checking the result.
  • Removed BUN_DEBUG blocks in defaultBunSQLObject/constructBunSQLObject — verified RETURN_IF_EXCEPTION immediately after already propagates the error, and re-entering JS via reportUncaughtExceptionAtEventLoop with a pending exception is the actual violation.
  • Checked for sibling getPropertySlot-then-continue sites in forEachPropertyImpl — the fast path is already correct, no others found.
  • Both regression tests now drain stderr concurrently and use process-isolated fixtures per harness convention.
Extended reasoning...

Overview

This PR fixes a debug-build assertion failure (SIGABRT) when Bun.inspect / console.log / error-message rendering walks an object whose lazy PropertyCallback initializer throws. Three files changed: a two-line reorder in bindings.cpp (JSC__JSValue__forEachPropertyImpl slow path), removal of two 3-line #if BUN_DEBUG blocks in BunObject.cpp, and two subprocess regression tests in inspect.test.js.

Security risks

None. This is exception-scope hygiene in the inspector's property walk. No user-controlled input parsing, auth, crypto, or trust boundaries are touched. The only observable behavior change is that inspect output omits properties whose lazy initializer threw instead of leaving a pending exception (or aborting).

Level of scrutiny

Medium — forEachPropertyImpl is a hot path (every console.log/inspect/ERR_INVALID_ARG_VALUE render), so a wrong change here would be very visible. But the fix is mechanical: it hoists CLEAR_IF_EXCEPTION(scope) above the if (!found) continue, which is exactly the ordering the fast-path branch of the same function already uses (bindings.cpp:5528-5536). The PR description correctly cites the JSC contract (setUpStaticFunctionSlot reports not-found while leaving the exception pending), and the two-line code comment records that so a future refactor doesn't reintroduce the bug. The BUN_DEBUG block removal is debug-only, and calling reportUncaughtExceptionAtEventLoop (which re-enters JS via process.get) while an exception is pending is itself a JSC exception-check violation — the RETURN_IF_EXCEPTION on the next line already does the right thing.

Other factors

All prior review feedback is resolved: CodeRabbit's stderr-draining note and my own stale duplicate of it were both addressed in ae03357 (verified in the current diff — both tests now Promise.all([stdout.text(), stderr.text(), exited])). The github-actions comment-cop flagged the new code comment; the author's response is reasonable — it's a two-line contract note in the same style as the neighboring "Ignore exceptions from Proxy getPrototype trap" comments, not a workaround justification. The regression tests are process-isolated (they clobber the global Symbol), assert signalCode === null for the abort case, and follow file-local subprocess conventions.

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.

1 participant