Fix Bun.inspect null deref with Proxy prototypes; stop reporting from inside the sql lazy-property builders - #30245
Fix Bun.inspect null deref with Proxy prototypes; stop reporting from inside the sql lazy-property builders#30245robobun wants to merge 21 commits into
Conversation
When the bun:sql internal module fails to load (e.g. stack overflow), the PropertyCallback returned an empty JSValue. JSC's reifyStaticProperty passes that straight to putDirect without checking for exceptions, which dereferences a null JSCell. Return jsUndefined() on the error paths so the property is reified to a valid value while the exception still propagates. Also ensure the debug-only error reporting re-throws the exception if it was cleared while printing.
|
Found 1 issue this PR may fix:
🤖 Generated with Claude Code |
WalkthroughThis PR centralizes guarded requires/constructors for Bun lazy properties, tightens exception clearing/reporting on property and prototype access, updates lazy-property wrapper return behavior to explicit undefined fallbacks, and adds regression tests for proxy traps and lazy-property crash scenarios. ChangesBun Lazy Property Exception Safety
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
…ypes - constructBunShell (Bun.$) had the same empty-JSValue-from-PropertyCallback issue as Bun.sql, crashing in putDirect when stack overflow throws during the shell builtin call. Return jsUndefined() on the error paths. - Coalesce empty returns from the Zig-backed BunObject lazy property wrappers to jsUndefined() for the same reason. - In forEachPropertyImpl, getPrototype on a Proxy can throw (getPrototypeOf trap) and return an empty JSValue; calling getObject() on it dereferences a null JSCell. Clear the exception and stop walking the chain. Also clear exceptions from getPropertySlot before the early continue so a pending exception does not leak into the next getPrototype call. Fixes #19650
|
Updated 3:47 AM PT - Aug 15th, 2026
⏳ @robobun, your commit bafa86f is still building in
|
JSC's reifyStaticProperty always stores the PropertyCallback result via putDirect, and JSValue::get then asserts (!scope.exception() || !hasSlot) under EXCEPTION_SCOPE_VERIFICATION. Returning jsUndefined() with a pending exception satisfies putDirect but trips that assertion on the LLInt path. Instead, catch the exception, report it via reportUncaughtExceptionAtEventLoop (so the user sees why initialization failed), clear it, and return jsUndefined(). The property is reified to undefined and subsequent accesses return undefined without crashing. Also covers the case where the second access would previously crash in release builds because an empty JSValue had been cached in the slot.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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 `@src/jsc/bindings/BunObject`+exports.h:
- Around line 96-105: The wrapper macro BunObject_lazyPropCb_wrap_##name
currently clears any exception from the Zig lazy getter (called via
BunObject_lazyPropCb_##name) then returns undefined, swallowing the error and
caching undefined; modify the wrapper to report the pending exception to the JS
engine before clearing it (i.e., call the same exception reporting used in the
explicit callbacks in BunObject.cpp while the DECLARE_THROW_SCOPE(vm) scope has
an exception), then call scope.tryClearException() and return undefined only
after reporting; keep the rest of the flow (decode result, return result or
jsUndefined) intact.
In `@test/regression/issue/19650.test.ts`:
- Around line 9-30: Replace the duplicate parameterized matrices by wrapping
both related tests in a single describe.each([...]) over the same key set and
move the existing test.concurrent.each(...) bodies inside that describe block as
individual test.concurrent(...) or test(...) cases; locate the current
test.concurrent.each invocation (the one iterating ["$", "sql", "SQL",
"postgres"]) and the matching block on lines 32-51 and change them to a single
describe.each that iterates the key array, with each iteration defining the two
test cases that previously duplicated the matrix, preserving existing test
names, scopes, and use of Bun.spawn/proc.exited and assertions.
🪄 Autofix (Beta)
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: 1f9fb9d2-14aa-4cd7-9c32-695e8cdba72d
📒 Files selected for processing (3)
src/jsc/bindings/BunObject+exports.hsrc/jsc/bindings/BunObject.cpptest/regression/issue/19650.test.ts
…-lazy-init-null-deref
The previous test only checked for a null signal and exit code in {0,1}.
Under the debug/UBSan build, the unfixed code hits a null-pointer member
call, prints a UBSan diagnostic, and exits with code 1 - which the test
accepted. The test now requires the subprocess to write 'OK' to stdout
after the access pattern, which the unfixed build never reaches on any
configuration (segfault on release, UBSan abort on debug).
|
Merged Verified locally:
Previous CI run's only non-expired failures were |
|
CI status: build 56705 at ae3fc29 — all failures unrelated to this PR's code paths. Main at the same merge base (0b20408, build 56565) is green. Error-level:
Warning-level (flaky, passed on retry): This PR only modifies Regression tests ( The fuzzer has reported 120+ variants of this fingerprint against main — all verified fixed by this PR's changes. Ready for review. |
There was a problem hiding this comment.
All prior feedback is addressed and I found no new issues, but this changes exception-handling semantics for several core Bun.* lazy initializers (permanently reifying to undefined on init failure and reporting via the event loop) and overlaps with several other open PRs for #19650 — worth a human look to confirm this is the approach to land.
Extended reasoning...
Overview
This PR fixes two distinct null-JSCell dereference crashes:
BunObject.cpp/BunObject+exports.h—PropertyCallbackentries (Bun.$,Bun.sql,Bun.SQL,Bun.postgres, plus the Zig-backed wrapper macro) previously returned an emptyJSValuewhen initialization threw. JSC'sreifyStaticPropertypasses that straight toputDirect, which null-derefs. The fix catches the exception, reports it viareportUncaughtExceptionAtEventLoop, clears it, and returnsjsUndefined()so the property is reified to a valid value.bindings.cppforEachPropertyImpl— the prototype-walk inBun.inspectcalled.getObject()on the result ofgetPrototype()without checking for a Proxy-trap exception, and didn't clear an exception fromgetPropertySloton the not-found branch. Both are now guarded withCLEAR_IF_EXCEPTIONand an empty-value check.
Tests are added in inspect.test.js (in-process Proxy-trap cases) and a new test/regression/issue/19650.test.ts (subprocess crash repros for all four lazy keys × two trigger scenarios).
Security risks
None identified. This is defensive crash hardening — no new attack surface, no auth/crypto/permissions, no user-controlled data paths beyond what already existed. The change makes hostile inputs (throwing Proxy traps, clobbered globals, near-stack-overflow) less dangerous, not more.
Level of scrutiny
Moderate-to-high. While the diff is not large (~80 lines of C++), it touches JSC exception-scope semantics in core runtime initialization for high-traffic APIs. Specific judgment calls a maintainer should weigh:
- Permanently caching
undefined: after a transient init failure (e.g. stack overflow on first access),Bun.sql/Bun.$etc. becomeundefinedfor the rest of the process. That's clearly better than a segfault, but it's a user-visible behavior choice vs. e.g. leaving the property un-reified for retry. reportUncaughtExceptionAtEventLoopfrom inside a property getter: this fires the global uncaught-exception handler synchronously during a property access, which is unusual.constructBunShellno longer throwsTypeError("Internal error: ...")on the non-object branches — it now silently returnsundefined. Probably fine (those branches were unreachable in practice), but it's a behavior change beyond the crash fix.- Zig wrapper macro intentionally does not report before clearing (discussed in-thread; rationale is those getters don't run JS so only termination exceptions can land there).
Other factors
- The find-duplicate-prs bot lists eight other PRs targeting the same crash class (#29671, #29807, #27308, #27294, #28550, #28413, #27307, #28368). A maintainer should confirm this is the variant they want to merge and close/rebase the others.
- All my prior inline feedback (use
test.concurrent.each, fixconstructBunShelltoo) was applied; thenapi.cppsibling was explicitly deferred to a follow-up, which is reasonable. - CodeRabbit's suggestion to report exceptions in the Zig wrapper macro was declined with a reasoned response that CodeRabbit accepted; that thread is still technically unresolved but settled.
- CI build #53645 was triggered for the latest commit; status not visible in the timeline.
…-lazy-init-null-deref
There was a problem hiding this comment.
No issues found, but this touches JSC exception-scope handling across several core PropertyCallbacks and changes failure semantics (init exceptions are now reported-then-swallowed and the property is permanently reified to undefined), so it's worth a human sign-off on that tradeoff.
Extended reasoning...
Overview
This PR fixes two fuzzer-found null-JSCell dereferences:
BunObject.cpp/BunObject+exports.h— thePropertyCallbacklazy initializers forBun.$,Bun.sql,Bun.SQL,Bun.postgres, and the Zig-backed wrapper macro now catch exceptions thrown during initialization, report them viareportUncaughtExceptionAtEventLoop, clear them, and returnjsUndefined()instead of an emptyJSValue. Previously the empty value flowed intoreifyStaticProperty→putDirect, which dereferenced a null cell.bindings.cppforEachPropertyImpl— the prototype-chain walk now clears exceptions aftergetPropertySlot(on the not-found branch too) and aftergetPrototype, and null-guards the result before calling.getObject(), so a throwing ProxygetPrototypeOf/gettrap no longer crashesBun.inspect.
Tests: two new Bun.inspect Proxy-trap cases in inspect.test.js, and a new test/regression/issue/19650.test.ts that spawns 8 subprocesses exercising the stack-overflow and clobbered-Symbol reproducers against each lazy key and asserts the child writes OK and exits without a signal.
Security risks
None identified. The change strictly hardens error paths (turning crashes into defined behavior) and doesn't touch auth, crypto, permissions, or input parsing. Exceptions are still surfaced to the user via the uncaught-exception reporter.
Level of scrutiny
Medium-high. This is production-critical native code at the JSC↔Bun boundary — ThrowScope discipline is subtle (the PR description itself notes that returning jsUndefined() with a pending exception trips an EXCEPTION_ASSERT). The fix is well-reasoned and consistent across all four callbacks plus the macro, and the regression tests are tight (require positive OK output, verified to fail on main). But there's a real behavioral decision baked in: a failed lazy init now permanently caches undefined for that property rather than throwing on access, and constructBunShell drops its previous throwTypeError("Internal error: ...") paths in favor of silently returning undefined. That's almost certainly the right call given JSC's PropertyCallback contract, but it's the kind of semantic choice a maintainer should ratify.
Other factors
- All prior inline review comments (mine and CodeRabbit's) are resolved; the author applied the
constructBunShellandtest.concurrent.eachsuggestions and gave reasoned pushback on the two declined ones. - The duplicate-PR bot lists 8 other open/closed PRs targeting the same bug class — a human should confirm this is the variant to land.
- CI passed on the ASAN shard for the new tests; remaining failures are documented as unrelated Windows flakes.
- The
napi.cppinstance of the same pattern was intentionally left for a follow-up (acknowledged in-thread).
…-lazy-init-null-deref
- inspect.test.js: the 'throwing getter' test used an accessor on the Proxy target with an empty handler, so getPropertySlot filled the slot as a GetterSetter without invoking the getter and the test passed on pre-PR code. Replace it with a throwing Proxy 'get' trap so performGet itself throws and getPropertySlot returns false with a pending exception, exercising the CLEAR_IF_EXCEPTION reorder. - get_valkey_default_client: createNoJsNoPubsub throws a plain TypeError when REDIS_URL is empty/invalid. The C++ wrapper clears the pending exception and reifies Bun.redis to undefined, so the diagnostic was dropped. Report it via report_active_exception_as_unhandled (mirroring s3_default_client) and return UNDEFINED to satisfy to_js_host_call's 'empty iff pending exception' assertion. Add regression test.
|
Heads up: the lazy The |
|
Triage note after #19650 was closed as fixed on main. Checked against a debug + ASAN build of main (165dc9f) and the 1.4.0 canary (da3851e):
Leaving this open for part 2. #30099 and #29642 are open for the same inspect crash, so whichever of the three lands, the other two can be closed; if this one is picked it should be cut down to the bindings.cpp hunk and the inspect tests, and it should no longer claim to fix #19650. |
…-lazy-init-null-deref # Conflicts: # src/runtime/api/BunObject.rs
| // PropertyCallbacks are reified by JSC::reifyStaticProperty, which hands the | ||
| // result straight to putDirect without checking for an exception, so a getter | ||
| // that threw has its exception folded here: report it, then reify the slot as | ||
| // undefined rather than an empty JSValue. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
There was a problem hiding this comment.
Trimmed to one line in 6ecdfcd; the reasoning is in the PR description.
| // JSC's reifyStaticProperty passes the PropertyCallback result straight to | ||
| // putDirect without checking for exceptions, and JSValue::get asserts | ||
| // (!scope.exception() || !hasSlot). So these callbacks must not leave a | ||
| // pending exception; if loading the module fails we report it and return | ||
| // jsUndefined() so the property is reified to a valid value. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
There was a problem hiding this comment.
Trimmed to one line in 6ecdfcd; the reasoning is in the PR description.
…PropertyCallback The pinned WebKit's setUpStaticFunctionSlot skips the put when a PropertyCallback returns empty and reports the slot as absent, so the builders' exceptions propagate to the read and the slot stays unreified. Clearing and reporting them here turned those throws into undefined and broke builtin-esm-lazy-exports.test.ts. What is left of that part is removing the debug-only report inside the sql/SQL/postgres builders: it re-enters the error reporter in the middle of the property lookup with the exception still pending, which aborts a debug build on a Structure assertion.
There was a problem hiding this comment.
Thanks — 5414053 addresses the earlier 🔴: the lazy-init wrappers, BunObject+exports.h/BunObject.rs hunks, and 19650.test.ts are gone, and defaultBunSQLObject/constructBunSQLObject are back to plain RETURN_IF_EXCEPTION(scope, {}) so the read throws catchably via the JSC fix. The remaining forEachPropertyImpl change and inspect tests look right.
One thing still outstanding from that comment: the PR title/description still describe the removed Part 1 approach (report-as-uncaught + reify to undefined) and claim "Fixes #19650" — please update before merge so the squash message matches what actually lands.
Extended reasoning...
Overview
Since my last review (2026-08-14T15:33Z 🔴), commit 5414053 dropped the superseded Part 1 changes. The diff is now 4 files / ~85 lines: bindings.cpp reorders CLEAR_IF_EXCEPTION before the !found continue and null-guards getPrototype()'s result in forEachPropertyImpl; BunObject.cpp removes two debug-only reportUncaughtExceptionAtEventLoop calls that are now wrong (the exception propagates catchably); inspect.test.js adds two Proxy-prototype crash tests; BunObject.test.ts adds a subprocess test locking in the throw-and-stay-unreified behavior.
Security risks
None. Defensive exception clearing / null guard in an inspection path; no auth, crypto, or untrusted-input parsing.
Level of scrutiny
Medium — bindings.cpp is core JSC glue, but the change is a small defensive guard following the same CLEAR_IF_EXCEPTION pattern already used a few lines away, and robobun's 2026-08-13 triage confirms this crash is still live on main.
Other factors
- The code half of my previous 🔴 is fully addressed; the description half (drop the Part-1 prose and "Fixes #19650") is not, so the squash-merge message would be misleading.
- robobun previously noted #30099 and #29642 target the same inspect crash — a maintainer should pick one.
- One nit (undrained piped stderr in the new
BunObject.test.tssubprocess test) is posted inline; not blocking. - I checked that
proto.getObject()is safe whenprotois a non-empty non-object primitive (returnsnullptr, terminating the loop), and that the removed#if BUN_DEBUGblocks were the only remaining Part-1 residue inBunObject.cpp.
|
Title and description were rewritten for the reduced scope (no #19650 claim) shortly before the last review ran, so that part is already done; the stderr nit is fixed in 230e894. Current diff is the forEachPropertyImpl guard plus removing the debug-only report in the two sql builders, with a test for each. |
…n what makes $ throw
There was a problem hiding this comment.
LGTM — both remaining nits from the last pass are addressed in 2a20b8d.
What was reviewed:
forEachPropertyImpl(bindings.cpp): theCLEAR_IF_EXCEPTIONreorder before the!foundcontinue and the empty-proto guard match the fast-path pattern at ~5583; no other unguardedgetPrototype().getObject()sites in that function.BunObject.cpp: pure deletion of the two#if BUN_DEBUGreports;defaultBunSQLObject/constructBunSQLObjectnow propagate identically toconstructBunShell.- Tests: both inspect cases now pin exact output; the lazy-property subprocess test drains stdout/stderr/exited concurrently and asserts the combined object; the comment now attributes
$to the shell builtin correctly.
Extended reasoning...
Overview
Four files. Two native hunks: (1) src/jsc/bindings/bindings.cpp reorders a CLEAR_IF_EXCEPTION in forEachPropertyImpl's slow path so a throwing Proxy get trap is cleared before the early continue, and adds CLEAR_IF_EXCEPTION + an empty check after getPrototype() so a throwing getPrototypeOf trap doesn't null-deref through .getObject(). This mirrors the existing fast-path handling a few lines above (bindings.cpp:5574-5584). (2) src/jsc/bindings/BunObject.cpp deletes two #if BUN_DEBUG blocks that called reportUncaughtExceptionAtEventLoop from inside the sql/SQL/postgres lazy-property builders while an exception was pending — that re-entered JS mid-lookup and tripped a debug-build assertion. The builders now propagate via RETURN_IF_EXCEPTION(scope, {}) exactly as constructBunShell already does, and JSC's fixed reifyStaticProperty handles the empty result. Tests: two new Bun.inspect cases in inspect.test.js (both crash on main) and one subprocess test in BunObject.test.ts covering the debug-only abort.
Security risks
None. This is exception-handling in the inspector's property walk and removal of debug-only diagnostic code. No auth, crypto, permissions, or untrusted-input parsing paths are touched. CLEAR_IF_EXCEPTION in forEachPropertyImpl is the established policy for user-code side effects during formatting (getters, traps) and is already used for the same trap on the fast path.
Level of scrutiny
Medium — native JSC bindings, but the change is mechanically small and pattern-matched against existing code in the same function. The bindings.cpp hunk is a textbook "exception check after every call that can enter JS" fix (REVIEW.md's most-blocked category), and the BunObject.cpp hunk is pure deletion of debug-only code that made the sql builders inconsistent with the shell builder. No new control flow, no allocation/ownership changes, no API surface change.
Other factors
This PR has been through many review rounds since May. The larger "part 1" (lazy-property wrappers in BunObject+exports.h / BunObject.rs) was correctly identified as superseded by oven-sh/WebKit#282 and removed in 5414053; the current diff is the minimal remainder. All prior inline feedback (mine and comment-cop's) is addressed: stderr is now drained (230e894), the inspect tests pin exact output instead of .not.toThrow(), and the test comment correctly distinguishes the $ shell-builtin path from the bun:sql require path (2a20b8d). The bug hunting system found nothing this run. No human reviewer has open concerns on the thread.
There was a problem hiding this comment.
LGTM — the reduced scope addresses everything from the earlier rounds.
What was reviewed:
forEachPropertyImplreordering:CLEAR_IF_EXCEPTIONnow runs before the!foundcontinue, and thegetPrototyperesult is null-checked before.getObject()— matches the fast-path pattern at line ~5584.BunObject.cpp: removing the debug-onlyreportUncaughtExceptionAtEventLoopcalls leavesRETURN_IF_EXCEPTION(scope, {})in place, so the sql builders now propagate the same wayconstructBunShellalready does.- Tests: both Proxy cases pin exact output; both subprocess tests drain stdout/stderr/exited concurrently and assert the combined object; the new
inspect(Bun)walk test exercises thegetPropertySlotreordering via the non-Proxy path.
Extended reasoning...
Overview
Four files touched. Production changes are ~7 lines net: (1) bindings.cpp — reorder getPropertySlot result handling in forEachPropertyImpl so CLEAR_IF_EXCEPTION runs before the early continue, and guard getPrototype().getObject() against an empty result from a throwing Proxy getPrototypeOf trap; (2) BunObject.cpp — delete two #if BUN_DEBUG blocks that called reportUncaughtExceptionAtEventLoop from inside the sql/SQL/postgres lazy-property builders, mid-lookup with the exception still pending. Three new tests in inspect.test.js and one in BunObject.test.ts.
Security risks
None. This is a crash-hardening change in the property-enumeration path of Bun.inspect. No auth, crypto, permissions, or untrusted-input parsing is touched. The added CLEAR_IF_EXCEPTION calls follow the same "ignore trap exceptions during inspection" policy already used a few lines above (line ~5584) and at ~5668-5671, so this does not introduce new exception-swallowing surface.
Level of scrutiny
Low-to-moderate. The bindings.cpp hunk is a mechanical null-guard + exception-clear that mirrors the existing fast-path branch in the same function. The BunObject.cpp hunk deletes debug-only code with no release-build effect; the exception is still propagated by the RETURN_IF_EXCEPTION(scope, {}) immediately after, and the pinned WebKit's reifyStaticProperty now handles the empty return correctly (verified in the 2026-08-14 review round).
Other factors
This PR has been through many review rounds. My 2026-08-14 blocking comment asked for the tryClearException()+jsUndefined() wrappers to be dropped since they regressed main's now-correct catchable-throw behavior — that was done in 5414053. The stderr-drain nit (230e894), the .not.toThrow() → .toBe("{}") strengthening, and the $-vs-bun:sql comment clarification (both 2a20b8d) were all applied. The most recent commit (bafa86f) adds the inspect(Bun) walk test, which exercises the getPropertySlot reordering through the non-Proxy lazy-property path — a good complement to the two Proxy tests. All subprocess tests follow the REVIEW.md convention (Promise.all on stdout/stderr/exited, combined-object assertion). No outstanding reviewer comments remain.
Two fuzzer findings that both surface as
member call on null pointer of type 'JSC::JSCell'(JSCJSValueCell.h), plus a debug-only abort next to the second one.1.
Bun.inspectwhen a property read throws mid-walkforEachPropertyImpl(bindings.cpp) walks the prototype chain withiterating = iterating->getPrototype(globalObject).getObject();When
iteratingis a Proxy whosegetPrototypeOftrap throws,getPrototypereturns an emptyJSValueand.getObject()dereferences a null cell. ThegetPropertySlot(...) == falsebranch a few lines up has the same problem: itcontinued before clearing the exception, so whatever the read threw stayed pending for the rest of the walk. A Proxygettrap is one way to get there; since the WebKit bump a lazy property whose builder throws is another, and that one needs no Proxy: the Bun object's$builder throwing duringBun.inspect(Bun)leaves its exception pending when the next property's builder is entered (the fuzzer's version of this isnew DecompressionStream(globalThis)near the stack limit, where the argument error message formatsglobalThis).Fix: clear the exception after
getPropertySlotbefore the earlycontinue, and aftergetPrototype(same as the fast path a few lines above), and stop the walk when the result is empty.Tests: three cases in
test/js/bun/util/inspect.test.js. The two Proxy ones hit the null deref on main; the third (globalThis.Symbol = NaN; Bun.inspect(Bun)in a child) aborts a debug build of main in the next builder's exception-scope check and shows the throwing properties being skipped while the rest of the object is still printed.2. Bun object lazy properties whose builder throws
This is what the fuzzer was mostly hitting (
Bun.$/Bun.sql/Bun.SQL/Bun.postgresread near the stack limit, or after clobberingSymbol, andBun.rediswith an invalidREDIS_URL): thePropertyCallbackreturned empty with an exception pending andreifyStaticPropertyput the empty value into the slot. That is fixed in the WebKit this branch now pins:reifyStaticPropertyskips the put for an empty result andsetUpStaticFunctionSlotreports the slot as absent with the exception still pending, so the read throws and the slot stays unreified (a later read runs the builder again; #37714 pins therediscase). Earlier revisions of this PR worked around it in the callbacks by reporting and clearing the exception and reifyingundefined. With the engine fix in place that is worse than what main does (the error stops being catchable and the slot is poisoned for the rest of the process), and CI caught it inbuiltin-esm-lazy-exports.test.ts, so those changes are gone.What remains is a debug-only problem in
defaultBunSQLObject/constructBunSQLObject: underBUN_DEBUGthey reported the exception to the uncaught handler from inside the builder, i.e. in the middle of the property lookup with the exception still pending. The reporter re-enters JS, and a debug build aborts onStructure::storedPrototype'sobject->structure() == thisassertion (globalThis.Symbol = NaN; Bun.sql, or anything that formats the whole Bun object near the stack limit). Removing the report makes these builders behave likeconstructBunShellalready does: the exception propagates to the read.Test:
test/js/bun/util/BunObject.test.tsreads$,sql,SQLandpostgrestwice each withSymbolclobbered and expects aTypeErrorevery time. On a debug build of main the child aborts; it passes on release builds of main, since the report was debug-only.Issue #19650 was the engine-level bug and is already closed, so this PR does not claim it.
no test proof · iteration 17 · 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