Fix crash when a property lookup throws while an object is being formatted - #37428
Fix crash when a property lookup throws while an object is being formatted#37428robobun wants to merge 2 commits into
Conversation
JSC__JSValue__forEachPropertyImpl left the exception pending when getPropertySlot returned false because a lazy property initializer or a Proxy trap threw. Every later lookup on the object then failed because of the stale exception, and walking to the next prototype called getObject() on the empty value returned by a throwing getPrototype, a null dereference. The same null dereference was reachable directly through a Proxy whose getPrototypeOf trap throws. Also drop the debug-only reporting of Bun.sql module load failures, which reported the exception while it was still pending on the VM and tripped a Structure assertion in debug builds.
WalkthroughChangesInspection exception safety
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
|
The automated review above was rate limited and did not look at the diff, so nothing to address from it. CI for this branch is still running; I will follow up if anything in it fails. |
|
On the napi_get_all_property_names site (napi.cpp:2072): agreed that it is the same null dereference, plus the unchecked getOwnPropertyDescriptor throw right before it. It is being fixed separately, since it needs its own addon-based test under test/napi and this PR does not otherwise touch N-API. I have noted it in the description so the exclusion is on record. |
Replacing the whole Symbol global also broke node:util, which Windows
needs to format process.env, so the subprocess died for an unrelated
reason there. Intercept just the Symbol("cwd") call the shell builtin
makes instead.
|
Updated 11:38 PM PT - Aug 10th, 2026
✅ @robobun, your commit f548930f4e570decb90c02e5b5caab42e7d1ee3c passed in 🧪 To try this PR locally: bunx bun-pr 37428That installs a local version of the PR into your bun-37428 --bun |
|
Nothing to act on from the automated summaries. State of the PR: the only review finding (the napi site) is tracked separately as noted above. The second commit only changes the test fixture, which previously replaced the Symbol global outright and failed on the Windows lanes for an unrelated reason (details in the description); the fix itself is unchanged. CI is running again on that commit. |
What
Fixes a crash in the object property walk used by
Bun.inspect,console.logand theexpect()failure messages (JSC__JSValue__forEachPropertyImplinsrc/jsc/bindings/bindings.cpp), found by the fuzzer. The crash needs a property lookup that throws while the object is being formatted.Release builds segfault (
Segmentation fault at address 0x5) on either of these:Debug builds additionally abort with
releaseAssertNoExceptionon the fuzzer's case, which is a lazy property on theBunobject throwing whileBunis formatted (the shell builtin behindBun.$calls the globalSymbol, which a previous REPRL script had replaced). In release builds that case silently drops every property ofBunthat comes after the throwing one.Root cause
In the slow path of the walk, a
getPropertySlotthat returned false was skipped withcontinuebefore theCLEAR_IF_EXCEPTIONthat follows it. JSC reports a throwing Proxy trap or a throwing static lazy property initializer exactly that way (returns false, exception pending), so the exception stayed on the VM:getPrototype()bails out and returns the emptyJSValue, and.getObject()on it dereferences null (an emptyJSValuepassesisCell(), andJSCell::getObjectreads the type byte at offset 5). A Proxy whosegetPrototypeOftrap throws reaches this line directly, without a stale exception.Fix
bindings.cpp: clear the exception aftergetPropertySlotregardless of its result (this is what the ordered variantJSC__JSValue__forEachPropertyOrderedalready does), and check the value returned bygetPrototypebefore callinggetObject()on it, ending the walk if the trap threw.Not in this PR:
napi_get_all_property_names(src/jsc/bindings/napi.cpp, the descriptor filter loop) has the samegetPrototype().getObject()chain and also ignores a throwinggetOwnPropertyDescriptorright before it. It is only reachable from a native addon and needs its own addon-based test, so it is being fixed separately.BunObject.cpp:defaultBunSQLObject/constructBunSQLObjecthad a debug-only block that passed a load failure of the SQL module toreportUncaughtExceptionAtEventLoopwhile the exception was still pending on the VM. Every other caller clears the exception first. The report runsprocess.get("_fatalException"), which reifies a static property onprocesswith the exception pending and trips theStructure::storedPrototypeassertion, soglobalThis.Symbol = NaN; Bun.sqlaborted debug builds, and so did the fuzzer's case once the walk got pastBun.$. The exception is propagated to the caller right below this block anyway (Bun.sqlthrows it), so the block is removed rather than reordered.Tests
Two tests added to
test/js/bun/util/inspect.test.jsunder "exceptions thrown while walking properties", both spawning a subprocess because the unfixed binary dies:Symbolmakes only theSymbol("cwd")call in the shell builtin throw, soBun.$fails to reify whileBun.inspect(Bun)runs. Unfixed release printsfalse false true(Archive, the property after$, is missing), unfixed debug aborts; fixed, it printsfalse true true. Verified on Linux and Windows (an earlier version of this test replacedSymbolentirely, which also broke loadingnode:util; Windows needs that to formatprocess.env, and a lazy initializer failing there hits an unrelated pre-existing abort, so the test failed on the Windows lanes of the first CI run).Also ran the full
inspect.test.js,BunObject.test.ts,mock-fn.test.jsandsql/adapter-override.test.tsagainst the debug build, and the repros withBUN_JSC_validateExceptionChecks=1.no test proof · iteration 1 · 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