From 0931891b927e76cf72e0408f2bd1d647a18e6862 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 10 May 2026 15:30:05 +0000 Subject: [PATCH 1/3] Fix null deref in forEachProperty when Proxy getPrototype throws When walking the prototype chain during property enumeration for formatting, getPrototype() on a ProxyObject can throw (via a getPrototypeOf trap) or return early due to a pending exception, returning an empty JSValue. Calling .getObject() on an empty JSValue dereferences a null JSCell*. Guard the result of getPrototype() the same way the fast path already does: clear any pending exception and treat an empty result as the end of the chain. --- src/jsc/bindings/bindings.cpp | 5 ++++- test/js/bun/util/inspect.test.js | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index 22ac38063376..9498c755e5a4 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -5444,7 +5444,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 "getPrototypeOf" trap. + CLEAR_IF_EXCEPTION(scope); + iterating = proto ? proto.getObject() : nullptr; } } diff --git a/test/js/bun/util/inspect.test.js b/test/js/bun/util/inspect.test.js index 32a70af30183..7b041faedcb5 100644 --- a/test/js/bun/util/inspect.test.js +++ b/test/js/bun/util/inspect.test.js @@ -453,6 +453,29 @@ const fixture = [ ), ]; +describe("Proxy in prototype chain", () => { + it("inspecting an object whose prototype is a Proxy with a throwing getPrototypeOf trap does not crash", () => { + const obj = {}; + const proto = Object.getPrototypeOf(obj); + Object.setPrototypeOf( + obj, + new Proxy(proto, { + getPrototypeOf() { + throw new Error("nope"); + }, + }), + ); + expect(() => Bun.inspect(obj)).not.toThrow(); + }); + + it("inspecting an object whose prototype is a Proxy wrapping a native prototype does not crash", () => { + const e = expect({}); + const proto = Object.getPrototypeOf(e); + Object.setPrototypeOf(e, new Proxy(proto, {})); + expect(() => Bun.inspect(e)).not.toThrow(); + }); +}); + describe("crash testing", () => { for (let input of fixture) { it(`inspecting "${input.toString().slice(0, 20).replaceAll("\n", "\\n")}" doesn't crash`, async () => { From b03795dcc025b25c4cba564d494d21b983af9cdf Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 10 May 2026 15:57:59 +0000 Subject: [PATCH 2/3] Clear exception before continue in property slot lookup When getPropertySlot returns false with a pending exception (Proxy trap throws), the continue skipped CLEAR_IF_EXCEPTION, leaving the exception pending into the next getPropertySlot call and tripping JSC's exception-scope verification. --- src/jsc/bindings/bindings.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index 9498c755e5a4..c8df5aa8f44d 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 hasProperty = object->getPropertySlot(globalObject, property, slot); // Ignore exceptions from "Get" proxy traps. CLEAR_IF_EXCEPTION(scope); + if (!hasProperty) + continue; if ((slot.attributes() & PropertyAttribute::DontEnum) != 0) { if (property == propertyNames->underscoreProto From 87ac0b8d73655d9f5deace658443e45af2e5713c Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 10 May 2026 16:23:25 +0000 Subject: [PATCH 3/3] ci: retrigger