Skip to content

Fix stack overflow when inspecting JSX elements with circular references - #29750

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

Fix stack overflow when inspecting JSX elements with circular references#29750
robobun wants to merge 1 commit into
mainfrom
farm/deb3abed/jsx-inspect-circular

Conversation

@robobun

@robobun robobun commented Apr 26, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Bun.inspect() / console.log() would crash with a stack overflow when formatting a React element that contained a circular reference to itself via key, props, or children.

const el = { $$typeof: Symbol.for("react.element"), type: "div", key: null, ref: null, props: {} };
el.key = el;
Bun.inspect(el); // segfault (stack overflow)

The JSX formatter recurses into key, props, and children, but .JSX was not included in canHaveCircularReferences(), so the visited-map tracking and stack-overflow guard were never applied. This PR adds .JSX to that set so circular JSX elements print [Circular] instead of recursing forever.

How did you verify your code works?

Added regression tests for circular references via key, props, and children. All existing inspect.test.js tests continue to pass.

Found by Fuzzilli (fingerprint bb8715595a137556).

@robobun

robobun commented Apr 26, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 2:38 AM PT - Apr 26th, 2026

@robobun, your commit 902d52a has 1 failures in Build #48105 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 29750

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

bun-29750 --bun

@coderabbitai

coderabbitai Bot commented Apr 26, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8792a626-08b5-4122-8802-5c6f384b800a

📥 Commits

Reviewing files that changed from the base of the PR and between 86e6ab6 and 902d52a.

📒 Files selected for processing (2)
  • src/bun.js/ConsoleObject.zig
  • test/js/bun/util/inspect.test.js

Walkthrough

The changes extend the console formatter's circular-reference detection to handle JSX values alongside other container-like types. Three test cases were added to validate circular-reference behavior in JSX elements, including self-referencing keys, circular props, and circular children.

Changes

Cohort / File(s) Summary
Circular-Reference Detection for JSX
src/bun.js/ConsoleObject.zig
Extended Formatter.Tag.canHaveCircularReferences logic to include JSX values, aligning them with other container/graph-like types for circular-reference detection and handling.
JSX Circular-Reference Test Cases
test/js/bun/util/inspect.test.js
Added three test cases validating circular-reference handling in React/JSX inspection: element key self-reference with [Circular] marker, circular references in props with inline rendering, and circular references in children with multi-line indented output.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main fix: preventing stack overflow when inspecting JSX elements with circular references.
Description check ✅ Passed The description fully addresses both template sections with clear explanation of the problem, code example, root cause, and verification approach.
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.

@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. bun test with react DOM shows infinite error log on expect failed #10886 - Infinite error log when expect fails on a React DOM element — JSX elements with circular refs (parent/child DOM nodes) recurse infinitely because .JSX was missing from canHaveCircularReferences()

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

Fixes #10886

🤖 Generated with Claude Code

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,

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.

🔴 The same crash still exists in the parallel jest pretty-formatter: src/bun.js/test/pretty_format.zig:328-330 has its own canHaveCircularReferences() that still omits .JSX, and its .JSX branch recurses into key/props/children via this.format() with no stack-check fallback. After this PR, expect(circularJsxEl).toMatchSnapshot() or a failing toEqual diff (via diff_format.zig) will still segfault — the fuzzer crash is only half-fixed. The same one-line change should be mirrored at pretty_format.zig:329.

Extended reasoning...

What the bug is

This PR correctly adds .JSX to canHaveCircularReferences() in src/bun.js/ConsoleObject.zig:1139 so that Bun.inspect() / console.log() no longer stack-overflow on circular JSX elements. However, Bun has a second, parallel formatter in src/bun.js/test/pretty_format.zig (JestPrettyFormat) with its own copy of canHaveCircularReferences() at lines 328-330:

pub fn canHaveCircularReferences(tag: Tag) bool {
    return switch (tag) {
        .Array, .Object, .Map, .Set => true,
        else => false,
    };
}

This copy still does not include .JSX, and unlike ConsoleObject.zig it has no stack_check.isSafeToRecurse() fallback either.

The code path that triggers it

JestPrettyFormat's .JSX branch in printAs() (line 1477+) is structurally identical to the one this PR fixes:

  • line 1531: recurses into key via this.format(...)
  • line 1580: recurses into each prop value via this.format(...)
  • line 1643: recurses into JSX children via this.format(...)

In format() (line ~876), the visited-map circular-reference tracking is gated on comptime canHaveCircularReferences(tag). Since .JSX returns false, no visited-set entry is recorded and the recursion is unbounded.

This formatter is reachable from user code via:

  • expect(...).toMatchSnapshot()expect.zig:819jestSnapshotPrettyFormatJestPrettyFormat.format
  • any failing expect(...).toEqual(...) / .toBe(...) diff → diff_format.zig:39,48JestPrettyFormat.format

Why existing code doesn't prevent it

Tag.get() in pretty_format.zig (lines 416-423) detects 25618typeof === Symbol.for('react.element') and returns .JSX exactly like ConsoleObject.zig does, so the value is routed into the unprotected .JSX branch rather than the protected .Object branch. Grep confirms there is no isSafeToRecurse / StackCheck guard anywhere in pretty_format.zig, so there is no secondary safety net.

Step-by-step proof

const el = { $$typeof: Symbol.for('react.element'), type: 'div', key: null, ref: null, props: {} };
el.props.foo = el;
expect(el).toMatchSnapshot();
  1. toMatchSnapshot calls jestSnapshotPrettyFormat(el) (expect.zig:819) → JestPrettyFormat.format(el).
  2. Tag.get(el) sees $$typeof === react.element → returns .JSX (pretty_format.zig:416-423).
  3. format() checks canHaveCircularReferences(.JSX)false → skips visited-map insert.
  4. printAs(.JSX, ...) iterates props, hits foo, calls this.format(el) at line 1580.
  5. Goto step 2. Stack grows until segfault.

The same applies with el.key = el (line 1531) or el.props.children = [el] (line 1643).

Impact

The fuzzer crash (fingerprint bb8715595a137556) that this PR targets is only half-fixed: Bun.inspect() is safe, but the test-runner pretty-formatter for the same value still crashes the process. Any user who snapshot-tests or diff-compares a React tree containing a back-reference (e.g., a parent ref stashed on a child) will hit a hard segfault in bun test.

Fix

Mirror the change at src/bun.js/test/pretty_format.zig:329 — add .JSX to the switch (and optionally .Error/.Event/.Function/.Class for parity with ConsoleObject.zig). It's the same one-token diff.

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

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

🤖 Generated with Claude Code

@robobun

robobun commented Apr 26, 2026

Copy link
Copy Markdown
Collaborator Author

Duplicate of #29709 which covers this plus the parallel pretty_format.zig path. Closing in favor of that one.

@robobun robobun closed this Apr 26, 2026
@robobun
robobun deleted the farm/deb3abed/jsx-inspect-circular branch April 26, 2026 09:37
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