Skip to content

Fix crash when a lazy Bun property builder throws during inspect - #37269

Closed
robobun wants to merge 2 commits into
mainfrom
farm/4cd2be49/inspect-lazy-prop-exception
Closed

robobun wants to merge 2 commits into
mainfrom
farm/4cd2be49/inspect-lazy-prop-exception

Conversation

@robobun

@robobun robobun commented Aug 9, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes a deterministic Fuzzilli crash (fingerprint e42c73f70c7e7925, debug assertion abort).

Repro

Symbol = NaN;        // any tampering that breaks internal module evaluation
Bun.inspect(Bun);    // or anything that renders Bun in an error message

The fuzzer hit this through new CompressionStream(globalThis): the invalid format error message inspects the received value, the walk reaches the Bun object, and materializing the lazy $ property evaluates shell.ts, which throws Symbol is not a function at module scope.

What was wrong

Two pending-exception bugs, both only observable as aborts in debug/ASAN builds:

  1. JSC__JSValue__forEachPropertyImpl cleared getter exceptions only when getPropertySlot reported the slot as found. A throwing lazy static property builder makes getPropertySlot return false with the exception left pending, so the continue skipped the clear and the walk entered the next lazy builder (Bun.Archive) with a pending exception, failing releaseAssertNoException. JSC__JSValue__forEachPropertyOrdered already clears before its continue; this applies the same order to the unordered walk.

  2. The Bun.sql / Bun.SQL builders called reportUncaughtExceptionAtEventLoop (debug builds only) while the module-load exception was still pending. That runs the uncaughtException machinery, and its process._fatalException lookup reifies static properties with a pending exception, which makes the lookup loop see a stale Structure* and fail the object->structure() == this assertion in Structure::storedPrototype. Reachable with just Symbol = NaN; Bun.sql. The report is gone; RETURN_IF_EXCEPTION propagates the real module error to whoever touched the property (PR Fix null pointer dereference in Bun.sql when module fails to load #27308 fixed an earlier version of this but was closed after the repo restructure made it unmergeable).

After the fix

  • Bun.inspect(Bun) completes, skipping properties whose builder threw.
  • Bun.sql / Bun.SQL throw the underlying module error instead of aborting.
  • The original fuzzer script exits with a regular uncaught TypeError.

Testing

Added a regression test in test/js/bun/util/inspect.test.js that spawns a child which clobbers Symbol, inspects Bun, and touches Bun.sql / Bun.SQL. It fails on an unfixed debug build (child dies with SIGABRT) and passes with this change. Release builds compile these assertions out, so the crash does not reproduce under USE_SYSTEM_BUN=1; the fail-before was verified against an unfixed debug binary.

Pre-existing local failures in inspect-error.test.js (extra at require stack frame) reproduce identically on an unfixed binary and are unrelated.


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

…azy builders

JSC__JSValue__forEachPropertyImpl skipped its exception clear when
getPropertySlot reported the property as not found, which happens when a
lazy static property builder throws (for example Bun.$ evaluating
shell.ts after the global Symbol was clobbered). The walk then entered
the next lazy builder with that exception still pending and failed the
exception scope assertion in debug builds. Clear the exception before
the continue, matching JSC__JSValue__forEachPropertyOrdered.

The Bun.sql and Bun.SQL builders also called
reportUncaughtExceptionAtEventLoop in debug builds while the module load
exception was still pending, running the uncaughtException machinery
(process._fatalException lookup) with a pending exception and tripping a
stale structure assertion. Drop the report and let RETURN_IF_EXCEPTION
propagate the error to whoever touched the property.
@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

Walkthrough

The change removes debug exception reporting from Bun SQL lazy helpers, makes property enumeration skip failed lookups after clearing exceptions, and adds a regression test for Bun.inspect(Bun) and subsequent SQL property access.

Changes

Lazy property exception handling

Layer / File(s) Summary
SQL lazy helper cleanup
src/jsc/bindings/BunObject.cpp
The Bun SQL and SQL constructor helpers no longer report pending exceptions through BUN_DEBUG.
Enumeration and inspection regression coverage
src/jsc/bindings/bindings.cpp, test/js/bun/util/inspect.test.js
Property enumeration now skips properties whose lookup fails. The regression test verifies inspection succeeds while Bun.sql and Bun.SQL still throw TypeError.

Possibly related PRs

  • oven-sh/bun#37107: Updates the same property enumeration path and adds related Bun.inspect(Bun) coverage.
  • oven-sh/bun#37175: Overlaps in lazy-property exception handling, enumeration logic, and regression tests.
  • oven-sh/bun#37256: Matches the same SQL lazy helper, enumeration, and regression-test changes.

Suggested reviewers: dylan-conway, jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary fix for crashes caused by throwing lazy Bun property builders during inspection.
Description check ✅ Passed The description explains the cause, implementation, expected behavior, reproduction, and verification details, although it does not use the template headings.
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
Contributor

Choose a reason for hiding this comment

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

LGTM — small, well-scoped exception-handling fix that mirrors the existing forEachPropertyOrdered pattern. The one inline nit (undrained stderr pipe in the test) is minor and doesn't block.

Extended reasoning...

Overview

Three changes: (1) bindings.cpp reorders CLEAR_IF_EXCEPTION in JSC__JSValue__forEachPropertyImpl so it runs before the continue when getPropertySlot returns false — the ordered sibling JSC__JSValue__forEachPropertyOrdered at line 5764-5766 already does exactly this. (2) BunObject.cpp deletes six lines of #if BUN_DEBUG-only reportUncaughtExceptionAtEventLoop calls in the Bun.sql/Bun.SQL lazy builders that ran with a pending exception; the RETURN_IF_EXCEPTION immediately following already propagates correctly. (3) A subprocess regression test in inspect.test.js.

Security risks

None. The change only affects how pending exceptions are cleared/propagated during property enumeration for inspect. No new user-controlled input paths, no auth/crypto/permissions.

Level of scrutiny

Native JSC bindings warrant care, but both native edits are minimal and mechanical: one is a pure reorder to match its sibling function's already-correct pattern (strictly widening when the exception clear fires — no case that previously cleared now skips it), and the other deletes debug-only code that was actively violating the "never enter JSC with a pending exception" invariant. Release-build behavior of BunObject.cpp is byte-identical. The PR description traces the exact assertion failures and references the prior PR #27308 that attempted the same Bun.sql fix.

Other factors

The bug hunter flagged one test-hygiene nit: the new test pipes stderr but only drains stdout in the Promise.all. That's worth fixing to match the neighboring test, but it's not a correctness blocker — the passing child writes nothing to stderr, and on regression the child is SIGABRT'd. I also confirmed forEachPropertyOrdered uses the identical hasProperty = ...; clear; if (!hasProperty) continue; sequence, so this is applying an established fix to its twin. No prior human reviews or outstanding comments on the PR.

Comment thread test/js/bun/util/inspect.test.js Outdated
Comment on lines +5600 to +5601
// Ignore exceptions from "Get" proxy traps and lazy static
// property builders, including when the slot was 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

@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 958-961: Update the subprocess result handling around the
Promise.all call to retain stderr instead of discarding it, then assert the
fixture’s stable stderr contract alongside stdout and exitCode. Avoid asserting
stderr is universally empty when debug or sanitizer diagnostics may be
permitted.
🪄 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: 0ef0bbc2-ef84-40d3-a6ff-83ae3a589455

📥 Commits

Reviewing files that changed from the base of the PR and between d2f48c6 and b54e964.

📒 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 on lines +958 to +961
const [stdout, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(stdout).toBe(["inspect: true", "sql: TypeError", "SQL: TypeError", ""].join("\n"));
expect(exitCode).toBe(0);

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the subprocess stderr contract.

Line 958 drains stderr but discards it. A lazy-builder debug report or another child diagnostic can appear while stdout and exitCode still match, so the regression test can pass after a regression. Retain stderr and assert the stable contract for this fixture together with stdout and exitCode. Do not use a blanket empty-string assertion if debug or sanitizer diagnostics are allowed.

🤖 Prompt for 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.

In `@test/js/bun/util/inspect.test.js` around lines 958 - 961, Update the
subprocess result handling around the Promise.all call to retain stderr instead
of discarding it, then assert the fixture’s stable stderr contract alongside
stdout and exitCode. Avoid asserting stderr is universally empty when debug or
sanitizer diagnostics may be permitted.

Sources: Coding guidelines, Learnings

@robobun

robobun commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator Author

Closing this as a duplicate. #37256 (opened earlier) carries the same two native changes verbatim: the clear-before-continue reorder in JSC__JSValue__forEachPropertyImpl and the removal of the debug-only reportUncaughtExceptionAtEventLoop calls in the Bun.sql / Bun.SQL builders. The bindings.cpp hunk is also part of #37107 and #37175.

The Windows lane failures on this PR's test (empty child stdout on windows 2019 x64 and windows 11 aarch64 in build 90968) are a problem with my test fixture, not the fix: on Windows, process.env is built by a JS builtin and node:util loads lazily inside the formatter, so clobbering Symbol before the first Bun.inspect call breaks the child for reasons unrelated to the property walk. #37256's test warms those dependencies first and passes on all lanes, so it supersedes this one.

On the review notes: the stderr drain was added in b54e964, and I'll skip the suggested stderr content assertion since the repo's test guidance rules out asserting on crash-report text; with this closed as a duplicate it's moot anyway.

@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 — the stderr-drain nit from the earlier pass is addressed, and this run found nothing further.

What was reviewed:

  • forEachPropertyImpl now clears the exception before continue, matching forEachPropertyOrdered at bindings.cpp:5763-5768 exactly.
  • The removed #if BUN_DEBUG blocks in defaultBunSQLObject/constructBunSQLObject were the only debug-only hooks between requireId and RETURN_IF_EXCEPTION; the propagation path is unchanged for release.
  • The comment-cop flag on bindings.cpp:5601 looks like a false positive — it's a one-sentence note naming what CLEAR_IF_EXCEPTION covers, not a workaround justification.
Extended reasoning...

Overview

Three files: src/jsc/bindings/bindings.cpp reorders CLEAR_IF_EXCEPTION to run before the if (!hasProperty) continue; in JSC__JSValue__forEachPropertyImpl, so a lazy static property builder that throws (and makes getPropertySlot return false with a pending exception) no longer carries that exception into the next iteration. src/jsc/bindings/BunObject.cpp deletes two #if BUN_DEBUG blocks that called reportUncaughtExceptionAtEventLoop while an exception was still pending — the immediately-following RETURN_IF_EXCEPTION already propagates correctly. test/js/bun/util/inspect.test.js adds a spawned-child regression test.

Security risks

None. This is exception-handling ordering inside the inspect property walk and removal of debug-only diagnostic code. No new inputs are parsed, no permissions/auth touched.

Level of scrutiny

Moderate — JSC exception discipline is sensitive, but the change is ~10 production lines. The bindings.cpp reorder brings the unordered walk into exact parity with its sibling JSC__JSValue__forEachPropertyOrdered (bindings.cpp:5763-5768), which already stores hasProperty, clears, then checks. The BunObject.cpp change is pure deletion of debug-gated code that violated the "never enter JS with a pending exception" invariant; release behavior is byte-identical.

Other factors

The prior review's stderr-drain nit was addressed in commit b54e964 — the test now reads proc.stderr.text() in the same Promise.all. The github-actions comment-cop flag on the two-line comment at bindings.cpp:5601 appears to be a heuristic false positive: the comment is a single wrapped sentence naming which exceptions are cleared (proxy traps and lazy builders), extending the pre-existing one-liner rather than justifying a workaround. The test asserts exact stdout, checks exit code last, isolates the Symbol = NaN global clobber in a subprocess, and drains both pipes. The bug-hunting system found no issues this run.

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