Fix stack overflow in Bun.inspect() on circular JSX elements#29709
Fix stack overflow in Bun.inspect() on circular JSX elements#29709robobun wants to merge 1 commit into
Conversation
|
Updated 9:59 PM PT - May 21st, 2026
✅ @robobun, your commit 815032915ccc7db987554602e81971f68bf991b9 passed in 🧪 To try this PR locally: bunx bun-pr 29709That installs a local version of the PR into your bun-29709 --bun |
WalkthroughAdds JSX to circular-reference detection and makes JSX props inspection safe when Changes
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
|
Found 1 issue this PR may fix:
🤖 Generated with Claude Code |
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
There was a problem hiding this comment.
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 351-353: The test creates el2 with props: 42 and currently only
asserts that an error is thrown when comparing it to {}
(expect(el2).toEqual({})), which allows any thrown error (including ones whose
message incorrectly contains "[Circular]"). Update the assertion for the thrown
error to also assert the error message does NOT contain the string "[Circular]"
so the test fails if a circular-related message is produced; target the existing
el2 setup and the current expect(...).toThrow invocation when making this
change.
🪄 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: 34ba822f-7961-4803-84d6-bf95dc3fc265
📒 Files selected for processing (2)
src/bun.js/test/pretty_format.zigtest/js/bun/util/inspect.test.js
| const el2 = { $$typeof: Symbol.for("react.element"), type: "div", props: 42, key: null }; | ||
| expect(() => expect(el2).toEqual({})).toThrow(); | ||
| }); |
There was a problem hiding this comment.
Strengthen the non-object props diff assertion to avoid false positives.
This currently passes on any thrown error, even if the message incorrectly includes [Circular]. Assert the thrown message does not contain [Circular] to lock in the intended behavior.
💡 Suggested test tightening
const el2 = { $$typeof: Symbol.for("react.element"), type: "div", props: 42, key: null };
- expect(() => expect(el2).toEqual({})).toThrow();
+ try {
+ expect(el2).toEqual({});
+ throw new Error("expected assertion to throw");
+ } catch (err) {
+ expect(String(err)).not.toContain("[Circular]");
+ }🤖 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 351 - 353, The test creates
el2 with props: 42 and currently only asserts that an error is thrown when
comparing it to {} (expect(el2).toEqual({})), which allows any thrown error
(including ones whose message incorrectly contains "[Circular]"). Update the
assertion for the thrown error to also assert the error message does NOT contain
the string "[Circular]" so the test fails if a circular-related message is
produced; target the existing el2 setup and the current expect(...).toThrow
invocation when making this change.
|
The Supersedes #29170 (same fix, this one has more test coverage). |
There was a problem hiding this comment.
LGTM — the pretty_format.zig mirror fix and the diff-formatter regression test address my earlier comment.
Extended reasoning...
Overview
This PR closes a fuzzer-found native stack overflow in the JSX inspection path. It touches two near-duplicate formatters — src/bun.js/ConsoleObject.zig (used by Bun.inspect/console.log) and src/bun.js/test/pretty_format.zig (used by the test diff renderer) — plus test/js/bun/util/inspect.test.js. In each formatter the change is two lines: add .JSX to canHaveCircularReferences() so the existing visited-map guard fires, and replace props.getObject().? with orelse break :props so a non-object props is treated as no-props instead of panicking. Five new tests cover circular props/children/key, non-object props, and the expect().toEqual() diff-formatter path.
Security risks
None. This is purely output-formatting / crash-avoidance code; it does not touch auth, filesystem, network, or parsing. Both edits are strictly defensive — they only add a cycle guard for one more tag and remove a force-unwrap.
Level of scrutiny
Low-to-medium. The change follows the exact pattern already used for .Array/.Object/.Map/.Set in the same function, so there's no novel logic. The labeled break :props exits after the defer this.quote_strings = prev_quote_strings is registered, so state is restored correctly. The only behavioural surface beyond crash-fix is that JSX values now occupy a slot in the visited map during recursion, which is the intended effect.
Other factors
My previous review flagged that pretty_format.zig was unpatched; the author applied the same fix in f26c1c3 and added a test that drives JestPrettyFormat via expect(el).toEqual({}). No CODEOWNERS cover these files. A bot flagged a possible duplicate (#29170), which maintainers may want to close, but that doesn't affect this PR's correctness.
f26c1c3 to
39e75db
Compare
There was a problem hiding this comment.
LGTM — the mirrored fix in pretty_format.zig addresses my earlier comment, and both code paths now have test coverage.
Extended reasoning...
Overview
This PR fixes a fuzzer-found native stack overflow when Bun.inspect() (and the test-runner diff formatter) encounters a self-referential JSX element. The change touches two near-identical formatters — src/jsc/ConsoleObject.zig and src/test_runner/pretty_format.zig — adding .JSX to canHaveCircularReferences() so the existing visited-map / [Circular] guard applies, and replacing a props.getObject().? force-unwrap with orelse break :props so a non-object props is skipped instead of asserting. Five new tests in inspect.test.js cover circular props/children/key, non-object props, and the expect().toEqual() diff path.
Security risks
None. This is purely output-formatting / crash-hardening code with no auth, crypto, filesystem, or network surface. The change strictly narrows behavior from a segfault to printing [Circular] or skipping invalid props.
Level of scrutiny
Low-to-medium. The fix is two mechanical edits per file: one enum case added to an existing switch, and one optional-unwrap converted to a labeled-block bailout. The defer this.quote_strings = prev_quote_strings placed before the break :props still fires on early exit, so state is correctly restored. The same pattern (visited-map guarded by canHaveCircularReferences()) already protects .Array/.Object/.Map/.Set, so adding .JSX follows the established design exactly.
Other factors
I previously flagged that the original revision only patched ConsoleObject.zig; the author applied the identical fix to pretty_format.zig in f26c1c3 and added a regression test exercising the diff-formatter path (expect(circularJsx).toEqual({})). That comment is resolved. CodeRabbit's remaining suggestion to tighten the props: 42 diff-formatter assertion is a minor test-quality nit — the test as written already regresses the crash (prior behavior was a segfault, not a thrown error). The CI failures listed by robobun are unrelated flakes (vendor-tarball 502 on darwin-x64, plus known-flaky tests in heap-snapshot/bake/webview/v8-date-parser that this PR does not touch). The bug-hunting system found no issues on the current revision.
There was a problem hiding this comment.
LGTM — the mirrored fix in pretty_format.zig addresses my earlier comment; the remaining .Event note is pre-existing and non-blocking.
Extended reasoning...
Overview
This PR fixes a fuzzer-found native stack overflow in Bun.inspect() for self-referential JSX elements. It makes two small changes, mirrored across src/jsc/ConsoleObject.zig and src/test_runner/pretty_format.zig: (1) add .JSX to canHaveCircularReferences() so the visited-map / [Circular] guard applies, and (2) replace the props.getObject().? force-unwrap with orelse break :props so a non-object props no longer panics. Five new tests in test/js/bun/util/inspect.test.js cover circular props, children, key, non-object props, and the test-diff-formatter path.
Security risks
None. This is purely a crash-hardening change in the value-formatter; it does not touch auth, network, filesystem, or parsing of untrusted input beyond making an existing code path more defensive.
Level of scrutiny
Low. Each Zig change is a one-line edit on a well-understood guard, applied identically in two near-duplicate formatters. The labelled-block break :props is the idiomatic Zig pattern already used elsewhere in these files. The change can only reduce crashes — it cannot regress correct output for well-formed JSX, since real React elements always have an object props and the visited-map check is a no-op on the first visit.
Other factors
My earlier review asked for the fix to be mirrored into pretty_format.zig; the author did so and added a regression test that exercises the diff-formatter path (expect(el).toEqual({})). The one new inline finding (.Event still omitted from pretty_format.zig's circular-reference list) is explicitly flagged as pre-existing, requires Object.defineProperty to trigger, and is a one-token follow-up the author can take or leave — it does not block this fuzzer fix. CodeRabbit's test-tightening suggestion is a minor nit. CI failure was an unrelated GitHub 502 on vendor tarball fetch.
|
|
||
| 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; |
There was a problem hiding this comment.
🟣 While syncing this line with ConsoleObject.zig (which lists .Function, .Error, .Class, .Event in addition to .JSX), .Event is still omitted here. The .Event arm of JestPrettyFormat.printAs() recurses into this.format(...) on a MessageEvent's data / ErrorEvent's error without a visited-map guard, so e.g. Object.defineProperty(ev, 'data', {value: ev}); expect(ev).toEqual({}) still overflows the native stack — same crash class this PR fixes for .JSX. Pre-existing, but a one-token addition on the line already being edited.
Extended reasoning...
What the bug is
This PR brings canHaveCircularReferences() in src/test_runner/pretty_format.zig:329 partially into sync with src/jsc/ConsoleObject.zig:1139 by adding .JSX. However, ConsoleObject.zig's version also lists .Function, .Error, .Class, and .Event — and after this PR pretty_format.zig still omits all four. Of those, .Event is the one that matters: the .Event arm of JestPrettyFormat.printAs() recursively calls this.format(...) on the event's data (MessageEvent) and error (ErrorEvent) properties, and that recursion is unguarded because the visited-map insert/lookup at the top of printAs() is gated on comptime Format.canHaveCircularReferences().
The specific code path
In pretty_format.zig:
Tag.get()— aMessageEvent/ErrorEventhasjs_type == .Event(JSEventType = 0b11101111perJSMessageEvent.h:49/JSErrorEvent.h:49), which is not.DOMWrapper(0b11101110), so thejs_type == .DOMWrapperearly-return doesn't fire..Eventis notisHidden(), so execution falls through to the finalswitchwhere.Event => .Event.printAs(.Event, ...)— forevent_type == .MessageEventit doesconst data = value.fastGet(this.globalThis, .data), thenconst tag = Tag.get(data, ...), thenthis.format(tag, ..., data, ...). For.ErrorEventit does the same with.@"error".canHaveCircularReferences()at line 329 returnsfalsefor.Event, so thethis.map.getOrPut(value)/[Circular]block at the top ofprintAs()is comptime-elided for the.Eventinstantiation.
Why nothing else prevents it
JestPrettyFormat has no depth limiter; the only cycle defence is the visited map, and it is only consulted when canHaveCircularReferences() returns true. ConsoleObject.zig already includes .Event in its list (line 1139), which is why Bun.inspect(ev) doesn't crash — but pretty_format.zig is a separate copy reached via diff_format.zig for expect().toEqual() / snapshot diffs, and it never got that addition.
Step-by-step proof
const ev = new MessageEvent('message'); Object.defineProperty(ev, 'data', { value: ev });— overridingdataon aMessageEventinstance viadefinePropertyworks (the existingMessageEvent with deleted datatest ininspect.test.jsalready relies on this).expect(ev).toEqual({})fails anddiff_format.zigcallsJestPrettyFormat.format(received, ...)to renderev.Tag.get(ev)returns{ .tag = .Event, .cell = .Event }.printAs(.Event, ..., ev, ...)is entered.comptime .Event.canHaveCircularReferences()isfalse, so no visited-map insert.event_typeresolves to.MessageEvent;fastGet(.data)returnsevitself.Tag.get(ev)again returns.Event;this.format(...)re-entersprintAs(.Event, ..., ev, ...)→ step 4. Native stack overflow → segfault in the test process.
The same applies to an ErrorEvent whose error property is overridden to point to itself.
Impact
Same crash class this PR fixes for .JSX: a self-referential value routed through the test diff formatter overflows the native stack and segfaults bun test. The trigger is admittedly contrived (requires Object.defineProperty to make a DOM event reference itself), so this is lower-priority than the JSX case the fuzzer actually found — but it's the identical mechanism on the very line this PR is editing to bring the two implementations into sync.
Fix
Add or tag == .Event to line 329:
return tag == .Array or tag == .Object or tag == .Map or tag == .Set or tag == .JSX or tag == .Event;(.Function, .Error, and .Class are also missing relative to ConsoleObject.zig, but those arms in pretty_format.zig don't recurse, so omitting them is harmless — adding them would just fully sync the two lists.)
There was a problem hiding this comment.
LGTM — targeted crash fix with good test coverage; the .Event note is pre-existing and can be a follow-up.
Extended reasoning...
Overview
This PR fixes a fuzzer-found native stack overflow in Bun.inspect() when formatting self-referential JSX elements. The change is ~6 lines of logic across two near-duplicate formatters (src/jsc/ConsoleObject.zig and src/test_runner/pretty_format.zig): adding .JSX to canHaveCircularReferences() so the existing visited-map guard fires, and replacing a props.getObject().? force-unwrap with orelse break :props so non-object props is skipped instead of asserting. Five new test cases in inspect.test.js cover circular props/children/key, non-object props, and the test-diff-formatter path.
Security risks
None. This is output-formatting code with no auth, crypto, filesystem, or network surface. The change strictly narrows behavior (turning two crash paths into bounded [Circular] / no-op output) and introduces no new inputs or side effects.
Level of scrutiny
Low-to-moderate. The fix is mechanical — it extends an existing enum allowlist by one variant and replaces a panic-on-null with an early break, both well-established patterns already used for other tags in the same functions. The labelled-block break :props correctly preserves the defer this.quote_strings = prev_quote_strings restore. Both copies of the formatter received identical edits.
Other factors
My earlier review flagged that pretty_format.zig needed the same fix; the author applied it and the thread is resolved. The remaining open comments are (1) CodeRabbit's minor test-tightening suggestion and (2) my own note that .Event is still missing from pretty_format.zig's list — both are non-blocking nits about pre-existing behavior, not regressions introduced here. Test coverage directly exercises the previously-crashing inputs in both formatter code paths.
There was a problem hiding this comment.
LGTM — thanks for addressing the test nits; the remaining .Event note is pre-existing and can be a follow-up.
Extended reasoning...
Overview
This PR fixes a fuzzer-found native stack overflow in Bun.inspect() / the test diff formatter when given self-referential JSX-shaped objects. The change is two-fold across both src/jsc/ConsoleObject.zig and src/test_runner/pretty_format.zig: (1) add .JSX to canHaveCircularReferences() so the existing visited-map guard applies, and (2) replace the props.getObject().? force-unwrap with orelse break :props so non-object props is treated as no-props instead of asserting. Three subprocess regression tests are added to test/js/bun/util/inspect.test.js.
Security risks
None. This is a defensive fix in output-formatting code — it strictly reduces crash surface by adding cycle detection and removing an unsafe unwrap. No new inputs, no auth/crypto/permission logic.
Level of scrutiny
Low. The Zig changes are mechanical: one enum tag appended to an existing comptime allowlist (identical pattern already used for .Array/.Object/etc.), and one .? swapped for orelse break :props on a labeled block. Both edits are mirrored verbatim across the two duplicate formatters. The visited-map machinery being enabled is pre-existing and well-exercised by other tags.
Other factors
- My earlier feedback (mirror the fix into
pretty_format.zig; test-convention nits around assertion order,tempDir, and the negativepaniccheck) has all been addressed in commits 39e75db and 8f62f57. - The one open note I left —
.Eventstill missing frompretty_format.zig'scanHaveCircularReferences()— is a pre-existing gap with a contrived trigger and is reasonable to defer to a follow-up; it does not block this fix. - The reported CI failures on 56394e7 are build-infra (
scripts/build/ci.ts, vendor tarball fetches) unrelated to the Zig/test changes here, consistent with the two "Retrigger CI" commits. - Test coverage is solid: circular via
props,children(single + array), andkey; non-objectprops; and theexpect().toEqual()diff path — all run in subprocesses so a regression segfault fails the test rather than killing the runner.
There was a problem hiding this comment.
LGTM — thanks for addressing the test-convention nits. The remaining .Event note on pretty_format.zig:329 is pre-existing and fine as a follow-up.
Extended reasoning...
Overview
Adds .JSX to canHaveCircularReferences() in both src/jsc/ConsoleObject.zig and src/test_runner/pretty_format.zig, and replaces the props.getObject().? force-unwrap with orelse break :props in both files. Three subprocess regression tests are added to test/js/bun/util/inspect.test.js covering circular props/children/key, non-object props, and the test-diff-formatter path.
Security risks
None. This is a defensive change in display/formatting code — it strictly removes a native stack overflow and an assertion failure on malformed JSX-shaped objects. No new inputs are trusted, no parsing/auth/crypto is touched.
Level of scrutiny
Low-to-medium. The fix is a one-token enum addition plus replacing a panic-on-null with a graceful early-out, mirrored across two near-identical formatters. The pattern matches how the other recursive tags (.Array, .Object, .Map, .Set) already participate in the visited-map guard, and ConsoleObject.zig already listed several other tags here. The labelled-block break :props correctly preserves the defer this.quote_strings = prev_quote_strings cleanup.
Other factors
- All earlier review feedback has been addressed: the duplicate formatter in
pretty_format.zigwas patched (f26c1c3), and the test-convention nits (assertion order,tempDir, dropping the negativepaniccheck) were fixed in 8f62f57. - The one open inline comment (
.Eventmissing frompretty_format.zig:329) is my own and explicitly marked as a pre-existing issue of the same class — it does not block this fuzzer-found crash fix. - Tests run the crashing inputs in subprocesses so a regression would fail the suite rather than segfault the runner.
- Reported CI failures on earlier commits were infra-level build-zig/build-cpp flakes unrelated to these Zig/JS-only changes; the author has retriggered.
There was a problem hiding this comment.
LGTM — minimal, well-tested crash fix; all prior review feedback addressed. The remaining .Event note is pre-existing and fine to defer.
Extended reasoning...
Overview
This PR fixes a fuzzer-found native stack overflow in Bun.inspect() (and the Jest diff formatter) when a JSX-shaped object references itself. The Zig changes are tiny and surgical: (1) add .JSX to canHaveCircularReferences() in both src/jsc/ConsoleObject.zig and src/test_runner/pretty_format.zig so the existing visited-map / [Circular] guard fires, and (2) replace props.getObject().? with orelse break :props so a non-object props no longer panics. Three new subprocess-based tests in test/js/bun/util/inspect.test.js cover circular props/children/key, non-object props, and the test-runner diff path.
Security risks
None. This is read-only formatting/inspection code; the change strictly narrows crash surface by adding cycle detection and removing a force-unwrap. No auth, crypto, filesystem, or network paths touched.
Level of scrutiny
Low–medium. The logic change is ~4 lines per file and follows the exact pattern already used for .Array/.Object/etc. The orelse break :props is strictly safer than the prior .? unwrap. Tests run in subprocesses so a regression segfault would still produce a clean failure rather than killing the suite.
Other factors
All earlier review feedback has been addressed: the fix was mirrored into pretty_format.zig (f26c1c3), and the test-convention nits — assertion order, tempDir over tmpdirSync, dropping the negative panic check — were applied in 8f62f57. My outstanding 🟣 note about .Event in pretty_format.zig is a pre-existing, contrived-trigger issue I explicitly flagged as optional; not a blocker for this PR. The single CI failure (test-http-should-emit-close-when-connection-is-aborted.ts timeout on Windows) is unrelated to these changes.
89b8a68 to
b36219f
Compare
The JSX formatter did not participate in circular reference detection, so a React element that referenced itself through props, key, or children would recurse until the native stack overflowed. Add Tag::JSX to can_have_circular_references() in both ConsoleObject.rs and the test diff formatter (pretty_format.rs) so the visited-map guard applies.
195aec0 to
8150329
Compare
There was a problem hiding this comment.
LGTM — thanks for addressing the test-convention nits. The pre-existing .Event gap in pretty_format.rs can be a follow-up.
Extended reasoning...
Overview
Two one-line additions: Tag::JSX is added to the can_have_circular_references() allowlist in both src/jsc/ConsoleObject.rs (Bun.inspect / console.log) and src/runtime/test_runner/pretty_format.rs (expect().toEqual() diff renderer). This enables the existing visited-map / [Circular] guard for JSX elements, fixing a fuzzer-found native stack overflow when a React element references itself. ~80 lines of subprocess-based regression tests cover circular props, children, key, and non-object props in both formatters.
Security risks
None. This is a defensive hardening change in output-formatting code — it gates more inputs through an existing cycle guard rather than adding new logic. No auth, crypto, parsing, or untrusted-input boundaries are touched beyond making inspection of pathological objects safer (turning a segfault into [Circular] text).
Level of scrutiny
Low-to-moderate. The Rust changes are mechanically trivial — adding one enum variant to two matches!() arms to opt into pre-existing safety machinery. There is no new control flow; the visited-map logic itself is unchanged. The test additions follow repo conventions (subprocess isolation so a regression fails the suite rather than segfaulting the runner, tempDir with using, stdout asserted before exitCode).
Other factors
All prior review feedback has been addressed: the fix was mirrored into pretty_format.rs after my first-round comment, and the latest revision applies all three test-convention nits from my second-round comment (assertion order, removed not.toContain("panic"), tempDir instead of tmpdirSync). The one remaining note — .Event still missing from pretty_format.rs's allowlist — is a pre-existing issue with a contrived trigger that I explicitly marked as optional; it should not block this fuzzer-fix PR. CodeRabbit's test-tightening suggestion is a minor nice-to-have that does not affect correctness. No bugs were found by the bug-hunting system on the current revision.
Fuzzilli found a stack overflow (fingerprint
d8d53c93d7954f20) when inspecting a React element that references itself:Tag::JSXwas not included incan_have_circular_references(), so the visited-map and stack-depth guards were never applied when recursing into JSXprops,key, orchildren. Any self-referential element overflowed the native stack.Applied in both
src/jsc/ConsoleObject.rs(Bun.inspect/console.log) andsrc/runtime/test_runner/pretty_format.rs(test diff formatter used byexpect().toEqual()).Circular references now print
[Circular]:Regression tests run the crashing inputs in subprocesses so a regression would fail the suite rather than segfault the runner.