Skip to content

Fix Bun.inspect null deref with Proxy prototypes; stop reporting from inside the sql lazy-property builders - #30245

Open
robobun wants to merge 21 commits into
mainfrom
farm/8928bc04/fix-sql-lazy-init-null-deref
Open

Fix Bun.inspect null deref with Proxy prototypes; stop reporting from inside the sql lazy-property builders#30245
robobun wants to merge 21 commits into
mainfrom
farm/8928bc04/fix-sql-lazy-init-null-deref

Conversation

@robobun

@robobun robobun commented May 4, 2026

Copy link
Copy Markdown
Collaborator

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.inspect when a property read throws mid-walk

forEachPropertyImpl (bindings.cpp) walks the prototype chain with

iterating = iterating->getPrototype(globalObject).getObject();

When iterating is a Proxy whose getPrototypeOf trap throws, getPrototype returns an empty JSValue and .getObject() dereferences a null cell. The getPropertySlot(...) == false branch a few lines up has the same problem: it continued before clearing the exception, so whatever the read threw stayed pending for the rest of the walk. A Proxy get trap 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 during Bun.inspect(Bun) leaves its exception pending when the next property's builder is entered (the fuzzer's version of this is new DecompressionStream(globalThis) near the stack limit, where the argument error message formats globalThis).

Fix: clear the exception after getPropertySlot before the early continue, and after getPrototype (same as the fast path a few lines above), and stop the walk when the result is empty.

const o = {};
Object.setPrototypeOf(o, new Proxy({}, { getPrototypeOf() { throw 0; } }));
Bun.inspect(o);

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.postgres read near the stack limit, or after clobbering Symbol, and Bun.redis with an invalid REDIS_URL): the PropertyCallback returned empty with an exception pending and reifyStaticProperty put the empty value into the slot. That is fixed in the WebKit this branch now pins: reifyStaticProperty skips the put for an empty result and setUpStaticFunctionSlot reports 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 the redis case). Earlier revisions of this PR worked around it in the callbacks by reporting and clearing the exception and reifying undefined. 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 in builtin-esm-lazy-exports.test.ts, so those changes are gone.

What remains is a debug-only problem in defaultBunSQLObject / constructBunSQLObject: under BUN_DEBUG they 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 on Structure::storedPrototype's object->structure() == this assertion (globalThis.Symbol = NaN; Bun.sql, or anything that formats the whole Bun object near the stack limit). Removing the report makes these builders behave like constructBunShell already does: the exception propagates to the read.

Test: test/js/bun/util/BunObject.test.ts reads $, sql, SQL and postgres twice each with Symbol clobbered and expects a TypeError every 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

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.
@github-actions github-actions Bot added the claude label May 4, 2026
@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. Seg fault at address 0x5 #19650 - Segfault at near-null address when a lazy property callback (e.g. Bun.sql, Bun.$) throws during reifyStaticProperties due to stack overflow — exactly the null JSCell dereference this PR guards against

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #19650

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This 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.

Changes

Bun Lazy Property Exception Safety

Layer / File(s) Summary
Require helper / control flow
src/jsc/bindings/BunObject.cpp
Adds requireBunSqlModule(VM&, Zig::GlobalObject*, ThrowScope&) and refactors defaultBunSQLObject / constructBunSQLObject to use it; clear/report pending exceptions and return jsUndefined() when require/lookup fails or returns non-object.
Shell construction handling
src/jsc/bindings/BunObject.cpp
Reworks constructBunShell to use a reportAndClear lambda, clear/report exceptions after factory call and ShellError access, and return jsUndefined() on invalid returns or exception paths.
Lazy-property wrapper
src/jsc/bindings/BunObject+exports.h
Updates BunObject_lazyPropCb_wrap_<name> implementation to establish a throw scope, decode Zig callback to a local result, clear/report exceptions, and return result when truthy or jsUndefined() otherwise.
Property iteration / prototype walk
src/jsc/bindings/bindings.cpp
In JSC__JSValue__forEachPropertyImpl, store getPropertySlot(...) result in found, clear exceptions immediately after slot lookup and skip when not found; read prototype into a temporary proto, clear prototype-trap exceptions, and update iterating only when proto is non-null.
Tests
test/js/bun/util/inspect.test.js, test/regression/issue/19650.test.ts
Adds two Bun.inspect tests for proxy prototypes that throw from getPrototypeOf or getters; adds regression suites that spawn subprocesses accessing lazy Bun keys ("$", "sql", "SQL", "postgres") during crash-prone scenarios to assert no signal termination and exit code 0 or 1.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR directly addresses issue #19650 by preventing null JSCell dereferences in lazy Bun property callbacks and prototype chain traversal, with added regression tests.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the two identified crash bugs: BunObject.cpp, BunObject+exports.h, bindings.cpp modifications, and corresponding regression tests.
Title check ✅ Passed The title clearly identifies both primary fixes: Bun.inspect Proxy prototype null dereferences and exception handling in SQL lazy-property builders.
Description check ✅ Passed The description provides detailed change rationale, affected cases, reproducers, and tests, but it does not use the template headings explicitly.

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Fix null JSCell deref in Bun lazy property callbacks and forEachProperty #29671 - Fixes the same null JSCell deref in defaultBunSQLObject/constructBunSQLObject plus other lazy property callbacks and forEachProperty
  2. Fix null deref in Bun object lazy PropertyCallbacks on exception #29807 - Fixes the same null deref in Bun object lazy PropertyCallbacks on exception, including the sql callbacks
  3. Fix null pointer dereference in Bun.sql when module fails to load #27308 - Fixes the same null pointer dereference in Bun.sql when module fails to load
  4. Fix null pointer crash in Bun.$ lazy property callback #27294 - Fixes null pointer crash in Bun.$ lazy property callback, also patches the sql callbacks with jsUndefined() returns
  5. Fix null pointer crash in lazy property reification during deepEquals #28550 - Fixes null pointer crash in lazy property reification, including the same sql callback fixes
  6. Fix null pointer deref in Bun.$ lazy init during stack overflow #28413 - Fixes null pointer deref in lazy init during stack overflow, including the same sql callback fixes
  7. fix(bindings): prevent null cell crash in PropertyCallback handlers on exception #27307 - Prevents null cell crash in PropertyCallback handlers on exception, including the sql callbacks
  8. Fix null pointer crash in reifyStaticProperty for BunObject PropertyCallbacks #28368 - Wraps PropertyCallbacks including the sql callbacks in null-safe wrappers via reifyStaticProperty

