Skip to content

Fix stack overflow when inspecting circular JSX elements#30021

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

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

Conversation

@robobun

@robobun robobun commented May 1, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Bun.inspect() / console.log() on a React element whose key, props, or children reference the element itself would recurse unboundedly and crash with a stack overflow (SIGSEGV).

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

The .JSX formatter tag was missing from canHaveCircularReferences(), so the visited-set [Circular] short-circuit and the stack_check.isSafeToRecurse() guard were skipped for JSX elements.

How did you verify your code works?

Added regression tests covering circular key, circular props, circular children, and repeated (non-circular) children to ensure those still print normally.

Found by Fuzzilli, fingerprint bb8715595a137556.

Bun.inspect() and console.log() on a React element whose key, props, or
children reference the element itself would recurse unboundedly and crash
with a stack overflow. Add .JSX to canHaveCircularReferences() so the
existing visited-set and stack guard apply, printing [Circular] instead.
@robobun

robobun commented May 1, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 5:23 PM PT - Apr 30th, 2026

@robobun, your commit 4a9bec5 has 1 failures in Build #49565 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 30021

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

bun-30021 --bun

@github-actions github-actions Bot added the claude label May 1, 2026
@coderabbitai

coderabbitai Bot commented May 1, 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: f8f5087c-fae0-45ab-b4fc-8eb577fc63d2

📥 Commits

Reviewing files that changed from the base of the PR and between 63043b6 and 4a9bec5.

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

Walkthrough

The circular-reference detection logic in the formatter is updated to treat React JSX elements as safely recurseable values, enabling consistent circular-reference tracking for JSX alongside other container types. Corresponding tests validate the behavior with self-referential JSX element structures.

Changes

Cohort / File(s) Summary
Circular-reference logic for JSX
src/bun.js/ConsoleObject.zig
Updated canHaveCircularReferences to classify JSX elements (.JSX tag) as recurseable/container-like values for visited-map tracking during formatting.
JSX circular reference tests
test/js/bun/util/inspect.test.js
Added test suite covering JSX circular-reference detection, including self-referential structures in key and props fields, and verification that non-circular repeated references are handled correctly.
🚥 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 crash when inspecting circular JSX elements, which is the primary issue addressed in this PR.
Description check ✅ Passed The description fully covers both required template sections with concrete details: explains what the PR does (stack overflow fix with example code), and how it was verified (regression tests added).
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.


Review rate limit: 0/5 reviews remaining, refill in 55 minutes and 37 seconds.

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

@github-actions

github-actions Bot commented May 1, 2026

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 to prevent stack overflow when inspecting circular JSX elements

🤖 Generated with Claude Code

@robobun

robobun commented May 1, 2026

Copy link
Copy Markdown
Collaborator Author

Duplicate of #29709 which covers this plus the same bug in pretty_format.zig and the non-object props case.

@robobun robobun closed this May 1, 2026
@robobun robobun deleted the farm/deb3abed/fix-jsx-circular-inspect branch May 1, 2026 00:22

@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 — the one-line fix and tests are correct for Bun.inspect; left a note about the parallel gap in pretty_format.zig which you may want to fold in here or do as a follow-up.

Extended reasoning...

Overview

This PR adds .JSX to the canHaveCircularReferences() switch in src/bun.js/ConsoleObject.zig so that the visited-set [Circular] guard and stack_check.isSafeToRecurse() fallback apply when formatting React elements. Four regression tests in test/js/bun/util/inspect.test.js cover circular key, circular prop, circular children, and a non-circular repeated-child case to guard against false positives.

Security risks

None. This is a defensive change to a debug/inspection formatter; it strictly adds a recursion guard. No auth, crypto, parsing, or untrusted-input handling is touched.

Level of scrutiny

Low. The production change is a single enum tag added to an existing switch arm whose semantics ("this tag's value can contain itself") are obviously true for JSX elements (objects with arbitrary key/props/children). The surrounding machinery (visited map insert/remove, stack check) is unchanged and already exercised by the other tags. The new tests verify both the crash fix and that the visited-set doesn't over-trigger on repeated-but-acyclic children.

