Skip to content

Fix stack overflow in Bun.inspect for circular JSX elements - #30126

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

Fix stack overflow in Bun.inspect for circular JSX elements#30126
robobun wants to merge 1 commit into
mainfrom
farm/deb3abed/fix-jsx-circular-inspect

Conversation

@robobun

@robobun robobun commented May 2, 2026

Copy link
Copy Markdown
Collaborator

Inspecting a React element whose key, props, or children referenced the element itself crashed with a stack overflow.

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

The JSX tag was not included in can_have_circular_references(), so the visited-map and stack checks in print_as were skipped for JSX elements. Circular JSX elements now print [Circular] like other circular values.

Found by Fuzzilli (fingerprint bb8715595a137556).

What does this PR do?

Adds Tag::JSX to can_have_circular_references() in src/jsc/ConsoleObject.rs and src/runtime/test_runner/pretty_format.rs.

How did you verify your code works?

Regression test in test/js/bun/util/inspect.test.js spawns a subprocess that inspects three circular JSX shapes (via key, props.children, and an arbitrary prop) and asserts [Circular] output with exit code 0. Without the fix, the subprocess segfaults (exit 139) and the test fails.

@github-actions github-actions Bot added the claude label May 2, 2026
@robobun

robobun commented May 2, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 7:42 AM PT - May 22nd, 2026

@robobun, your commit d8afc4d has some failures in Build #56858 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 30126

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

bun-30126 --bun

@coderabbitai

coderabbitai Bot commented May 2, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds .JSX to circular-reference detection in console formatting and the pretty-print formatter, and adds a regression test that verifies Bun.inspect prints [Circular] for three different circular references on JSX-like objects.

Changes

JSX Circular Reference Detection

Layer / File(s) Summary
Core Implementation
src/bun.js/ConsoleObject.zig
Tag.canHaveCircularReferences now returns true for .JSX, enabling cycle detection for JSX-tagged values.
Formatter Update
src/bun.js/test/pretty_format.zig
JestPrettyFormat.Formatter.Tag.canHaveCircularReferences updated to include .JSX, aligning pretty-printer behavior with console logic.
Tests
test/js/bun/util/inspect.test.js
Added async regression test jsx with circular references does not stack overflow that spawns a Bun process, constructs three JSX-like cyclic objects (cycles via key, props.children, and props), calls Bun.inspect, and asserts stdout contains [Circular] and exit code 0.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main fix: addressing a stack overflow issue in Bun.inspect when handling circular JSX elements.
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.
Description check ✅ Passed The pull request description comprehensively covers the problem, root cause, fix, and verification approach, matching all required template sections.

✏️ 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

github-actions Bot commented May 2, 2026

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() for circular JSX elements by adding JSX to canHaveCircularReferences

🤖 Generated with Claude Code

Comment thread src/test_runner/pretty_format.zig
Comment thread src/bun.js/test/pretty_format.zig Outdated

@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 — minimal, targeted fix with regression tests; the remaining inline note is pre-existing and fine for a follow-up.

Extended reasoning...

Overview

Adds .JSX to canHaveCircularReferences() in both ConsoleObject.zig and pretty_format.zig (one-token change each) plus three regression tests in inspect.test.js. This enables the existing visited-map / stack-safety guard for JSX-tagged values so circular React-element-shaped objects print [Circular] instead of stack-overflowing.

Security risks

None. Pure formatter hardening — strictly reduces crash surface reachable from user JS.

Level of scrutiny

Low. Two one-line additions to an enum predicate, following an established pattern. I verified the .JSX printAs branches in both files only recurse into sub-values (type/key/props/children), never re-entering on the element itself, so there's no spurious-[Circular] hazard like the one that forced the .Event revert in cdfc31b.

Other factors

My two earlier review rounds were addressed: I suggested adding .Event, then flagged the resulting fall-through regression, and the author correctly reverted to .JSX-only and scoped .Event out as a follow-up. The new inline comment about props.getObject().? is explicitly pre-existing and not introduced here. CI noise on build #50297 is from known flakes per the retrigger commit messages. The duplicate-PR bot flagged #29709 — maintainers can dedupe, but that doesn't affect correctness of this change.

Comment thread src/jsc/ConsoleObject.zig Outdated

@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 — minimal and correct fix; the .Event regression was reverted and the remaining inline note is pre-existing.

Extended reasoning...

Overview

Adds .JSX to canHaveCircularReferences() in both ConsoleObject.zig and pretty_format.zig (one token each) so that the visited-map guard in printAs runs for JSX-tagged values, plus three regression tests in inspect.test.js covering circular key, props.children, and arbitrary props.

Security risks

None. This is a formatter-only change that enables an existing cycle-detection guard for one more tag. No new inputs, no auth/crypto/permissions surface.

Level of scrutiny

Low. The fix is the obvious one-line extension of an existing allowlist, and the failure mode it addresses (stack overflow on a fuzzer-crafted circular JSX object) is strictly improved. I verified that the .JSX branch in both files only recurses into key_value / property_value / children — never back into printAs on the same value with a different tag — so there is no spurious-[Circular] regression risk of the kind that affected the briefly-added .Event case.

Other factors