🤖 Generated with Claude Code

Comment thread test/js/sql/sql-lazy-init-stack-overflow.test.ts Outdated
Comment thread src/jsc/bindings/BunObject.cpp Outdated
…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
@robobun robobun changed the title Fix null deref in Bun.sql/Bun.SQL lazy property callbacks Fix null JSCell derefs in Bun object lazy init and Bun.inspect with Proxy prototypes May 4, 2026
@robobun

robobun commented May 4, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 3:47 AM PT - Aug 15th, 2026

@robobun, your commit bafa86f is still building in Build #97903, but has 1 failures so far (All Failures):

Comment thread src/jsc/bindings/bindings.cpp
robobun added 2 commits May 4, 2026 18:02
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.

@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: 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

📥 Commits

Reviewing files that changed from the base of the PR and between da65188 and aad7f3e.

📒 Files selected for processing (3)
  • src/jsc/bindings/BunObject+exports.h
  • src/jsc/bindings/BunObject.cpp
  • test/regression/issue/19650.test.ts

Comment thread src/jsc/bindings/BunObject+exports.h Outdated
Comment thread test/regression/issue/19650.test.ts Outdated
robobun added 2 commits May 12, 2026 07:40
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).
@robobun

robobun commented May 12, 2026

Copy link
Copy Markdown
Collaborator Author

Merged main (23 commits behind) and tightened the regression test: it now requires the subprocess to write OK to stdout after the access pattern completes. Previously the test only checked signalCode === null && exitCode ∈ {0,1} — under the debug/UBSan build, the unfixed code exits with code 1 after the null-pointer diagnostic, so the old test passed without the fix.

Verified locally:

  • origin/main src + bun bd test 19650 → 8/8 fail (stdout empty)
  • PR src + bun bd test 19650 → 8/8 pass
  • USE_SYSTEM_BUN=1 bun test 19650 → 8/8 fail (SIGILL)

Previous CI run's only non-expired failures were node-http-backpressure-max, test-http-should-emit-close-when-connection-is-aborted, and hot.test.ts — none touch BunObject.cpp / bindings.cpp.

@robobun

robobun commented May 12, 2026

Copy link
Copy Markdown
Collaborator Author

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:

  • x64-musl-baseline build-rust — rustup toolchain download timed out (static.rust-lang.org … tcp connect error: Operation timed out). Infra flake.
  • test/js/node/test/parallel/test-stream-readable-setEncoding-existing-buffers.js on Windows 11 aarch64 — Illegal instruction at 0x7FF7E42D878C with ntdll.dll frames in the trace. Pure node:stream Readable.setEncoding test; passes locally on Linux x64 and touches none of the files this PR changes.

Warning-level (flaky, passed on retry): jsc-stress.test.ts Wasm mprotect failed: 487 on Windows (appeared on every build of this PR), 30205.test.ts napi addon on x64-asan.

This PR only modifies BunObject.cpp/BunObject+exports.h (Bun.* lazy PropertyCallbacks), bindings.cpp (forEachPropertyImpl), BunObject.rs (get_valkey_default_client), inspect.test.js, and 19650.test.ts. None of those intersect with rustup, node:stream, JSC Wasm JIT, or napi cleanup hooks.

Regression tests (19650.test.ts 9/9, inspect.test.js 74/74) pass on all shards where the build completed.

