Skip to content

Fix crash when inspecting circular or malformed JSX elements - #30383

Closed
robobun wants to merge 1 commit into
mainfrom
farm/d4a9f37f/fix-jsx-inspect-circular
Closed

Fix crash when inspecting circular or malformed JSX elements#30383
robobun wants to merge 1 commit into
mainfrom
farm/d4a9f37f/fix-jsx-inspect-circular

Conversation

@robobun

@robobun robobun commented May 8, 2026

Copy link
Copy Markdown
Collaborator

Bun.inspect() on a React element (object with $$typeof set to Symbol.for("react.element")) would segfault in two cases:

  • If the element contained a circular reference through key or props.children, the formatter recursed without cycle detection until stack overflow. The .JSX tag was missing from canHaveCircularReferences().

    const elem = { $$typeof: Symbol.for("react.element"), type: "div", key: null, ref: null, props: {} };
    elem.key = elem;
    Bun.inspect(elem); // SIGSEGV
  • If props was not an object, getObject().? unwrapped null and panicked.

    Bun.inspect({ $$typeof: Symbol.for("react.element"), type: "div", props: 123 }); // panic

The same fixes are applied to the jest pretty-format path in src/test_runner/pretty_format.zig.

Found by Fuzzilli (fingerprint c3da574162c76702).

Bun.inspect() on a React element (object with $$typeof set to
Symbol.for('react.element')) would segfault in two cases:

- If the element contained a circular reference through key or
  props.children, the formatter recursed without cycle detection
  until stack overflow. The .JSX tag was missing from
  canHaveCircularReferences().

- If props was not an object, getObject().? unwrapped null and
  panicked.

Apply the same fixes to the jest pretty-format path.
@github-actions github-actions Bot added the claude label May 8, 2026
@robobun

robobun commented May 8, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 8:57 PM PT - May 7th, 2026

@robobun, your commit d5d510d has 1 failures in Build #52721 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 30383

That installs a local version of the PR into your bun-30383 executable, so you can run:

bun-30383 --bun

@coderabbitai

coderabbitai Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR extends JSX handling in Bun's console formatting system. It adds JSX values to circular-reference detection eligibility and refactors JSX props retrieval to safely convert props to objects via toJS() before iteration, replacing assumptions that props were always valid objects. Tests validate circular-reference handling and non-object props rendering.

Changes

JSX Circular Reference and Props Handling

Layer / File(s) Summary
Circular Reference Support
src/jsc/ConsoleObject.zig, src/test_runner/pretty_format.zig
JSX is added to the set of formatter tags eligible for circular-reference tracking alongside existing container-like types.
JSX Props Conversion
src/jsc/ConsoleObject.zig, src/test_runner/pretty_format.zig
JSX props handling refactors from direct object extraction to guarded retrieval with toJS() conversion before property iteration.
Test Coverage
test/js/bun/util/inspect.test.js
Three test cases validate circular-reference detection in JSX key and props.children, and rendering of non-object props as self-closing JSX strings.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and specifically describes the main fix: preventing crashes when inspecting JSX elements with circular references or malformed props.
Description check ✅ Passed The description includes both required sections with substantial detail: clear explanation of what the PR does (two segfault/panic scenarios with code examples) and verification through test cases and Fuzzilli discovery.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/test_runner/pretty_format.zig`:
- Around line 1537-1538: The JSX formatter currently only recognizes
"react.element" and "react.fragment" and therefore skips React 19 transitional
elements; update the tag-name check that matches "react.element" and
"react.fragment" to also accept "react.transitional.element" (the same place
that branches into the hardened JSX path) so that when you extract props (the
block using value.get(this.globalThis, "props") -> props_obj ->
props_obj.toJS()) transitional elements follow the JSX formatting path just like
the other React element types.

In `@test/js/bun/util/inspect.test.js`:
- Around line 777-810: Replace the three separate it(...) cases ("handles
circular key", "handles circular children", "handles non-object props") with a
single parameterized it.each(...) table that supplies a description, a function
or factory to build the elem fixture (so you can create circular references
inside the factory for the first two cases), and the expected assertion
type/value; call Bun.inspect on the produced elem in the test body and branch
the assertion: expect(...).toContain("[Circular]") for the first two fixtures
and expect(...).toBe("<div />") for the non-object props fixture. Ensure you
reference the same symbols used now (elem, props, $$typeof, Bun.inspect) so the
factory builds identical shapes and the descriptions match the original test
names.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: fbe36a92-f047-4378-9bf5-5b64b0cf9af1

📥 Commits

Reviewing files that changed from the base of the PR and between 9ed6e89 and d5d510d.

📒 Files selected for processing (3)
  • src/jsc/ConsoleObject.zig
  • src/test_runner/pretty_format.zig
  • test/js/bun/util/inspect.test.js

Comment on lines +1537 to +1538
if (if (try value.get(this.globalThis, "props")) |props| props.getObject() else null) |props_obj| {
const props = props_obj.toJS();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Compare React element symbol coverage between the two formatter implementations.
rg -n -C2 'react\.(element|transitional\.element|fragment)' \
  src/test_runner/pretty_format.zig \
  src/jsc/ConsoleObject.zig

Repository: oven-sh/bun

Length of output: 1629


React 19 transitional elements still bypass the JSX formatter.

This file detects only "react.element" and "react.fragment" (lines 419-422) but not "react.transitional.element", unlike src/jsc/ConsoleObject.zig (line 1307). React 19 elements will skip the hardened JSX path added in this PR and fall back to generic object formatting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test_runner/pretty_format.zig` around lines 1537 - 1538, The JSX
formatter currently only recognizes "react.element" and "react.fragment" and
therefore skips React 19 transitional elements; update the tag-name check that
matches "react.element" and "react.fragment" to also accept
"react.transitional.element" (the same place that branches into the hardened JSX
path) so that when you extract props (the block using value.get(this.globalThis,
"props") -> props_obj -> props_obj.toJS()) transitional elements follow the JSX
formatting path just like the other React element types.