My earlier feedback loop on this PR is fully resolved: the .Event addition that caused a fall-through regression was reverted in cdfc31b, and the remaining inline comment about props.getObject().? is explicitly flagged as pre-existing (not introduced here) and can be handled separately. The new tests directly exercise the three recursion entry points. CI failures on cdfc31b are the known Zig-compiler-ICE / build-cache infra flakes being retriggered in subsequent commits, not related to this diff. A duplicate-PR bot flagged #29709 as covering the same fix; that's a merge-coordination concern for maintainers, not a correctness one.

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/js/bun/util/inspect.test.js`:
- Around line 339-345: The test currently pipes proc.stderr but never consumes
it and asserts exitCode before checking outputs; update the test to
await/collect stderr (e.g., const [stdout, stderr, exitCode] = await
Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]) or await
separately), filter out ASAN startup warning lines from stderr when bunEnv is
used (use a regex to remove lines like "WARNING: AddressSanitizer:"), assert
stdout and filtered stderr expectations first, and only then assert
expect(exitCode).toBe(0); reference the proc/stdout/stderr/exitCode variables
and bunEnv in your changes.
🪄 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: d1a10d80-8713-4dfa-b693-c713dd808ac6

📥 Commits

Reviewing files that changed from the base of the PR and between cdfc31b and 0ed925c.

📒 Files selected for processing (1)
  • test/js/bun/util/inspect.test.js

Comment thread test/js/bun/util/inspect.test.js Outdated

@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.

♻️ Duplicate comments (1)
test/js/bun/util/inspect.test.js (1)

319-344: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Consume stderr and assert exitCode last.

proc.stderr is still piped but never read, and the exit code is checked before any output validation. That can hide failures and can deadlock if the child writes to stderr. Please collect stderr, filter the known ASAN startup warning when using bunEnv, verify stdout/stderr first, and only then assert exitCode.

Suggested fix
-  const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
-  expect(exitCode).toBe(0);
-  expect(stdout).toBe("<div key=[Circular] />\n" + "<div>\n  [Circular]\n</div>\n" + "<div foo=[Circular] />\n");
+  const [stdout, stderr, exitCode] = await Promise.all([
+    proc.stdout.text(),
+    proc.stderr.text(),
+    proc.exited,
+  ]);
+  const stderrLines = stderr
+    .split("\n")
+    .filter(line => line.length > 0)
+    .filter(line => !line.startsWith("WARNING: ASAN interferes"));
+  expect(stderrLines).toEqual([]);
+  expect(stdout).toBe("<div key=[Circular] />\n" + "<div>\n  [Circular]\n</div>\n" + "<div foo=[Circular] />\n");
+  expect(exitCode).toBe(0);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/js/bun/util/inspect.test.js` around lines 319 - 344, The test spawns
proc with stderr piped but never reads it and asserts exitCode before verifying
outputs; modify the test to concurrently read proc.stdout.text() and
proc.stderr.text(), filter known ASAN startup warning from stderr when bunEnv is
used, then assert stdout and (filtered) stderr contents first, and only after
validating outputs assert exitCode; refer to symbols proc, stdout, stderr,
exitCode, Bun.inspect and bunEnv to locate and update the spawn result handling
and expectations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@test/js/bun/util/inspect.test.js`:
- Around line 319-344: The test spawns proc with stderr piped but never reads it
and asserts exitCode before verifying outputs; modify the test to concurrently
read proc.stdout.text() and proc.stderr.text(), filter known ASAN startup
warning from stderr when bunEnv is used, then assert stdout and (filtered)
stderr contents first, and only after validating outputs assert exitCode; refer
to symbols proc, stdout, stderr, exitCode, Bun.inspect and bunEnv to locate and
update the spawn result handling and expectations.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f69d7049-4c9f-4256-9b5a-75b52829676c

📥 Commits

Reviewing files that changed from the base of the PR and between 0ed925c and a53ddd4.

📒 Files selected for processing (1)
  • test/js/bun/util/inspect.test.js

Comment thread test/js/bun/util/inspect.test.js Outdated

@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 — adding .JSX to both canHaveCircularReferences() switches is the minimal correct fix, and the test locks in exact output. My remaining inline notes are pre-existing / nit-level and non-blocking.

Extended reasoning...

Overview

Two one-token additions: .JSX is appended to the canHaveCircularReferences() match arms in src/bun.js/ConsoleObject.zig:1139 and src/bun.js/test/pretty_format.zig:329, plus a subprocess regression test in test/js/bun/util/inspect.test.js covering circular key, props.children, and arbitrary-prop cycles. The .Event addition I'd suggested earlier was tried and then correctly reverted (cdfc31b) after I flagged the spurious-[Circular] regression on the fall-through path, so the diff is back to the minimal JSX-only fix.

Security risks

None. This only changes which formatter tags participate in the visited-map cycle guard during value inspection. No auth, crypto, permissions, or untrusted-input parsing is touched; the change strictly adds a safety check.

Level of scrutiny

Low. Adding a tag to an existing enum switch that already handles .Array/.Object/.Map/.Set identically is mechanical. I verified the .JSX printAs branch in both files never re-enters printAs on the same value (unlike .Event's fall-through), so the remove_before_recurse concern doesn't apply here — confirmed by the test's exact-output assertion (<div key=[Circular] />, not bare [Circular]).

Other factors

  • The author was responsive across the review cycle: applied the .Event suggestion, then reverted when the regression was identified, scoping this PR back to the JSX crash only.
  • My two unresolved inline comments are (1) a pre-existing props.getObject().? unwrap on hand-crafted JSX-like objects — explicitly out of scope and not introduced here — and (2) a nit on test assertion ordering (stderr/stdout before exitCode) that affects only failure diagnostics, not correctness. Neither blocks.
  • CI failures on the latest build are infra flakes (Zig compiler ICE in std/process/Child.zig, Azure agent provisioning) per the retrigger commit messages, unrelated to this diff.
  • A duplicate-PR bot flagged #29709 as covering the same fix; a human can dedupe at merge time, but that doesn't affect correctness of this change.

@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 test now follows the spawn conventions and the .JSX-only scope is correct.

Extended reasoning...

Overview

Adds .JSX to canHaveCircularReferences() in both src/bun.js/ConsoleObject.zig and src/bun.js/test/pretty_format.zig, plus a subprocess regression test in test/js/bun/util/inspect.test.js. This is a one-token addition to two switch/or-chain expressions that gates the existing visited-map / [Circular] machinery, fixing a Fuzzilli-found stack overflow when a hand-crafted React element references itself via key/props/children.

Security risks

None. This only affects the formatter's cycle-detection set; no parsing, auth, or external input handling is touched. The change strictly narrows a crash surface.

Level of scrutiny

Low-to-moderate. The functional diff is two characters of enum membership in well-understood cycle-detection logic. I verified that the .JSX arm of printAs in both files never re-enters printAs on the same value (only on key_value / property_value / children), so there is no spurious-[Circular] hazard of the kind that forced the .Event revert earlier in this PR's history. The existing inspect.test.js JSX cases (jsx with two elements, jsx with fragment, inline-snapshot JSX in the inspect test) cover the non-circular path and would catch any regression there.

Other factors

All earlier review feedback has been resolved: the .Event addition that regressed CloseEvent/CustomEvent snapshots was reverted in cdfc31b; the test was updated in 61823f2 to read stderr, assert it empty, assert stdout, then exitCode — matching CLAUDE.md conventions. The remaining open inline note about props.getObject().? is explicitly pre-existing and out of scope. The three CI failures (s3-storage-class, ssg-pages-router, dev-and-prod) are unrelated to inspect/formatting. github-actions flagged #29709 as a possible duplicate; that's a merge-coordination question, not a correctness concern with this diff.

@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 — minimal, targeted fix with a regression test; all earlier review feedback addressed.

Extended reasoning...

Overview

Two one-line Zig changes add .JSX to canHaveCircularReferences() in both src/bun.js/ConsoleObject.zig and src/bun.js/test/pretty_format.zig, so the existing visited-map / [Circular] guard in printAs now applies to JSX elements. A new subprocess test in test/js/bun/util/inspect.test.js exercises three circular shapes (via key, props.children, and a custom prop) and asserts exact output.

Security risks

None. This is output-formatting only; it tightens recursion handling rather than loosening anything. No auth, crypto, permissions, or untrusted-input parsing is touched.

Level of scrutiny

Low. The change is purely additive to an existing enum-tag allowlist that gates cycle detection. I verified that neither formatter's .JSX branch ever re-enters printAs on the same value (it only recurses into key/props/children sub-values), so there is no spurious-[Circular] regression of the kind that the briefly-attempted .Event addition caused — that change was correctly reverted in cdfc31b and is out of scope here.

Other factors

  • All four prior inline review threads (mine and CodeRabbit's) are resolved; the test now reads stderr, asserts it empty, then stdout, then exitCode, matching repo conventions.
  • The remaining open inline comment about props.getObject().? is explicitly pre-existing and flagged for a follow-up, not a blocker for this PR.
  • The robobun build-failure banner on de0a61d reflects CI infrastructure flakes (per the "systemic across builds" / "container git state stuck" retrigger commit messages), not a problem with this two-line code change.
  • A duplicate-PR bot flagged #29709 as covering the same fix; that's a merge-coordination question for maintainers, not a correctness concern with this diff.

@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 — minimal, correct crash fix with a regression test; the ASAN-stderr nit below is non-blocking.

Extended reasoning...

Overview

Two one-token additions: .JSX is added to canHaveCircularReferences() in both src/bun.js/ConsoleObject.zig and src/bun.js/test/pretty_format.zig, plus a subprocess regression test in test/js/bun/util/inspect.test.js covering circular references via key, props.children, and an arbitrary prop.

Correctness

The .JSX arm in both formatters recursively calls this.format() on key, prop values, and children, so it clearly needs the visited-map / stack guard. I traced both .JSX printAs branches and confirmed neither re-enters printAs on the same value with a different tag (the pattern that caused the spurious [Circular] when .Event was briefly added and then reverted in cdfc31b). Sibling repeats (e.g. [el, el]) are unaffected because the defer remove runs between iterations. Existing non-circular JSX tests in this file continue to exercise the unchanged happy path.

Security risks

None. This is output formatting only; the change strictly adds a recursion guard to a path that previously had none.

Level of scrutiny

Low. Mechanical extension of an existing enum-set check, mirrors the established pattern for .Array/.Object/.Map/.Set, and is covered by a targeted regression test for a Fuzzilli-found segfault. No CODEOWNERS apply to the touched files.

Other factors

  • Earlier review feedback (read/assert stderr, assert exitCode last) was addressed in 61823f2; those threads are resolved.
  • The remaining inline nit about filtering the ASAN startup banner from stderr is a possible CI-only flake on the linux-asan job; test/CLAUDE.md's canonical spawn pattern uses the unfiltered expect(stderr).toBe(""), so this is non-blocking and CI will surface it if real.
  • The pre-existing props.getObject().? crash I flagged earlier is out of scope per the author's reply and predates this PR.
  • A duplicate-PR bot flagged #29709; that's a process question for maintainers, not a correctness concern here.

Comment on lines +341 to +342
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");

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.

🟡 🟡 Nit: CodeRabbit's suggestion you partially applied in 61823f2 also filtered the WARNING: ASAN interferes with JSC signal handlers… banner that ASAN builds write to stderr at startup (see expectations.txt:15 and the 17 sibling subprocess tests — e.g. string_decoder.test.js:303-307, setTimeout.test.js:429 — that strip it before asserting empty stderr). With a raw expect(stderr).toBe("") this test may go red on the linux-asan job; the one-line filter is cheap insurance:

expect(stderr.split("\n").filter(l => l && !l.startsWith("WARNING: ASAN interferes"))).toEqual([]);
Extended reasoning...

What this is

Commit 61823f2 addressed the earlier review by reading proc.stderr.text() and reordering the assertions, but dropped one piece of CodeRabbit's committable suggestion (inline comment 3177287793): the filter for the ASAN startup banner. On linux ASAN builds, the bun-asan binary may write WARNING: ASAN interferes with JSC signal handlers; … to stderr during JSC initialization (the comment at string_decoder.test.js:303 attributes it to WebKit's Options.cpp, which is not in this checkout). bunEnv (test/harness.ts:72-74) sets ASAN_OPTIONS=allow_user_segv_handler=1:disable_coredump=0, which does not suppress that banner. With the new expect(stderr).toBe("") at line 342, the linux-asan job would see received: "WARNING: ASAN interferes with JSC signal handlers; …\n" and fail this test.

Why existing safeguards don't prevent it

There is no harness helper that strips this line — every subprocess test that wants to assert empty stderr under ASAN does it inline. Seventeen test files in the repo currently filter exactly this prefix (e.g. test/js/node/string_decoder/string-decoder.test.js:305-308, test/js/web/timers/setTimeout.test.js:429, test/js/bun/resolve/bun-main-entry-point.test.ts:16, test/js/node/buffer-copy-fill-detach.test.ts:23, test/js/bun/udp/udp_socket.test.ts:39, test/regression/issue/29519.test.ts), several with a verbatim comment that "ASAN builds unconditionally print … to stderr at startup". And test/expectations.txt:15 skips child_process.test.ts on ASAN with the note # Unexpected identifier "WARNING" — independent evidence that the banner is real on the ASAN builder, not just a CodeRabbit-learned cargo-cult.

Step-by-step proof

  1. linux-asan job spawns bunExe() (= bun-asan) with -e <script> and env: bunEnv.
  2. During JSC Options::initialize(), the ASAN-aware path emits WARNING: ASAN interferes with JSC signal handlers; …\n to stderr before any user code runs.
  3. The script runs, writes the three <div …> lines to stdout, exits 0.
  4. proc.stderr.text() resolves to "WARNING: ASAN interferes with JSC signal handlers; …\n".
  5. expect(stderr).toBe("") fails; the stdout/exitCode assertions never run.

Addressing the refutation

One reviewer points out that the literal fprintf is not in src/ (only the comment at ZigGlobalObject.cpp:288), that test/CLAUDE.md's canonical spawn pattern is the unfiltered expect(stderr).toBe(""), and that ~80 other tests use that unfiltered form without being listed as ASAN failures in expectations.txt. That's all accurate, and it's why this is a nit rather than a guaranteed CI break: either the banner is gated on something not every test hits, or the linux-asan job tolerates/curates these. But the expectations.txt skip (Unexpected identifier "WARNING") and the seventeen explicit filters — including one merged the day before this PR in #30058 — are direct evidence that the banner does surface on the ASAN builder for at least some bunExe() spawns. Since CodeRabbit already proposed the filter on this exact diff and the author applied two-thirds of that suggestion, finishing it is the path of least resistance.

Fix

const stderrLines = stderr.split("\n").filter(l => l && !l.startsWith("WARNING: ASAN interferes"));
expect(stderrLines).toEqual([]);

(or the two-.filter() form from CodeRabbit's committable suggestion). Test-only, no behavior change on non-ASAN builds.

@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 — minimal, correct fix with regression coverage.

Extended reasoning...

Overview

Adds .JSX to canHaveCircularReferences() in both ConsoleObject.zig and pretty_format.zig, plus a subprocess regression test in inspect.test.js covering three circular shapes (key, props.children, arbitrary prop). The earlier .Event addition that regressed the fall-through path was reverted in cdfc31b, and the test was updated in 61823f2 to follow the repo's stderr/stdout/exitCode assertion ordering. Commits since then are CI retriggers only.

Security risks

None. This is purely output-formatting logic; the change strictly adds a cycle guard (visited-map + stack-overflow check) to a code path that previously lacked one. No new untrusted input parsing, no auth/crypto/permissions surface.

Level of scrutiny

Low. The diff is a one-token addition to two existing switch/boolean expressions. I verified that neither .JSX printAs branch calls this.printAs(..., value, ...) on the same value (only on key/props/children sub-values), so the visited-map insertion cannot produce a spurious [Circular] the way the reverted .Event change did. Existing JSX inspect tests in this file (jsx with two elements, jsx with fragment, the inline <div> cases in the inspect test) cover the non-circular path against regression.

Other factors

All inline review threads are resolved. The two remaining notes from prior runs are explicitly out of scope: the pre-existing props.getObject().? unwrap is unchanged by this PR, and the ASAN stderr-filter suggestion is a self-acknowledged nit (multiple CI retriggers since 61823f2 indicate the test is passing as written). The duplicate-PR bot flagged #29709, but that's a routing concern for maintainers, not a correctness issue with this diff.

@robobun
robobun force-pushed the farm/deb3abed/fix-jsx-circular-inspect branch from a6e7fda to 3659e59 Compare May 3, 2026 03:26

@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 — one-token addition to the existing circular-ref guard in both formatters, with a subprocess regression test; the open ASAN-stderr note is a non-blocking nit.

Extended reasoning...

Overview

The PR adds .JSX to canHaveCircularReferences() in both src/bun.js/ConsoleObject.zig and src/bun.js/test/pretty_format.zig so that the existing visited-map guard in printAs runs for JSX-tagged values, fixing a fuzzer-found stack overflow when a hand-crafted React element references itself via key, props, or children. A subprocess regression test in test/js/bun/util/inspect.test.js exercises all three cycle shapes and asserts [Circular] output with exit code 0.

Security risks

None. This is a defensive change that removes a user-JS-reachable segfault. It does not touch auth, crypto, permissions, or any trust boundary; it only gates an already-existing cycle-detection map on one additional enum variant.

Level of scrutiny

Low. The functional change is literally one token per file, appended to an existing allow-list that already covers .Array, .Object, .Map, .Set, etc. I verified that the .JSX arm of printAs in both formatters never calls this.printAs(..., value, ...) on the same value (it only recurses on type, key, children, and individual prop values), so there is no spurious-[Circular] hazard of the kind that bit the briefly-attempted .Event addition — that experiment was correctly reverted in cdfc31b after review caught the fall-through regression.

Other factors

All earlier inline review threads are resolved: the .Event regression was reverted, the test was updated to read/assert stderr and order exitCode last per repo conventions. The one open thread is my own nit about filtering the ASAN startup banner before expect(stderr).toBe("") — explicitly framed as cheap insurance, not a blocker, and CI will surface it if it actually bites. The pre-existing props.getObject().? unwrap and the duplicate-PR notice (#29709) are both informational and out of scope for this fix's correctness.

@robobun
robobun force-pushed the farm/deb3abed/fix-jsx-circular-inspect branch 2 times, most recently from ec40846 to a7a3cfd Compare May 3, 2026 10:08

@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 — one-token addition to two canHaveCircularReferences() lists with a regression test; CI is green modulo an unrelated http flake, so my earlier ASAN-stderr nit is non-blocking.

Extended reasoning...

Overview

Adds .JSX to Tag.canHaveCircularReferences() in both src/bun.js/ConsoleObject.zig:1139 and src/bun.js/test/pretty_format.zig:329, so the visited-map / [Circular] guard at the top of printAs now fires for JSX-tagged values. A subprocess regression test in test/js/bun/util/inspect.test.js exercises three circular shapes (via key, props.children, and a non-children prop) and asserts the exact output plus clean exit.

Security risks

None. This is a formatter-only change that turns a stack-overflow crash (Fuzzilli fingerprint bb8715595a137556) into a bounded [Circular] print. No new inputs are accepted, no auth/crypto/permissions code is touched, and the test spawns bunExe() with a fixed inline script.

Level of scrutiny

Low. The Zig change is a single enum tag appended to two existing match lists, mirroring the established pattern for .Array/.Object/.Map/.Set. I verified the .JSX arm of printAs in both files never calls this.printAs(..., value, ...) on the same value (it only recurses into key/props/children sub-values), so there is no spurious-[Circular] hazard like the one I flagged when .Event was briefly added — that experiment was reverted in cdfc31b and the diff is back to the minimal JSX-only fix.

Other factors

All earlier review threads are resolved: the .Event regression I flagged was reverted, and the test was updated to read/assert stderr and check exitCode last per repo conventions. My two remaining inline notes are explicitly non-blocking — one is a pre-existing props.getObject().? crash on the same path (out of scope per the author), and the other is an ASAN-banner stderr filter nit that Build #50536 shows did not actually fail on the ASAN job. No CODEOWNERS cover these paths. github-actions flagged a possible duplicate (#29709), which is a process question rather than a correctness one.

@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 — one-token addition to an existing cycle guard, verified by the new regression test; remaining inline notes are pre-existing/nit-level.

Extended reasoning...

Overview

Adds .JSX to Tag.canHaveCircularReferences() in both src/bun.js/ConsoleObject.zig and src/bun.js/test/pretty_format.zig, plus a subprocess regression test in test/js/bun/util/inspect.test.js. The Zig change is a single enum value appended to an existing switch arm in each file; everything else (the visited-map insert, [Circular] write, and defer remove) is pre-existing machinery that now applies to the JSX tag.

Security risks

None. This is output-formatting code; the change strictly enables an existing safety guard (visited-map cycle detection) for one more tag. No new inputs are parsed, no allocations change, no auth/crypto/permissions surface.

Level of scrutiny

Low. The fix is mechanical: .JSX recursively formats key, props.*, and children via this.format(), so it belongs in the same set as .Array/.Object/.Map/.Set. I verified that the .JSX branch in both formatters never calls printAs(..., value, ...) on the same value (only on child values), so there is no spurious-[Circular] hazard like the one that forced the .Event revert in cdfc31b. The existing JSX tests in inspect.test.js (jsx with two elements, jsx with anon component, jsx with fragment, plus the inline JSX cases in the inspect test) cover the non-circular path and would catch any regression there.

Other factors

  • Build #50594 failures are all known systemic flakes (s3-storage-class, hot.test, http-emit-close, dev-and-prod) that this branch has been retriggering for; inspect.test.js itself passed on every platform.
  • The earlier .Event-in-pretty_format.zig regression I flagged was reverted by the author and is now out of scope; the diff is back to .JSX only.
  • My two remaining inline notes are explicitly pre-existing (props.getObject().? panic on hand-crafted non-object props) and nit (ASAN stderr-banner filter). Neither blocks: the first predates this PR, and the second did not surface as a CI failure on the latest build.
  • A duplicate PR (#29709) was flagged by the bot; that's a process question for maintainers and doesn't affect this PR's correctness.

@Jarred-Sumner
Jarred-Sumner force-pushed the farm/deb3abed/fix-jsx-circular-inspect branch from 7e609a1 to 484a3fd Compare May 4, 2026 10:24

@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 two open inline notes (pre-existing props.getObject().? and the ASAN stderr filter) are both non-blocking nits.

Extended reasoning...

Overview

This PR adds .JSX to canHaveCircularReferences() in both src/jsc/ConsoleObject.zig and src/test_runner/pretty_format.zig, plus a subprocess regression test in test/js/bun/util/inspect.test.js. The actual source change is two enum-list additions — about as minimal as a fix gets.

Correctness

Unlike .Event (which the author tried and correctly reverted in cdfc31b after I flagged the spurious-[Circular] regression), the .JSX branch in both formatters never re-enters printAs on the same value with a different tag — it only recurses into key, prop values, and children, which are distinct JSValues (or genuinely circular). So adding .JSX to the visited-map gate cannot produce a false [Circular]. Verified against both printAs(.JSX, …) implementations.

Security risks

None. This is output-formatting code; the change strictly tightens behavior (turns a stack overflow into a bounded [Circular] print). No new attack surface.

Level of scrutiny

Low. Fuzzer-found crash fix, two-line source delta, well-understood mechanism (the same visited-map guard already used for .Array/.Object/.Map/.Set), with a regression test that exercises three distinct cycle shapes (via key, props.children, and an arbitrary prop).

Other factors

  • All four resolved review threads were addressed correctly (assertion ordering, stderr draining, the .Event add+revert).
  • Two inline comments remain open but are explicitly non-blocking: the pre-existing props.getObject().? unwrap (flagged as a follow-up for the same fuzzer family) and a 🟡 nit about filtering the ASAN startup banner from stderr — CI will surface the latter if it's actually load-bearing, and ~80 other tests use the same unfiltered expect(stderr).toBe("").
  • The current ❌ on Build #50983 is a CI-wide agent outage per the retrigger commit message, not a test failure.
  • A possible duplicate (#29709) was flagged by the dedup bot; that's a merge-coordination concern, not a correctness one.

@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 — one-line additions to canHaveCircularReferences() in both formatters, with a subprocess regression test. The remaining ASAN-stderr filter is a nit; CI will surface it if it matters.

Extended reasoning...

Overview

This PR adds .JSX to Tag.canHaveCircularReferences() in both src/jsc/ConsoleObject.zig and src/test_runner/pretty_format.zig, so the visited-map cycle guard at the top of printAs runs for JSX-tagged values. Without it, a hand-crafted React element whose key/props/children referenced itself recursed forever and segfaulted (Fuzzilli fingerprint bb8715595a137556). A subprocess regression test in test/js/bun/util/inspect.test.js exercises all three self-reference shapes and asserts [Circular] in the output.

Security risks

None. This is a crash-hardening fix in the inspect/pretty-print path; it does not touch auth, IO, permissions, or parsing of untrusted input beyond what Bun.inspect already handles. The change strictly adds a guard.

Level of scrutiny

Low. The diff is two one-token edits to existing boolean predicates plus a test. Unlike the briefly-attempted .Event addition (reverted in cdfc31b after I flagged the spurious-[Circular] fall-through), the .JSX branch in both formatters never re-enters printAs on the same value as a different tag — it only recurses into key/props/children, which are distinct JSValues — so there is no remove_before_recurse hazard here.

Other factors

All earlier review threads are resolved: the .Event regression was reverted, and the test now reads/asserts stderr and orders exitCode last per CLAUDE.md. My one open inline comment (filtering the WARNING: ASAN interferes… banner before expect(stderr).toBe("")) is explicitly a nit — test/CLAUDE.md's canonical pattern is the unfiltered form, ~80 sibling tests use it, and the linux-asan job will tell us definitively. The pre-existing props.getObject().? crash I noted is out of scope (author already declined to widen scope for .Event); it predates this PR. The five most recent commits are pure CI retriggers with no code changes since I last reviewed.

@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 two one-line .JSX additions to canHaveCircularReferences() are correct and the regression test covers all three cycle paths; remaining inline notes are pre-existing/nit-level and non-blocking.

Extended reasoning...

Overview

This PR adds .JSX to canHaveCircularReferences() in both src/jsc/ConsoleObject.zig and src/test_runner/pretty_format.zig, plus a subprocess regression test in test/js/bun/util/inspect.test.js. The Zig changes are each a single enum value appended to an existing switch/boolean expression — purely additive, gating the visited-map cycle guard that already exists at the top of printAs. I verified neither .JSX arm re-enters printAs on the same value (only on key/props/children sub-values), so there is no spurious-[Circular] risk of the kind that forced the .Event revert in cdfc31b.

Security risks

None. This is output formatting for Bun.inspect/console.log/snapshot serialization. The change strictly adds a guard (visited-map insert + [Circular] short-circuit) to a path that previously had none, converting a user-JS-reachable stack overflow into a safe string. No auth, crypto, permissions, or untrusted-input parsing involved.

Level of scrutiny

Low. The fix is mechanical and mirrors the existing pattern for .Array/.Object/.Map/.Set in the same function. The earlier review cycle already caught and reverted the one risky over-extension (adding .Event to pretty_format.zig without the remove_before_recurse plumbing). What remains is the minimal, obviously-correct version.

Other factors

  • The PR has been through several review rounds; the test was reworked per feedback to read/assert stderr and order assertions per repo convention.
  • My two open inline comments are explicitly non-blocking: the props.getObject().? note is pre-existing (not introduced here), and the ASAN stderr filter is a nit I flagged as "not a guaranteed CI break" — ~80 sibling tests use the same unfiltered expect(stderr).toBe("") without issue.
  • Current CI failures (build #51406) are infrastructure: a Windows build-cpp agent-creation failure and the known-flaky test-http-should-emit-close-when-connection-is-aborted.ts timeout — neither related to this diff (recent commits are all "ci: retrigger" for infra outages).
  • A duplicate PR (#29709) implements the identical fix, corroborating this is the right shape.

@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 — minimal, correct fix. The two open inline comments (pre-existing props.getObject().? crash and the ASAN stderr-filter nit) are non-blocking and fine as follow-ups.

Extended reasoning...

Overview

Adds .JSX to canHaveCircularReferences() in both src/jsc/ConsoleObject.zig and src/test_runner/pretty_format.zig (one token each), plus a subprocess regression test in test/js/bun/util/inspect.test.js covering circular refs via key, props.children, and an arbitrary prop. This enables the existing visited-map / [Circular] guard for JSX elements, fixing a Fuzzilli-found stack overflow.

Security risks

None. Pure formatter/inspect output change; no auth, crypto, parsing, or untrusted-input handling beyond what already exists. The change strictly adds a safety check.

Level of scrutiny

Low. The diff is two enum-predicate additions following the established pattern for .Array/.Object/.Map/.Set. I verified the .JSX arm of printAs in both files never re-enters printAs on the same value (it only recurses into type/key/children/prop values), so there is no spurious-[Circular] regression of the kind that adding .Event caused (and which was correctly reverted in cdfc31b).

Other factors

All earlier blocking review feedback was addressed: the .Event over-reach was reverted, and the test was updated to drain stderr and assert stdout/stderr before the exit code per repo conventions. The two unresolved inline comments are my own and explicitly non-blocking — one is a pre-existing crash on the same code path (props.getObject().? with non-object props), the other is an optional ASAN stderr-banner filter; neither is introduced by this PR. Recent commits are CI infrastructure retriggers, not code changes. A duplicate-PR bot flagged #29709 covering the same fix; that's a merge-coordination concern, not a correctness one.

@robobun
robobun force-pushed the farm/deb3abed/fix-jsx-circular-inspect branch from 454bb69 to a2535f9 Compare May 6, 2026 05:13

@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 — minimal, correct fix; remaining inline comments are non-blocking (pre-existing / test nit).

Extended reasoning...

Overview

Two one-line additions: .JSX is added to canHaveCircularReferences() in both src/jsc/ConsoleObject.zig and src/test_runner/pretty_format.zig, plus a subprocess regression test in test/js/bun/util/inspect.test.js. This enables the existing visited-map guard at the top of printAs for JSX-tagged values, turning a fuzzer-found stack overflow into a [Circular] print.

Security risks

None. This is output-formatting code; the change strictly adds a cycle check to a path that previously had none. No new attack surface, no auth/crypto/permissions involvement.

Level of scrutiny

Low. The mechanism (canHaveCircularReferences() gating a getOrPut/[Circular] guard) is established and already covers eight other tags in ConsoleObject.zig. I verified the .JSX branch in both formatters never re-dispatches printAs on the same value (only on type/key/props/children sub-values), so there is no spurious-[Circular] risk of the kind that forced the .Event revert earlier in this PR's history.

Other factors

All blocking feedback from earlier rounds has been addressed: the .Event regression I flagged was reverted in cdfc31b, and the test now drains stderr and asserts stdout before exitCode per repo conventions. The two unresolved inline comments are mine and explicitly non-blocking — one is a pre-existing props.getObject().? crash on the same path (out of scope, noted for follow-up), the other is a test nit about filtering the ASAN startup banner that CI will surface if it actually matters. Commits since my last review are CI retriggers only; the diff is unchanged. The bug hunting system found no issues this round.

@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 two-line fix is correct and the remaining open items are non-blocking nits / pre-existing issues.

Extended reasoning...

Overview

Adds .JSX to canHaveCircularReferences() in both src/jsc/ConsoleObject.zig and src/test_runner/pretty_format.zig, plus a subprocess regression test in test/js/bun/util/inspect.test.js. The Zig change is two enum-list additions; the test exercises three circular-JSX shapes (via key, props.children, and a custom prop) and asserts [Circular] output with exit code 0.

Security risks

None. This only widens the set of tags for which the existing visited-map / stack-depth guard runs in the inspect/snapshot formatters. No new I/O, parsing, auth, or untrusted-input surface.

Level of scrutiny

Low. The fix is the minimal, obvious change for a fuzzer-found stack overflow: include .JSX in the comptime predicate that gates the cycle check. I verified the .JSX printAs branch in both files never calls printAs(..., value, ...) on the same value (it only recurses on key, prop values, and children), so there is no spurious-[Circular] regression of the kind that forced reverting the .Event addition earlier in this PR's review.

Other factors

This PR has already been through several review rounds. The substantive feedback (the .Event regression, test assertion ordering / draining stderr) was addressed; the two open inline comments are (1) a pre-existing props.getObject().? crash on hand-crafted JSX that predates this PR, and (2) a test-ergonomics nit about filtering the ASAN stderr banner — neither blocks. Recent commits are purely CI retriggers for infrastructure outages with no code changes. A duplicate PR (#29709) independently arrived at the same fix.

@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 — two-line fix adding Tag::JSX to circular-reference detection, with a passing regression test; the other 10 .rs files are pure rustfmt whitespace from autofix.ci.

Extended reasoning...

Overview

The substantive change is two one-line additions: Tag::JSX is added to can_have_circular_references() in src/jsc/ConsoleObject.rs:2084 and src/runtime/test_runner/pretty_format.rs:530. This gates the visited-map / depth check at the top of print_as, so a JSX-like object whose key/props/children references itself now prints [Circular] instead of recursing until stack overflow. A regression test in inspect.test.js spawns a subprocess and asserts the exact output for three circular shapes. The remaining 10 .rs files (crash_handler, errno, tracy, Arguments, run_command, upgrade_command, jsc_hooks, ChromeProcess, spawn/process, spawn_sys/spawn_process) are 100% rustfmt reflow of long #[cfg(any(...))] / #[cfg_attr(...)] attributes onto multiple lines, applied by autofix.ci — zero semantic change.

Security risks

None. This is output formatting; no auth, crypto, fs, or network surface is touched. The fix removes a user-JS-reachable crash (Fuzzilli fingerprint bb8715595a137556).

Level of scrutiny

Low. Adding a tag to a matches! set that already contains Object/Array/Map/Set/etc. is the canonical fix for this bug class. I verified the .JSX arm dispatches to a dedicated print_jsx (ConsoleObject.rs:3617) and never re-enters print_as on the same value with a different tag, so it cannot hit the spurious-[Circular] regression that the briefly-added .Event did (now reverted per cdfc31b). The regression test's exact-string assertions (<div key=[Circular] /> etc.) independently confirm no false positives.

Other factors

All earlier review threads are resolved: the .Event add/revert cycle landed cleanly back at JSX-only scope; the test was updated to read/assert stderr and order exitCode last per repo conventions. My two remaining inline notes are explicitly non-blocking — one is a 🟣 pre-existing props.getObject() unwrap unrelated to this diff, and the 🟡 ASAN-banner filter nit was effectively mooted by CI (Build #55037 passed inspect.test.js on all platforms; the lone failure is an unrelated worker_threads SIGTRAP on macOS aarch64). A bot flagged #29709 as a possible duplicate, which is a merge-coordination concern rather than a correctness one.

When a React element's key, props, or children referenced the element
itself, Bun.inspect would recurse infinitely and crash with a stack
overflow. The JSX tag was not included in the set of tags that can have
circular references, so the visited-map check was skipped.

Add Tag::JSX to can_have_circular_references() in both the console
formatter and the jest snapshot formatter.
@robobun
robobun force-pushed the farm/deb3abed/fix-jsx-circular-inspect branch from fd69ece to d8afc4d Compare May 22, 2026 13:04

@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 — one enum variant added to two matches! arms to enable existing cycle detection for JSX, with a solid regression test.

Extended reasoning...

Overview

The PR adds Tag::JSX to can_have_circular_references() in src/jsc/ConsoleObject.rs (one line in a matches! arm) and src/runtime/test_runner/pretty_format.rs (same), plus a subprocess regression test in test/js/bun/util/inspect.test.js covering three circular JSX shapes (via key, props.children, and an arbitrary prop).

Correctness

I traced both print_jsx / Tag::JSX print paths: every recursive self.format(...) call operates on a sub-value (key_value, property_value, children, child) — never on value itself. So unlike the .Event fall-through case that was tried and reverted earlier in this PR's history, enabling the visited-map for JSX cannot produce a spurious [Circular] on the element; it strictly turns an unbounded recursion into a bounded one for genuine cycles. The visited-map insert/remove gating at print_as_prelude (ConsoleObject.rs:3537) and the FORMAT.can_have_circular_references() guards (pretty_format.rs:1179/2644) already handle the bookkeeping.

Security risks

None. This is output-formatting cycle detection; it narrows behavior from crash → [Circular] string. No new inputs, no auth/permissions/data exposure surface.

Level of scrutiny

Low. Two-line mechanical change to enum match arms that opts JSX into pre-existing, well-exercised cycle-detection machinery used by Object/Array/Map/Set. The regression test locks in exact output for all three cycle entry points. No CODEOWNERS for these paths.

Other factors

All substantive prior feedback was addressed: the .Event addition that caused a fall-through regression was reverted (cdfc31b), and the test was updated to read/assert stderr and check exit code last. The one remaining unresolved inline comment is my own self-described nit about filtering the ASAN stderr banner — it's a test-ergonomics concern (cheap insurance against a possible linux-asan flake), not a correctness issue, and ~80 other tests in the repo use the same unfiltered expect(stderr).toBe("") pattern. CI has run on the final commit (d8afc4d, build #56858). The pre-existing props.getObject() hardening and .Event in pretty_format.rs were explicitly deferred to follow-ups and are out of scope here.

@robobun

robobun commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator Author

Closing as a duplicate of #29709. The source change is byte-identical: add Tag::JSX to can_have_circular_references() in pretty_format.rs and to the matching matches! in ConsoleObject.rs. #29709 has broader test coverage (self-referencing props, props.children, array children, and a self-referencing key).

@robobun robobun closed this Jul 1, 2026
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