Bun.inspect: don't leave an exception pending when a property lookup fails - #38610
Bun.inspect: don't leave an exception pending when a property lookup fails#38610robobun wants to merge 1 commit into
Conversation
…ing on JSC__JSValue__forEachPropertyImpl skipped a property as soon as getPropertySlot returned false, but a lazy static property initializer or a Proxy get trap can return false with an exception still pending. That exception then stayed set while the next property was reified (dropping it from the output, and tripping the exception scope assertion in debug builds) and made the Proxy getPrototypeOf path return an empty JSValue that was dereferenced with getObject(). Clear the exception right after the lookup regardless of its result, and stop walking the prototype chain when getPrototype returns empty.
|
Warning Review limit reached
Next review available in: 1 minute Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
Comment |
|
@coderabbitai review |
Action performedReview triggered.
|
|
Caution CodeRabbit couldn't update its existing comment. Error details |
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
There was a problem hiding this comment.
LGTM — focused crash fix that reorders an existing CLEAR_IF_EXCEPTION and adds an empty-value guard.
What was reviewed:
- Confirmed the
getPropertySlotreordering matches whatforEachPropertyOrderedalready does (bindings.cpp:5788-5792), and thegetPrototypeempty-check mirrors the fast-path at line 5574. - Checked that swallowing these exceptions is consistent with the function's existing tail-end
tryClearException, so non-crashing output is unchanged. - Tests follow harness conventions (subprocess for the
globalThis.processclobber, combined stdout/stderr/exitCode assertion, exact-value expects).
Extended reasoning...
Overview
Two small hunks in JSC__JSValue__forEachPropertyImpl (src/jsc/bindings/bindings.cpp), the property walk behind Bun.inspect/console.log:
- Move the existing
CLEAR_IF_EXCEPTION(scope)to run before thecontinuewhengetPropertySlotreturns false, so a Proxygettrap or lazy-property initializer that throws doesn't leave an exception pending into the next iteration. - Check
iterating->getPrototype(globalObject)for an emptyJSValue(and clear the exception) before calling.getObject()on it, so a throwinggetPrototypeOftrap stops the walk instead of segfaulting.
Plus three tests in test/js/bun/util/inspect.test.js covering the Proxy-get-throws, Proxy-getPrototypeOf-throws, and lazy-Bun-property-throws cases.
Security risks
None. This is formatter/inspect code; the change is purely defensive (clear a pending exception, null-check an empty JSValue). No new user-controlled inputs reach a syscall or allocation.
Level of scrutiny
Low-to-medium. It's C++ in the JSC bindings, so exception-scope discipline matters, but the change is a two-line reorder plus a null guard — no new control flow, no allocation, no ownership transfer. Both hunks bring the slow path in line with what the sibling code already does: forEachPropertyOrdered clears the exception before checking hasProperty (bindings.cpp:5788-5792), and the fast-path prototype walk already guards if (JSValue proto = ...) with a trailing CLEAR_IF_EXCEPTION (bindings.cpp:5574-5584). The function already swallows exceptions at its tail (bindings.cpp:5706-5709), so the "swallow" semantics for these lookups are unchanged — only the "leave pending across the next JSC call" bug is removed.
Other factors
- The PR description explains the mechanism precisely (stale exception →
setUpStaticFunctionSlotreports missing /getPrototypereturns empty →.getObject()on tag-only JSValue segfaults), and the tests are the actual repros: two crash on the current release build, one dropsArchivefrom the output / trips the debug assertion. - Tests follow REVIEW.md conventions: added to the existing
inspect.test.js, subprocess-isolated where they mutate process globals, pipes drained concurrently withPromise.all, combined{stdout, stderr, exitCode}assertion, exacttoBeon inspect output. - The lazy-property test's reliance on
Bun.$readingprocessandArchivebeing adjacent is documented in the comment above it; if the Bun object's property set changes later the test may need a tweak, but that's an acceptable coupling for a regression test of this specific assertion. - No outstanding reviewer comments; only a CodeRabbit rate-limit notice on the timeline.
|
Updated 10:42 AM PT - Aug 14th, 2026
⏳ @robobun, your commit ad64014 is still building in
|
|
Closing as a duplicate. #37428 already has the same two For the record, the Windows failure on this PR's CI run (build 96315) was not caused by the fix. Setting globalThis.process = undefined;
Bun.inspect({ [Bun.inspect.custom]() { return 1; } });That initializer is already being fixed in #37202, so nothing further is needed from this PR. |
What does this PR do?
JSC__JSValue__forEachPropertyImpl(the property walk behindBun.inspect/console.log) skipped a property as soon asgetPropertySlotreturned false. Two lookups can return false with an exception still pending: a lazily built static property (PropertyCallback) whose initializer throws, and a Proxygettrap that throws. Thecontinueran before the existingCLEAR_IF_EXCEPTION, so the exception stayed set while the next property was looked up.What that did:
Archive) was built with the stale exception pending. In release buildssetUpStaticFunctionSlotthen reported it as missing and it vanished from the output; in debug builds the exception scope assertion fired (releaseAssertNoException, the crash this PR started from, found by fuzzing: a script that brokeBun.$'s initializer and then calledBun.inspect(Bun)).iterating->getPrototype(globalObject)bailed out on the pending exception and returned an emptyJSValue, and.getObject()on it segfaulted (Segmentation fault at address 0x5on the current canary).The same
getPrototypeline also crashed on its own whenever a ProxygetPrototypeOftrap throws, since the empty return value was never checked.The fix is the two hunks in
bindings.cpp: clear the exception right aftergetPropertySlotregardless of its result (this is whatforEachPropertyOrderedalready does), and stop the prototype walk whengetPrototypereturns empty instead of dereferencing it. Exceptions from these lookups were already being swallowed at the end of the function, so the output for the non-crashing cases is unchanged.Repros that crash the current release build and pass with this change:
How did you verify your code works?
Added three tests to
test/js/bun/util/inspect.test.js. On the unfixed build the two Proxy tests segfault (release) or trip UBSan (debug), and the lazy property test fails withArchivemissing from the output (release) or the exception scope assertion in the child process (debug). All three pass withbun bd test test/js/bun/util/inspect.test.js, along with the rest of that file and the console tests.