From ad64014979e31410f164f1b683bbabe2121e6c34 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:22:36 +0000 Subject: [PATCH] Bun.inspect: clear exceptions from failed property lookups before moving on JSC__JSValue__forEachPropertyImpl skipped a property as soon as getPropertySlot returned false, but a lazy static property initializer or a Proxy get trap can return false with an exception still pending. That exception then stayed set while the next property was reified (dropping it from the output, and tripping the exception scope assertion in debug builds) and made the Proxy getPrototypeOf path return an empty JSValue that was dereferenced with getObject(). Clear the exception right after the lookup regardless of its result, and stop walking the prototype chain when getPrototype returns empty. --- src/jsc/bindings/bindings.cpp | 14 ++++++--- test/js/bun/util/inspect.test.js | 53 ++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index 82662920a933..70f473b11e75 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -5616,10 +5616,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 @@ -5691,7 +5692,12 @@ static void JSC__JSValue__forEachPropertyImpl(JSC::EncodedJSValue JSValue0, JSC: break; if (iterating == globalObject) break; - iterating = iterating->getPrototype(globalObject).getObject(); + JSValue proto = iterating->getPrototype(globalObject); + // Ignore exceptions from Proxy "getPrototypeOf" traps. + CLEAR_IF_EXCEPTION(scope); + if (!proto) + break; + iterating = proto.getObject(); } } diff --git a/test/js/bun/util/inspect.test.js b/test/js/bun/util/inspect.test.js index 5c91f3dbe05f..8e755441333c 100644 --- a/test/js/bun/util/inspect.test.js +++ b/test/js/bun/util/inspect.test.js @@ -579,6 +579,59 @@ it("Bun.inspect huge sparse array summarizes holes without iterating them", asyn }); }); +describe("Bun.inspect with a Proxy in the prototype chain", () => { + it("skips properties whose get trap throws", () => { + const proto = new Proxy( + { a: 1, b: 2 }, + { + get(target, key, receiver) { + if (key === "a" || key === "b") throw new Error("get trap"); + return Reflect.get(target, key, receiver); + }, + }, + ); + expect(Bun.inspect(Object.create(proto))).toBe("{}"); + expect(Bun.inspect(Object.assign(Object.create(proto), { own: 3 }))).toBe("{\n own: 3,\n}"); + }); + + it("stops walking the prototype chain when the getPrototypeOf trap throws", () => { + const proto = new Proxy( + { a: 1 }, + { + getPrototypeOf() { + throw new Error("getPrototypeOf trap"); + }, + }, + ); + expect(Bun.inspect(Object.create(proto))).toBe("{\n a: 1,\n}"); + expect(Bun.inspect(Object.create(Object.create(proto)))).toBe("{\n a: 1,\n}"); + }); +}); + +// Bun.$ is built lazily and reads the global `process` while doing so, so clobbering +// `process` makes that property throw while Bun.inspect enumerates the Bun object. The +// exception used to stay pending while the next lazy property (Archive) was built, which +// dropped it from the output in release builds and tripped an assertion in debug builds. +it("Bun.inspect keeps going when a lazily built property throws", async () => { + const code = ` + globalThis.process = undefined; + const out = Bun.inspect(Bun); + console.log(typeof out, out.includes("Archive:"), out.includes("version:")); + `; + 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: "string true true\n", + stderr: "", + exitCode: 0, + }); +}); + describe("console.logging function displays async and generator names", async () => { const cases = [ function () {},