From ead71115d9c3fa390e6cf27dd90533da655a432a Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 9 Aug 2026 08:52:40 +0000 Subject: [PATCH 1/2] inspect: clear pending exception when a lazy property initializer throws A static-table PropertyCallback that runs JS (Bun.$, Bun.sql) can throw, for example when the global Symbol has been overwritten. JSC reports the slot as not found with the exception left pending, but the property walk in JSC__JSValue__forEachPropertyImpl skipped its exception clear on that branch, so the stale exception aborted assert-enabled builds at the next property's reification and could leak into unrelated code in release builds. Also remove the debug-only uncaught exception report from the Bun.sql lazy getters: it re-entered the uncaught exception machinery with the exception still pending, tripping a stale-structure assertion inside process.get. The exception already propagates to the caller. --- src/jsc/bindings/BunObject.cpp | 6 ---- src/jsc/bindings/bindings.cpp | 8 +++-- test/js/bun/util/inspect.test.js | 51 ++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 9 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..39f92984dcee 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -5596,10 +5596,12 @@ 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 found = object->getPropertySlot(globalObject, property, slot); + // Ignore exceptions from "Get" proxy traps and throwing lazy + // property initializers (which report the slot as not found). CLEAR_IF_EXCEPTION(scope); + if (!found) + continue; if ((slot.attributes() & PropertyAttribute::DontEnum) != 0) { if (property == propertyNames->underscoreProto diff --git a/test/js/bun/util/inspect.test.js b/test/js/bun/util/inspect.test.js index 5c91f3dbe05f..ade951492197 100644 --- a/test/js/bun/util/inspect.test.js +++ b/test/js/bun/util/inspect.test.js @@ -928,3 +928,54 @@ describe.skipIf(!isASAN)("object mutated while being formatted", () => { expect(exitCode).toBe(0); }); }); + +it("skips properties whose lazy initializer throws instead of leaving the exception pending", async () => { + // Overwriting the global Symbol breaks the lazily-initialized Bun.$ and + // Bun.sql properties (their module-scope code calls Symbol). Inspecting the + // Bun object reifies every property; a throwing initializer has to be + // skipped, not left as a pending exception, which aborts assert-enabled + // builds. + const code = ` + Symbol++; + const out = Bun.inspect(Bun); + if (typeof out !== "string" || out.length === 0) throw new Error("empty inspect output"); + console.log("ok"); + `; + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", code], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); + expect(stdout).toBe("ok\n"); + expect(exitCode).toBe(0); +}); + +it("building an invalid-argument error message survives lazy initializers that throw", async () => { + // Same walk reached through ERR_INVALID_ARG_VALUE rendering the received + // value: new CompressionStream(globalThis) inspects globalThis for its + // error message while the global Symbol is broken. Getters that report + // their failure can set a nonzero exit code, so only assert that execution + // gets past the constructor instead of aborting. + const code = ` + Symbol++; + let caught; + try { + new CompressionStream(globalThis); + } catch (e) { + caught = e; + } + if (!caught) throw new Error("expected CompressionStream to throw"); + console.log("ok"); + `; + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", code], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout] = await Promise.all([proc.stdout.text(), proc.exited]); + expect(stdout).toBe("ok\n"); + expect(proc.signalCode).toBeNull(); +}); From ae03357520459f7936bfedee652531e3408574fc Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 9 Aug 2026 08:57:44 +0000 Subject: [PATCH 2/2] test: drain stderr in the lazy initializer regression tests --- test/js/bun/util/inspect.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/js/bun/util/inspect.test.js b/test/js/bun/util/inspect.test.js index ade951492197..b527a8dc0874 100644 --- a/test/js/bun/util/inspect.test.js +++ b/test/js/bun/util/inspect.test.js @@ -947,7 +947,7 @@ it("skips properties whose lazy initializer throws instead of leaving the except stdout: "pipe", stderr: "pipe", }); - const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); + const [stdout, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); expect(stdout).toBe("ok\n"); expect(exitCode).toBe(0); }); @@ -975,7 +975,7 @@ it("building an invalid-argument error message survives lazy initializers that t stdout: "pipe", stderr: "pipe", }); - const [stdout] = await Promise.all([proc.stdout.text(), proc.exited]); + const [stdout] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); expect(stdout).toBe("ok\n"); expect(proc.signalCode).toBeNull(); });