diff --git a/src/jsc/bindings/BunObject.cpp b/src/jsc/bindings/BunObject.cpp index 04c1c4ca808b..02cee98b85fe 100644 --- a/src/jsc/bindings/BunObject.cpp +++ b/src/jsc/bindings/BunObject.cpp @@ -320,9 +320,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)); } @@ -332,9 +329,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 40e19b3e571e..357c6d13d27e 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -5605,10 +5605,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 builders. CLEAR_IF_EXCEPTION(scope); + if (!hasProperty) + continue; if ((slot.attributes() & PropertyAttribute::DontEnum) != 0) { if (property == propertyNames->underscoreProto @@ -5680,7 +5681,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..9a29c9562ed2 100644 --- a/test/js/bun/util/inspect.test.js +++ b/test/js/bun/util/inspect.test.js @@ -928,3 +928,84 @@ describe.skipIf(!isASAN)("object mutated while being formatted", () => { expect(exitCode).toBe(0); }); }); + +// Run in a child: a regression here segfaults the process instead of throwing. +describe("property lookup throws while formatting an object", () => { + it.concurrent("Proxy traps in the prototype chain", async () => { + const fixture = ` + { + // Only "a" throws, so the walk has to carry on past it without the + // exception still pending when "b" and "c" are looked up. + const proto = new Proxy( + { a: 1, b: 2, c: 3 }, + { + get(target, key, receiver) { + if (key === "a") throw new Error("get trap"); + return Reflect.get(target, key, receiver); + }, + }, + ); + console.log(Bun.inspect(Object.create(proto))); + } + { + const proto = new Proxy( + { a: 1 }, + { + getPrototypeOf() { + throw new Error("getPrototypeOf trap"); + }, + }, + ); + const obj = Object.create(proto); + obj.x = 1; + console.log(Bun.inspect(obj)); + console.log(obj); + } + `; + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", fixture], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(stdout).toMatchInlineSnapshot(` + "{ + b: 2, + c: 3, + } + { + x: 1, + a: 1, + } + { + x: 1, + a: 1, + } + " + `); + expect(exitCode).toBe(0); + }); + + it.concurrent("lazy property of the Bun object", async () => { + // Bun.redis builds the default client the first time it is read, and an + // invalid REDIS_URL makes that throw. Only that property may be left out; + // the properties visited after it must still be printed. + const fixture = ` + const keys = Object.keys(Bun); + const out = Bun.inspect(Bun); + console.log(JSON.stringify(keys.filter(key => !out.includes("\\n " + key + ":")))); + `; + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", fixture], + env: { ...bunEnv, REDIS_URL: "http://not-a-redis-url" }, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(stdout).toBe('["redis"]\n'); + expect(exitCode).toBe(0); + }); +});