The fuzzer has reported 120+ variants of this fingerprint against main — all verified fixed by this PR's changes. Ready for review.

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

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:

  1. BunObject.cpp / BunObject+exports.hPropertyCallback entries (Bun.$, Bun.sql, Bun.SQL, Bun.postgres, plus the Zig-backed wrapper macro) previously returned an empty JSValue when initialization threw. JSC's reifyStaticProperty passes that straight to putDirect, which null-derefs. The fix catches the exception, reports it via reportUncaughtExceptionAtEventLoop, clears it, and returns jsUndefined() so the property is reified to a valid value.
  2. bindings.cpp forEachPropertyImpl — the prototype-walk in Bun.inspect called .getObject() on the result of getPrototype() without checking for a Proxy-trap exception, and didn't clear an exception from getPropertySlot on the not-found branch. Both are now guarded with CLEAR_IF_EXCEPTION and 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. become undefined for 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.
  • reportUncaughtExceptionAtEventLoop from inside a property getter: this fires the global uncaught-exception handler synchronously during a property access, which is unusual.
  • constructBunShell no longer throws TypeError("Internal error: ...") on the non-object branches — it now silently returns undefined. 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, fix constructBunShell too) was applied; the napi.cpp sibling 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.

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

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:

  1. BunObject.cpp / BunObject+exports.h — the PropertyCallback lazy initializers for Bun.$, Bun.sql, Bun.SQL, Bun.postgres, and the Zig-backed wrapper macro now catch exceptions thrown during initialization, report them via reportUncaughtExceptionAtEventLoop, clear them, and return jsUndefined() instead of an empty JSValue. Previously the empty value flowed into reifyStaticPropertyputDirect, which dereferenced a null cell.
  2. bindings.cpp forEachPropertyImpl — the prototype-chain walk now clears exceptions after getPropertySlot (on the not-found branch too) and after getPrototype, and null-guards the result before calling .getObject(), so a throwing Proxy getPrototypeOf/get trap no longer crashes Bun.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 constructBunShell and test.concurrent.each suggestions 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.cpp instance of the same pattern was intentionally left for a follow-up (acknowledged in-thread).

Comment thread test/js/bun/util/inspect.test.js Outdated
Comment thread src/jsc/bindings/BunObject+exports.h Outdated
- 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.
@robobun

robobun commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator Author

Heads up: the lazy PropertyCallback half of this PR is now superseded by #33211, which is based on current main and follows the convention #31826 later established in BunProcess.cpp for the same problem on the process.* lazy getters (the pending exception is reported as uncaught rather than cleared silently). The hunks here in BunObject+exports.h, BunObject.cpp, and BunObject.rs will conflict with it.

The forEachPropertyImpl fix in bindings.cpp is separate and still needed. That null deref (iterating->getPrototype(globalObject).getObject() with a throwing Proxy getPrototypeOf trap) is still present on main, currently at bindings.cpp:5519. This PR should be narrowed to just that part once #33211 lands.

Comment thread test/regression/issue/19650.test.ts Outdated
@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

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.

Comment thread src/jsc/bindings/BunObject+exports.h Outdated
Comment on lines +96 to +99
// 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.

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Trimmed to one line in 6ecdfcd; the reasoning is in the PR description.

Comment thread src/jsc/bindings/BunObject.cpp Outdated
Comment on lines +318 to +322
// 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.

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Trimmed to one line in 6ecdfcd; the reasoning is in the PR description.

Comment thread src/jsc/bindings/BunObject.cpp Outdated
…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.
@robobun robobun changed the title Fix null JSCell derefs in Bun object lazy init and Bun.inspect with Proxy prototypes Fix Bun.inspect null deref with Proxy prototypes; stop reporting from inside the sql lazy-property builders Aug 14, 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.

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.ts subprocess test) is posted inline; not blocking.
  • I checked that proto.getObject() is safe when proto is a non-empty non-object primitive (returns nullptr, terminating the loop), and that the removed #if BUN_DEBUG blocks were the only remaining Part-1 residue in BunObject.cpp.

Comment thread test/js/bun/util/BunObject.test.ts Outdated
@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

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.

Comment thread test/js/bun/util/BunObject.test.ts Outdated
Comment thread test/js/bun/util/inspect.test.js Outdated

@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 — both remaining nits from the last pass are addressed in 2a20b8d.

What was reviewed:

  • forEachPropertyImpl (bindings.cpp): the CLEAR_IF_EXCEPTION reorder before the !found continue and the empty-proto guard match the fast-path pattern at ~5583; no other unguarded getPrototype().getObject() sites in that function.
  • BunObject.cpp: pure deletion of the two #if BUN_DEBUG reports; defaultBunSQLObject/constructBunSQLObject now propagate identically to constructBunShell.
  • 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.

@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 reduced scope addresses everything from the earlier rounds.

What was reviewed:

  • forEachPropertyImpl reordering: CLEAR_IF_EXCEPTION now runs before the !found continue, and the getPrototype result is null-checked before .getObject() — matches the fast-path pattern at line ~5584.
  • BunObject.cpp: removing the debug-only reportUncaughtExceptionAtEventLoop calls leaves RETURN_IF_EXCEPTION(scope, {}) in place, so the sql builders now propagate the same way constructBunShell already 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 the getPropertySlot reordering 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.

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