Other factors

The bug-hunting system correctly flagged that the sister formatter JestPrettyFormat (src/bun.js/test/pretty_format.zig:328-330) has the same omission, so a circular JSX element reaching expect() diff output can still overflow. That is a pre-existing issue in a file this PR does not touch, not a defect in the change itself, so I'm treating it as a non-blocking follow-up rather than a reason to withhold approval. The fix as scoped — Bun.inspect / console.log — is complete and well-tested.

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 fix is needed in the sister formatter src/bun.js/test/pretty_format.zig — its canHaveCircularReferences() (lines 328-330) still omits .JSX, so a circular React element passed to a failing expect() matcher (e.g. expect(el).toBe(null)) still recurses unboundedly in DiffFormatterJestPrettyFormat and SIGSEGVs. Mirroring this one-line change there (and ideally also adding .Error/.Event/.Function/.Class for parity) would close the remaining hole.

Extended reasoning...

Summary

This PR fixes the circular-JSX stack overflow in Bun.inspect()/console.log() by adding .JSX to canHaveCircularReferences() in src/bun.js/ConsoleObject.zig. However, Bun has a near-identical second formatter — JestPrettyFormat in src/bun.js/test/pretty_format.zig — with its own copy of canHaveCircularReferences() that was not updated. That copy still only returns true for .Array, .Object, .Map, and .Set, so the exact crash this PR's title promises to fix is still reachable through expect() failure output.

Code path

JestPrettyFormat is invoked from user code via DiffFormatter (src/bun.js/test/diff_format.zig:39/48), which renders the "Expected/Received" diff for failing matchers like toBe, toEqual, toStrictEqual, toHaveProperty, toMatchSnapshot, etc. When the received value is a React element, Tag.get() returns .JSX and printAs(.JSX, ...) runs.

Inside the .JSX branch (pretty_format.zig:1477-1700):

  • line 1531 calls this.format(...) on the element's key
  • line 1580 calls this.format(...) on each prop value
  • lines 1643/1666 call this.format(...) on children

The visited-map [Circular] short-circuit at lines 876-888 is gated by if (comptime Format.canHaveCircularReferences()). Since .JSX returns false from canHaveCircularReferences() in this file, that guard is compiled out for the JSX path. Additionally, unlike ConsoleObject.zig, pretty_format.zig has no stack_check.isSafeToRecurse() fallback anywhere in the file, so there is no secondary depth limit either.

Step-by-step proof

const el = { $$typeof: Symbol.for('react.element'), type: 'div', props: {}, key: null };
el.key = el;
expect(el).toBe(null); // fails by reference
  1. toBe fails → DiffFormatter is asked to format received = el.
  2. diff_format.zig:39 calls JestPrettyFormat.format(..., el, ...).
  3. Tag.get(el) sees $$typeof === Symbol.for('react.element') → returns .JSX.
  4. format() enters the if (comptime Format.canHaveCircularReferences()) block — skipped, because for .JSX it is false in pretty_format.zig.
  5. printAs(.JSX, ...) reaches line 1531 and calls this.format(...) on el.key, which is el itself.
  6. Goto step 3. No visited-set entry was ever recorded, no stack guard exists → unbounded recursion → SIGSEGV.

The same applies if the cycle goes through props.foo = el or props.children = el.

Impact

Any test that asserts on a self-referencing React element (or any object graph that contains one) and fails will crash the test runner with a segfault instead of printing a diff. This is the exact crash class described in the PR title and description, just on the bun:test formatting path rather than the Bun.inspect path.

Fix

Mirror the change in src/bun.js/test/pretty_format.zig:329:

.Array, .Object, .Map, .Set, .JSX => true,

For full parity with ConsoleObject.zig it would also make sense to add .Error, .Event, .Function, .Class while you're there, since that file's canHaveCircularReferences() lags behind on those tags too.

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