diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index 22ac38063376..04976e17d8d3 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -5369,10 +5369,11 @@ static void JSC__JSValue__forEachPropertyImpl(JSC::EncodedJSValue JSValue0, JSC: } JSC::PropertySlot slot(object, PropertySlot::InternalMethodType::Get); - if (!object->getPropertySlot(globalObject, property, slot)) - continue; + bool hasSlot = object->getPropertySlot(globalObject, property, slot); // Ignore exceptions from "Get" proxy traps. CLEAR_IF_EXCEPTION(scope); + if (!hasSlot) + continue; if ((slot.attributes() & PropertyAttribute::DontEnum) != 0) { if (property == propertyNames->underscoreProto @@ -5444,7 +5445,10 @@ 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 "getPrototype" trap. + CLEAR_IF_EXCEPTION(scope); + iterating = proto ? proto.getObject() : nullptr; } } diff --git a/test/js/bun/util/inspect-proxy-prototype.test.ts b/test/js/bun/util/inspect-proxy-prototype.test.ts new file mode 100644 index 000000000000..32940ad84b99 --- /dev/null +++ b/test/js/bun/util/inspect-proxy-prototype.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, test } from "bun:test"; + +describe("Bun.inspect with Proxy in prototype chain", () => { + test("does not crash when a getter reached through a Proxy prototype throws", () => { + // The Expect prototype has getters (.rejects / .resolves) that mutate state and can + // throw when invoked in sequence. Going through a Proxy forces the slow property + // iteration path which invokes those getters. + const e = expect(1); + const proto = Object.getPrototypeOf(e); + Object.setPrototypeOf(e, new Proxy(proto, {})); + const out = Bun.inspect(e); + expect(out).toContain("toBe"); + }); + + test("does not crash when a Proxy getPrototypeOf trap throws during iteration", () => { + class Foo { + bar() {} + } + const obj = new Foo(); + const proto = Object.getPrototypeOf(obj); + Object.setPrototypeOf( + obj, + new Proxy(proto, { + getPrototypeOf() { + throw new Error("nope"); + }, + }), + ); + const out = Bun.inspect(obj); + expect(out).toContain("bar"); + }); + + test("does not crash when a Proxy get trap throws during iteration", () => { + class Foo { + bar() {} + baz() {} + } + const obj = new Foo(); + const proto = Object.getPrototypeOf(obj); + let count = 0; + Object.setPrototypeOf( + obj, + new Proxy(proto, { + get(t, k, r) { + if (++count > 3) throw new Error("get fail"); + return Reflect.get(t, k, r); + }, + }), + ); + expect(() => Bun.inspect(obj)).not.toThrow(); + }); +});