From c93f76e7b25a1e7384fd289dcf199c95467db09e Mon Sep 17 00:00:00 2001 From: robobun Date: Wed, 29 Apr 2026 09:27:29 +0000 Subject: [PATCH 1/2] Fix stack overflow when inspecting JSX elements with circular references Bun.inspect() on a React element whose props (or children) referenced the element itself would recurse until the native stack was exhausted, because the .JSX format tag was not included in canHaveCircularReferences() and therefore skipped both the visited-map check and the stack-overflow guard. Also fix a panic when props is not an object. --- src/bun.js/ConsoleObject.zig | 7 +++---- test/js/bun/util/inspect.test.js | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/bun.js/ConsoleObject.zig b/src/bun.js/ConsoleObject.zig index a1cfeb51035..328e399b00f 100644 --- a/src/bun.js/ConsoleObject.zig +++ b/src/bun.js/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,12 @@ pub const Formatter = struct { } } - if (try value.get(this.globalThis, "props")) |props| { + if (try value.get(this.globalThis, "props")) |props| props: { 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().?; + const props_obj = props.getObject() orelse break :props; var props_iter = try jsc.JSPropertyIterator(.{ .skip_empty_name = true, .include_value = true, diff --git a/test/js/bun/util/inspect.test.js b/test/js/bun/util/inspect.test.js index 32a70af3018..77abefa5982 100644 --- a/test/js/bun/util/inspect.test.js +++ b/test/js/bun/util/inspect.test.js @@ -772,3 +772,28 @@ it("CustomEvent", () => { }" `); }); + +describe("JSX element", () => { + it("handles circular props without crashing", () => { + const el = { $$typeof: Symbol.for("react.element"), type: "div", props: {} }; + el.props = el; + expect(Bun.inspect(el)).toContain("[Circular]"); + }); + + it("handles circular children without crashing", () => { + const el = { $$typeof: Symbol.for("react.element"), type: "div", props: {} }; + el.props.children = el; + expect(Bun.inspect(el)).toContain("[Circular]"); + }); + + it("handles circular array children without crashing", () => { + const el = { $$typeof: Symbol.for("react.element"), type: "div", props: { children: [] } }; + el.props.children.push(el); + expect(Bun.inspect(el)).toContain("[Circular]"); + }); + + it("handles non-object props without crashing", () => { + const el = { $$typeof: Symbol.for("react.element"), type: "div", props: 42 }; + expect(Bun.inspect(el)).toBe("
"); + }); +}); From 146336221bc875754c23bae645b47a3bd3527277 Mon Sep 17 00:00:00 2001 From: robobun Date: Wed, 29 Apr 2026 09:36:18 +0000 Subject: [PATCH 2/2] Apply same JSX circular/non-object props fix to pretty_format.zig The expect() diff formatter has a duplicate JSX printing path with the same unsafe props.getObject().? unwrap and the same missing .JSX entry in canHaveCircularReferences(). Apply the identical fix and add a regression test that exercises pretty_format via expect().toEqual(). --- src/bun.js/test/pretty_format.zig | 7 ++-- .../bun/test/pretty-format-overflow.test.ts | 42 +++++++++++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/bun.js/test/pretty_format.zig b/src/bun.js/test/pretty_format.zig index a9e27eb9735..88b41ae9e99 100644 --- a/src/bun.js/test/pretty_format.zig +++ b/src/bun.js/test/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,12 @@ pub const JestPrettyFormat = struct { } } - if (try value.get(this.globalThis, "props")) |props| { + if (try value.get(this.globalThis, "props")) |props| props: { 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().?; + const props_obj = props.getObject() orelse break :props; var props_iter = try jsc.JSPropertyIterator(.{ .skip_empty_name = true, .include_value = true, diff --git a/test/js/bun/test/pretty-format-overflow.test.ts b/test/js/bun/test/pretty-format-overflow.test.ts index 4acdf03b511..677dfa04b02 100644 --- a/test/js/bun/test/pretty-format-overflow.test.ts +++ b/test/js/bun/test/pretty-format-overflow.test.ts @@ -50,4 +50,46 @@ test("deep nesting", () => { // Verify it actually formatted and showed the diff (not just crashed) expect(stderr).toContain("expect(received).toEqual(expected)"); }, 30000); + + test("JSX element with circular or non-object props", async () => { + const dir = tempDirWithFiles("pretty-format-jsx", { + "jsx.test.ts": ` +import { test, expect } from "bun:test"; + +test("circular props", () => { + const el: any = { $$typeof: Symbol.for("react.element"), type: "div", props: {} }; + el.props = el; + expect(el).toEqual({}); +}); + +test("circular children", () => { + const el: any = { $$typeof: Symbol.for("react.element"), type: "div", props: {} }; + el.props.children = el; + expect(el).toEqual({}); +}); + +test("non-object props", () => { + const el: any = { $$typeof: Symbol.for("react.element"), type: "div", props: 42 }; + expect(el).toEqual({}); +}); +`, + }); + + const proc = Bun.spawn({ + cmd: [bunExe(), "test", "jsx.test.ts"], + env: bunEnv, + cwd: dir, + stderr: "pipe", + stdout: "pipe", + }); + + const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); + + expect(exitCode).toBe(1); + expect(stderr).not.toContain("panic"); + expect(stderr).not.toContain("SIGSEGV"); + expect(stderr).toContain("[Circular]"); + expect(stderr).toContain("expect(received).toEqual(expected)"); + expect(stderr).toContain("3 fail"); + }, 30000); });