From dbf0c2cb60f86e80764fc715d96d7b7e083fb8eb Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 13 May 2026 18:00:30 +0000 Subject: [PATCH] Fix crash when inspecting JSX elements with circular or non-object props The JSX tag was not included in canHaveCircularReferences, so inspecting a React element whose props/key/children referred back to itself would recurse until the stack overflowed. The JSX formatter also unconditionally unwrapped props as an object, panicking when props was a primitive. --- src/jsc/ConsoleObject.zig | 8 +++----- src/test_runner/pretty_format.zig | 8 +++----- test/js/bun/util/inspect.test.js | 25 +++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/jsc/ConsoleObject.zig b/src/jsc/ConsoleObject.zig index 2489ea14ea03..b53179812689 100644 --- a/src/jsc/ConsoleObject.zig +++ b/src/jsc/ConsoleObject.zig @@ -1136,7 +1136,7 @@ pub const Formatter = struct { pub fn canHaveCircularReferences(tag: Tag) bool { return switch (tag) { - .Function, .Array, .Object, .Map, .Set, .Error, .Class, .Event => true, + .Function, .Array, .Object, .Map, .Set, .Error, .Class, .Event, .JSX => true, else => false, }; } @@ -3140,13 +3140,11 @@ pub const Formatter = struct { } } - if (try value.get(this.globalThis, "props")) |props| { + if (try value.get(this.globalThis, "props")) |props| if (props.getObject()) |props_obj| { const prev_quote_strings = this.quote_strings; defer this.quote_strings = prev_quote_strings; this.quote_strings = true; - // SAFETY: JSX props are always objects - const props_obj = props.getObject().?; var props_iter = try jsc.JSPropertyIterator(.{ .skip_empty_name = true, .include_value = true, @@ -3303,7 +3301,7 @@ pub const Formatter = struct { } } } - } + }; writer.writeAll(" />"); }, diff --git a/src/test_runner/pretty_format.zig b/src/test_runner/pretty_format.zig index 2a5fcdaf5600..cd22590de2d2 100644 --- a/src/test_runner/pretty_format.zig +++ b/src/test_runner/pretty_format.zig @@ -326,7 +326,7 @@ pub const JestPrettyFormat = struct { } pub inline fn canHaveCircularReferences(tag: Tag) bool { - return tag == .Array or tag == .Object or tag == .Map or tag == .Set; + return tag == .Array or tag == .Object or tag == .Map or tag == .Set or tag == .JSX; } const Result = struct { @@ -1534,13 +1534,11 @@ pub const JestPrettyFormat = struct { } } - if (try value.get(this.globalThis, "props")) |props| { + if (try value.get(this.globalThis, "props")) |props| if (props.getObject()) |props_obj| { const prev_quote_strings = this.quote_strings; defer this.quote_strings = prev_quote_strings; this.quote_strings = true; - // SAFETY: JSX props are always an object. - const props_obj = props.getObject().?; var props_iter = try jsc.JSPropertyIterator(.{ .skip_empty_name = true, .include_value = true, @@ -1691,7 +1689,7 @@ pub const JestPrettyFormat = struct { } } } - } + }; writer.writeAll(" />"); }, diff --git a/test/js/bun/util/inspect.test.js b/test/js/bun/util/inspect.test.js index 32a70af30183..97681f5246ba 100644 --- a/test/js/bun/util/inspect.test.js +++ b/test/js/bun/util/inspect.test.js @@ -316,6 +316,31 @@ it("jsx with fragment", () => { expect(input).toBe(output); }); +it("jsx with circular props does not crash", () => { + const el = { $$typeof: Symbol.for("react.element"), type: "div", key: null, ref: null, props: {} }; + el.props = el; + expect(Bun.inspect(el)).toContain("[Circular]"); +}); + +it("jsx with circular key does not crash", () => { + const el = { $$typeof: Symbol.for("react.element"), type: "div" }; + el.key = el; + expect(Bun.inspect(el)).toContain("[Circular]"); +}); + +it("jsx with circular children does not crash", () => { + const el = { $$typeof: Symbol.for("react.element"), type: "div", key: null, ref: null, props: {} }; + el.props = { children: el }; + expect(Bun.inspect(el)).toContain("[Circular]"); +}); + +it("jsx with non-object props does not crash", () => { + for (const props of [123, "str", true, null, Symbol("x")]) { + const el = { $$typeof: Symbol.for("react.element"), type: "div", key: null, ref: null, props }; + expect(() => Bun.inspect(el)).not.toThrow(); + } +}); + it("inspect", () => { expect(Bun.inspect(new TypeError("what")).includes("TypeError: what")).toBe(true); expect(Bun.inspect("hi")).toBe('"hi"');