From 088d9ce6e925b247a43a74b2aa1ac48db5544eef Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 10 Aug 2026 17:57:18 +0000 Subject: [PATCH] fix(inspect): clear exceptions between property lookups in forEachProperty JSC__JSValue__forEachPropertyImpl left an exception pending when getPropertySlot() returned false because a Proxy trap or a lazy property initializer threw, then kept looking up the remaining properties with it still pending. The getPrototype() call at the end of the loop also dereferenced the empty value returned when a Proxy getPrototypeOf trap throws (or when an exception is already pending), which segfaults in release builds. Clear the exception before checking the getPropertySlot() result and stop walking the prototype chain when getPrototype() throws. Also drop the debug-only reportUncaughtExceptionAtEventLoop() calls in the Bun.sql / Bun.SQL initializers: they ran while the exception was still pending, which trips a structure assertion in debug builds as soon as the uncaught exception handler touches a static property, and the exception is propagated to the caller right after anyway. --- src/jsc/bindings/BunObject.cpp | 6 ---- src/jsc/bindings/bindings.cpp | 14 +++++++--- test/js/bun/util/inspect.test.js | 48 ++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/src/jsc/bindings/BunObject.cpp b/src/jsc/bindings/BunObject.cpp index 8a022228eb4c..e4d4ab407ba5 100644 --- a/src/jsc/bindings/BunObject.cpp +++ b/src/jsc/bindings/BunObject.cpp @@ -319,9 +319,6 @@ static JSValue defaultBunSQLObject(VM& vm, JSObject* bunObject) auto scope = DECLARE_THROW_SCOPE(vm); auto* globalObject = defaultGlobalObject(bunObject->globalObject()); JSValue sqlValue = globalObject->internalModuleRegistry()->requireId(globalObject, vm, InternalModuleRegistry::BunSql); -#if BUN_DEBUG - if (scope.exception()) globalObject->reportUncaughtExceptionAtEventLoop(globalObject, scope.exception()); -#endif RETURN_IF_EXCEPTION(scope, {}); RELEASE_AND_RETURN(scope, sqlValue.getObject()->get(globalObject, vm.propertyNames->defaultKeyword)); } @@ -331,9 +328,6 @@ static JSValue constructBunSQLObject(VM& vm, JSObject* bunObject) auto scope = DECLARE_THROW_SCOPE(vm); auto* globalObject = defaultGlobalObject(bunObject->globalObject()); JSValue sqlValue = globalObject->internalModuleRegistry()->requireId(globalObject, vm, InternalModuleRegistry::BunSql); -#if BUN_DEBUG - if (scope.exception()) globalObject->reportUncaughtExceptionAtEventLoop(globalObject, scope.exception()); -#endif RETURN_IF_EXCEPTION(scope, {}); auto clientData = WebCore::clientData(vm); RELEASE_AND_RETURN(scope, sqlValue.getObject()->get(globalObject, clientData->builtinNames().SQLPublicName())); diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index c084a885ee86..5197a5f49f6a 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -5596,10 +5596,11 @@ static void JSC__JSValue__forEachPropertyImpl(JSC::EncodedJSValue JSValue0, JSC: } JSC::PropertySlot slot(object, PropertySlot::InternalMethodType::Get); - if (!object->getPropertySlot(globalObject, property, slot)) - continue; - // Ignore exceptions from "Get" proxy traps. + bool hasProperty = object->getPropertySlot(globalObject, property, slot); + // Ignore exceptions from "Get" proxy traps and lazy property initializers. CLEAR_IF_EXCEPTION(scope); + if (!hasProperty) + continue; if ((slot.attributes() & PropertyAttribute::DontEnum) != 0) { if (property == propertyNames->underscoreProto @@ -5671,7 +5672,12 @@ static void JSC__JSValue__forEachPropertyImpl(JSC::EncodedJSValue JSValue0, JSC: break; if (iterating == globalObject) break; - iterating = iterating->getPrototype(globalObject).getObject(); + JSValue prototype = iterating->getPrototype(globalObject); + // Ignore exceptions from Proxy "getPrototypeOf" traps. + CLEAR_IF_EXCEPTION(scope); + if (!prototype) [[unlikely]] + break; + iterating = prototype.getObject(); } } diff --git a/test/js/bun/util/inspect.test.js b/test/js/bun/util/inspect.test.js index 5c91f3dbe05f..b098b5e8eab4 100644 --- a/test/js/bun/util/inspect.test.js +++ b/test/js/bun/util/inspect.test.js @@ -467,6 +467,54 @@ describe("crash testing", () => { } }); +// An exception thrown while looking up a property (Proxy trap, lazily initialized +// property) used to stay pending while the next property was looked up, and a +// throwing getPrototypeOf trap crashed the walk up the prototype chain. +// Run in a child so a regression fails this test instead of killing the runner. +it("Bun.inspect ignores exceptions thrown while enumerating properties", async () => { + const code = ` + { + const target = { a: 1, b: 2 }; + const proto = new Proxy(target, { + get(t, key, receiver) { + if (key in target) throw new Error("get " + String(key)); + return Reflect.get(t, key, receiver); + }, + }); + const obj = Object.create(proto); + obj.own = 1; + console.log(Bun.inspect(obj)); + } + { + const proto = new Proxy({ fromProto: 2 }, { + getPrototypeOf() { throw new Error("getPrototypeOf"); }, + }); + const obj = Object.create(proto); + obj.own = 1; + console.log(Bun.inspect(obj)); + } + { + // Several of Bun's lazily initialized properties (Bun.$ among them) call + // Symbol() when first accessed, so this makes their initializers throw. + globalThis.Symbol = undefined; + const out = Bun.inspect(Bun); + console.log(typeof out, out.includes("inspect")); + } + `; + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", code], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stdout, stderr, exitCode }).toEqual({ + stdout: "{\n own: 1,\n}\n" + "{\n own: 1,\n fromProto: 2,\n}\n" + "string true\n", + stderr: "", + exitCode: 0, + }); +}); + it("possibly formatted emojis log", () => { expect(Bun.inspect("✔")).toBe('"✔"'); });