Comment on lines +777 to +810
it("handles circular key", () => {
const elem = {
$$typeof: Symbol.for("react.element"),
type: "div",
key: null,
ref: null,
props: { children: [] },
};
elem.key = elem;
expect(Bun.inspect(elem)).toContain("[Circular]");
});

it("handles circular children", () => {
const elem = {
$$typeof: Symbol.for("react.element"),
type: "div",
key: null,
ref: null,
props: {},
};
elem.props.children = elem;
expect(Bun.inspect(elem)).toContain("[Circular]");
});

it("handles non-object props", () => {
const elem = {
$$typeof: Symbol.for("react.element"),
type: "div",
key: null,
ref: null,
props: 123,
};
expect(Bun.inspect(elem)).toBe("<div />");
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cat -n test/js/bun/util/inspect.test.js | sed -n '770,820p'

Repository: oven-sh/bun

Length of output: 1236


🏁 Script executed:

rg "describe\.each" test/js/bun/util/inspect.test.js -A 5 -B 2

Repository: oven-sh/bun

Length of output: 37


🏁 Script executed:

rg "describe\.each" test/js/bun/ --type js --type ts -l | head -20

Repository: oven-sh/bun

Length of output: 627


🏁 Script executed:

cat test/js/bun/test/jest-each.test.ts | head -100

Repository: oven-sh/bun

Length of output: 1609


🏁 Script executed:

rg "describe\.each|it\.each" test/js/bun/test/jest-each.test.ts -A 8 -B 1 | head -80

Repository: oven-sh/bun

Length of output: 1455


🏁 Script executed:

rg "it\.each" test/js/bun/test/jest-each.test.ts -A 5 -B 1 | head -40

Repository: oven-sh/bun

Length of output: 1126


🏁 Script executed:

# Check if there are examples of describe.each with complex objects in the test suite
rg "describe\.each.*\(" test/js/bun/ -A 10 -B 1 --type ts --type js | head -60

Repository: oven-sh/bun

Length of output: 4859


🏁 Script executed:

# Let's verify the exact syntax expected by checking how describe.each callback should work
cat test/js/bun/udp/udp_socket.test.ts | sed -n '1,50p'

Repository: oven-sh/bun

Length of output: 2056


🏁 Script executed:

# Check the jest-each test more carefully to understand the callback structure
rg "describe\.each.*\{" test/js/bun/typescript/type-export.test.ts -A 8 -B 2 | head -50

Repository: oven-sh/bun

Length of output: 1323


🏁 Script executed:

# Let's confirm the exact callback signature expected for describe.each
rg "describe\.each.*\)" test/js/bun/udp/udp_socket.test.ts -A 5 -B 1

Repository: oven-sh/bun

Length of output: 358


🏁 Script executed:

# Check if there's any example that directly executes logic in describe.each callback
rg "describe\.each" test/js/bun/ --type ts --type js -A 15 | grep -A 15 "describe\.each.*hostname" | head -25

Repository: oven-sh/bun

Length of output: 1780


🏁 Script executed:

# Let me verify the exact structure needed - check if describe.each can work with verify functions or if we need it.each
cat test/js/bun/dns/resolve-dns.test.ts | grep -A 20 "describe.each(validHostnames)"

Repository: oven-sh/bun

Length of output: 546


Use it.each() for these parameterized test cases.

Lines 777–810 contain repeated test structure with only fixture/assertion differences. Using it.each() will consolidate these tests and align with repo conventions per the coding guidelines.

♻️ Suggested refactor
 describe("JSX element", () => {
-  it("handles circular key", () => {
-    const elem = {
-      $$typeof: Symbol.for("react.element"),
-      type: "div",
-      key: null,
-      ref: null,
-      props: { children: [] },
-    };
-    elem.key = elem;
-    expect(Bun.inspect(elem)).toContain("[Circular]");
-  });
-
-  it("handles circular children", () => {
-    const elem = {
-      $$typeof: Symbol.for("react.element"),
-      type: "div",
-      key: null,
-      ref: null,
-      props: {},
-    };
-    elem.props.children = elem;
-    expect(Bun.inspect(elem)).toContain("[Circular]");
-  });
-
-  it("handles non-object props", () => {
-    const elem = {
-      $$typeof: Symbol.for("react.element"),
-      type: "div",
-      key: null,
-      ref: null,
-      props: 123,
-    };
-    expect(Bun.inspect(elem)).toBe("<div />");
-  });
+  it.each([
+    {
+      name: "handles circular key",
+      setup: () => {
+        const elem = {
+          $$typeof: Symbol.for("react.element"),
+          type: "div",
+          key: null,
+          ref: null,
+          props: { children: [] },
+        };
+        elem.key = elem;
+        return elem;
+      },
+      assertion: (output) => expect(output).toContain("[Circular]"),
+    },
+    {
+      name: "handles circular children",
+      setup: () => {
+        const elem = {
+          $$typeof: Symbol.for("react.element"),
+          type: "div",
+          key: null,
+          ref: null,
+          props: {},
+        };
+        elem.props.children = elem;
+        return elem;
+      },
+      assertion: (output) => expect(output).toContain("[Circular]"),
+    },
+    {
+      name: "handles non-object props",
+      setup: () => ({
+        $$typeof: Symbol.for("react.element"),
+        type: "div",
+        key: null,
+        ref: null,
+        props: 123,
+      }),
+      assertion: (output) => expect(output).toBe("<div />"),
+    },
+  ])("$name", ({ setup, assertion }) => {
+    assertion(Bun.inspect(setup()));
+  });
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/js/bun/util/inspect.test.js` around lines 777 - 810, Replace the three
separate it(...) cases ("handles circular key", "handles circular children",
"handles non-object props") with a single parameterized it.each(...) table that
supplies a description, a function or factory to build the elem fixture (so you
can create circular references inside the factory for the first two cases), and
the expected assertion type/value; call Bun.inspect on the produced elem in the
test body and branch the assertion: expect(...).toContain("[Circular]") for the
first two fixtures and expect(...).toBe("<div />") for the non-object props
fixture. Ensure you reference the same symbols used now (elem, props, $$typeof,
Bun.inspect) so the factory builds identical shapes and the descriptions match
the original test names.

@github-actions

github-actions Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor

Found 2 issues this PR may fix:

  1. bun crashed with error "panic(main thread): integer overflow oh no: Bun has crashed." when do bun test with @testing-library/react using toMatchSnapshot #19534 - Panic crash in bun test with @testing-library/react using toMatchSnapshot — the crash in pretty_format when serializing React component trees is consistent with the missing .JSX in canHaveCircularReferences() and the unsafe getObject() unwrap that this PR fixes.
  2. ToMatchSnapshot broken with react components #5540 - toMatchSnapshot broken with React components producing massive output — without .JSX in canHaveCircularReferences(), the formatter would infinitely expand circular React component trees. This PR's circular reference fix addresses the crash/explosion, though the full serialization format may still differ from Jest.

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #19534
Fixes #5540

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Fix stack overflow in Bun.inspect for circular JSX elements #30126 - Also fixes stack overflow in Bun.inspect for circular JSX elements by adding JSX to canHaveCircularReferences()
  2. console: guard JSX/Proxy recursion in Bun.inspect (stack overflow SIGSEGV) #29709 - Also fixes stack overflow in Bun.inspect() on circular JSX elements

🤖 Generated with Claude Code

@robobun

robobun commented May 8, 2026

Copy link
Copy Markdown
Collaborator Author

Duplicate of #29709 which already covers both the circular JSX fix and the non-object props fix.

@robobun robobun closed this May 8, 2026
@robobun
robobun deleted the farm/d4a9f37f/fix-jsx-inspect-circular branch May 8, 2026 03:56

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — straightforward crash fix with tests.

Extended reasoning...

Overview

This PR fixes two crashes in Bun.inspect() / jest pretty-format when handling malformed JSX-shaped objects (objects with $$typeof: Symbol.for("react.element")). It touches src/jsc/ConsoleObject.zig, src/test_runner/pretty_format.zig, and adds three regression tests in test/js/bun/util/inspect.test.js.

The two changes are:

  1. Add .JSX to canHaveCircularReferences() so the existing visited-map cycle detection in printAs() kicks in for JSX elements, preventing unbounded recursion → stack overflow when key or props.children form a cycle.
  2. Replace props.getObject().? (which panics on null) with an upfront if (... props.getObject() else null) |props_obj| guard, so non-object props values are simply skipped and the element renders as <div />.

Security risks

None. This is purely a defensive hardening of debug-output formatting against fuzzer-generated inputs. No new attack surface, no auth/crypto/permissions involvement; if anything it removes a DoS-via-crash vector for code that logs untrusted objects.

Level of scrutiny

Low. The diff is ~10 lines of logic across two near-identical formatters, both following the established pattern already used for .Array, .Object, .Map, .Set. The getObject() null-check is the idiomatic way this codebase guards optional object coercion. Behavior for well-formed JSX elements is unchanged (props_obj.toJS() round-trips to the same JSValue that was previously bound as props).

Other factors

Three new tests directly exercise each crash path (circular key, circular children, non-object props) and assert the expected [Circular] / <div /> output. The bug-hunting system found no issues. No outstanding reviewer comments on the PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant