Skip to content

Fix stack overflow when inspecting circular JSX elements#30657

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

Fix stack overflow when inspecting circular JSX elements#30657
robobun wants to merge 1 commit into
mainfrom
farm/fe541895/jsx-inspect-circular

Conversation

@robobun

@robobun robobun commented May 14, 2026

Copy link
Copy Markdown
Collaborator

What

Bun.inspect / console.log segfaulted with a stack overflow when a React element contained a reference to itself via key, props, or children.

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

Why

The .JSX formatter tag was not included in canHaveCircularReferences(), so the visited-map and stack-depth guards were skipped. The formatter recursed into key / props / children, hit the same element, and looped until the native stack was exhausted.

Fix

Add .JSX to canHaveCircularReferences() in both ConsoleObject.zig and pretty_format.zig. Circular JSX now prints [Circular] like other object types.

Found by Fuzzilli (fingerprint 471aa8ea6d631cd1).

Bun.inspect / console.log would recurse without bound when a React
element contained a reference to itself (via key, props, or children),
leading to a segfault. Include .JSX in canHaveCircularReferences so the
visited-map and stack-depth guards apply.
@robobun

robobun commented May 14, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 7:38 PM PT - May 13th, 2026

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


🧪   To try this PR locally:

bunx bun-pr 30657

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

bun-30657 --bun

@coderabbitai

coderabbitai Bot commented May 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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: cbff6b3e-7732-4060-93c0-0714cc05ef04

📥 Commits

Reviewing files that changed from the base of the PR and between b8ecc78 and cc9132a.

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

Walkthrough

The PR extends circular-reference detection to JSX values by marking the .JSX tag as eligible for tracking in both the console object formatter and Jest pretty-printer. A new test case validates that Bun.inspect correctly formats React-like elements with circular references using [Circular] markers.

Changes

JSX Circular Reference Detection

Layer / File(s) Summary
JSX circular reference eligibility in formatters
src/jsc/ConsoleObject.zig, src/test_runner/pretty_format.zig
Both canHaveCircularReferences methods now include the .JSX tag, enabling circular-reference detection for JSX elements alongside existing support for arrays, objects, maps, and sets.
Test coverage for JSX circular references
test/js/bun/util/inspect.test.js
A test case constructs React-element-like objects with circular references through key, props.foo, and props.children (direct and array-wrapped), verifying Bun.inspect outputs include [Circular] markers.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: fixing a stack overflow when inspecting circular JSX elements, which is the core issue and primary change across all modified files.
Description check ✅ Passed The PR description follows the template with complete sections, including 'What' (the problem), 'Why' (the root cause), and 'Fix' (the solution). It provides clear context and verification details.
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

This PR may be a duplicate of:

  1. Fix stack overflow in Bun.inspect() on circular JSX elements #29709 - Same fix: adds .JSX to canHaveCircularReferences() in ConsoleObject.zig and pretty_format.zig to fix stack overflow when inspecting circular JSX elements
  2. Fix stack overflow in Bun.inspect for circular JSX elements #30126 - Same fix: adds .JSX to canHaveCircularReferences() to prevent stack overflow in Bun.inspect for circular JSX elements

🤖 Generated with Claude Code

@robobun

robobun commented May 14, 2026

Copy link
Copy Markdown
Collaborator Author

Duplicate of #29709.

@robobun robobun closed this May 14, 2026
@robobun robobun deleted the farm/fe541895/jsx-inspect-circular branch May 14, 2026 02:37

@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 that enables the existing circular-reference guard for the JSX formatter tag.

Extended reasoning...

Overview

Two one-line changes adding .JSX to canHaveCircularReferences() in src/jsc/ConsoleObject.zig and src/test_runner/pretty_format.zig, plus four regression tests in test/js/bun/util/inspect.test.js. The .JSX branch of printAs() recurses into key / props / children via this.format(), but was excluded from the comptime-gated visited-map check at the top of printAs(), so a self-referential React element exhausted the native stack. Adding the tag enables the same getOrPut / [Circular] short-circuit already used for .Array, .Object, .Map, .Set.

Security risks

None. This is purely additive to a debug/inspection code path — it turns a SIGSEGV into a [Circular] string. No new inputs are read, no new control flow is reachable, no auth/crypto/permissions involved.

Level of scrutiny

Low. The fix is mechanical and mirrors the existing pattern for every other recursive tag; the only behavior change is that the visited-map guard now fires for JSX values. The four new tests cover all recursion entry points in the .JSX arm (key, arbitrary prop, children as JSX, children as array).

Other factors

The inline note about .Event still being absent from pretty_format.zig's version is a pre-existing, contrived edge case (requires overriding a native getter with defineProperty) and is explicitly flagged as a non-blocking parity nit. It doesn't affect the correctness of this fix.

Comment on lines 328 to 330
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;
}

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.

🟣 While you're on this line: ConsoleObject.zig's canHaveCircularReferences() also includes .Event, but this version still doesn't. The .Event branch in printAs() here recurses via this.format() into a MessageEvent's data / ErrorEvent's error, so an event whose data points back to itself can still stack-overflow the Jest formatter — same root cause as the JSX fix. Pre-existing and contrived, but it's a one-token addition for parity.

Extended reasoning...

What

This PR adds .JSX to canHaveCircularReferences() in both ConsoleObject.zig and pretty_format.zig so that circular JSX elements get a visited-map guard. However, the two functions are still not in parity: ConsoleObject.zig:1139 lists .Function, .Array, .Object, .Map, .Set, .Error, .Class, .Event, .JSX, while pretty_format.zig:329 after this PR lists only .Array, .Object, .Map, .Set, .JSX. The missing one that matters is .Event — the .Event branch in pretty_format.zig's printAs() does recurse into user-controlled values, so it has the exact same stack-overflow exposure that this PR is fixing for .JSX. (.Function/.Class/.Error in pretty_format.zig do not recurse, so they don't need to be added — .Event is the only real gap.)

Code path

  1. MessageEvent, ErrorEvent, CloseEvent etc. are constructed with JSType(JSEventType) (0b11101111) per src/jsc/bindings/JSDOMWrapper.h, which maps to JSType.Event in Zig. In Tag.get(), this is distinct from .DOMWrapper (which returns .Private), so it falls through to the final switch and returns .tag = .Event.
  2. format() dispatches to printAs(.Event, ...). Because .Event is not in canHaveCircularReferences(), the if (comptime Format.canHaveCircularReferences()) block is skipped — no entry is inserted into this.map for the event.
  3. Inside the .Event => { ... } arm, for a MessageEvent the formatter does const data = (try value.fastGet(this.globalThis, .data)) orelse .js_undefined; and then try this.format(tag, ..., data, ...). For an ErrorEvent it does the same with .error. fastGet is a normal property lookup, so an own-property override is honored.
  4. If data (or error) is the event itself, Tag.get(data) returns .Event again, and step 2 re-enters printAs(.Event, value) with no visited-map check anywhere on the path.

Why nothing else catches it

The visited-map guard in printAs is gated entirely on comptime Format.canHaveCircularReferences(). Since .Event returns false there, the getOrPut / found_existing check never runs for the event value. The only other recursion limiter in this file is the per-type guard, and there is none for .Event. Note that indirect cycles through a plain object (e.g. ev.data = obj; obj.x = ev) are caught, because .Object is guarded and inserts obj into the map — only the direct Event→Event self-reference loops forever.

Step-by-step proof

const ev = new MessageEvent("message");
Object.defineProperty(ev, "data", { value: ev, configurable: true });
// In the Jest pretty-formatter (e.g. via expect(ev).toMatchSnapshot()):
// Tag.get(ev)            -> { tag: .Event }
// printAs(.Event, ev)    -> canHaveCircularReferences() == false, no map insert
//   fastGet(ev, .data)   -> ev
//   Tag.get(ev)          -> { tag: .Event }
//   format -> printAs(.Event, ev)   // again, no map insert
//     fastGet(ev, .data) -> ev      // ...
// Native stack overflow.

The Object.defineProperty override is the same class of "contrived but fuzzer-reachable" input as the JSX repro this PR fixes (and inspect.test.js already exercises Object.defineProperty on MessageEvent for the "deleted data" test, so it's a supported shape).

Impact and fix

Impact: a snapshot/diff of such an event in bun test crashes the process with a native stack overflow instead of printing [Circular]. It's pre-existing and requires deliberately overriding a native getter, so it shouldn't block this PR — but since the fix is literally adding or tag == .Event to the line already being edited, and ConsoleObject.zig already has it, it's worth doing here for parity.

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