diff --git a/src/jsc/ConsoleObject.rs b/src/jsc/ConsoleObject.rs index a9270cf88662..52cc89368174 100644 --- a/src/jsc/ConsoleObject.rs +++ b/src/jsc/ConsoleObject.rs @@ -1954,6 +1954,7 @@ pub mod formatter { | Tag::Error | Tag::Class | Tag::Event + | Tag::JSX ) } } @@ -3289,10 +3290,6 @@ pub mod formatter { if self.global_this.has_exception() { return Err(jsc::JsError::Thrown); } - if !can_circ { - return Ok(true); - } - if !self.stack_check.is_safe_to_recurse() { self.failed = true; if self.can_throw_stack_overflow { @@ -3300,6 +3297,9 @@ pub mod formatter { } return Ok(false); } + if !can_circ { + return Ok(true); + } if self.map_node.is_none() { let mut node = core::ptr::NonNull::new(visited::Pool::get_node()) @@ -3554,18 +3554,20 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - let target = value.get_proxy_internal_field(jsc::ProxyField::Target); + let mut target = value.get_proxy_internal_field(jsc::ProxyField::Target); // Proxy does not allow non-objects here. debug_assert!(target.is_cell()); // TODO: if (options.showProxy), print like // `Proxy { target: ..., handlers: ... }` — this is default off so // it is not used. - self.format::( - Tag::get(target, self.global_this)?, - writer_, - target, - self.global_this, - ) + loop { + let tag = Tag::get(target, self.global_this)?; + if !matches!(tag.tag.tag(), Tag::Proxy) { + return self.format::(tag, writer_, target, self.global_this); + } + target = target.get_proxy_internal_field(jsc::ProxyField::Target); + debug_assert!(target.is_cell()); + } } #[inline(never)] diff --git a/src/runtime/test_runner/pretty_format.rs b/src/runtime/test_runner/pretty_format.rs index ed3cf5371ac7..1c2343fb1055 100644 --- a/src/runtime/test_runner/pretty_format.rs +++ b/src/runtime/test_runner/pretty_format.rs @@ -419,7 +419,10 @@ impl Tag { #[inline] pub(crate) const fn can_have_circular_references(self) -> bool { - matches!(self, Tag::Array | Tag::Object | Tag::Map | Tag::Set) + matches!( + self, + Tag::Array | Tag::Object | Tag::Map | Tag::Set | Tag::JSX | Tag::Event + ) } } @@ -1870,6 +1873,7 @@ impl<'a> Formatter<'a> { { evt @ (EventType::MessageEvent | EventType::ErrorEvent) => evt, _ => { + let _ = self.map.remove(&value); return self.print_as::( writer.ctx, value, JSType::Event, ); diff --git a/test/js/bun/util/inspect.test.js b/test/js/bun/util/inspect.test.js index 5c91f3dbe05f..3b66ccd640d7 100644 --- a/test/js/bun/util/inspect.test.js +++ b/test/js/bun/util/inspect.test.js @@ -1,5 +1,5 @@ import { describe, expect, it } from "bun:test"; -import { bunEnv, bunExe, isASAN, isWindows, normalizeBunSnapshot, tmpdirSync } from "harness"; +import { bunEnv, bunExe, isASAN, isWindows, normalizeBunSnapshot, tempDir, tmpdirSync } from "harness"; import { join } from "path"; import util from "util"; it("prototype", () => { @@ -316,6 +316,176 @@ it("jsx with fragment", () => { expect(input).toBe(output); }); +it.concurrent("jsx with circular references does not crash", async () => { + // Run in a subprocess: without the fix this overflows the native stack and segfaults, + // which would otherwise kill the test runner before it can record a failure. + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + ` + const a = { $$typeof: Symbol.for("react.element"), type: "div", props: null, key: null }; + a.props = a; + console.log(Bun.inspect(a)); + + const b = { $$typeof: Symbol.for("react.element"), type: "div", key: null }; + b.props = { children: b }; + console.log(Bun.inspect(b)); + + const c = { $$typeof: Symbol.for("react.element"), type: "span", key: null }; + c.props = { children: [c, c] }; + console.log(Bun.inspect(c)); + + const d = { $$typeof: Symbol.for("react.element"), type: "div", props: {} }; + d.key = d; + console.log(Bun.inspect(d)); + `, + ], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stdout).toContain("[Circular]"); + expect(stdout).toContain("
\n [Circular]\n
"); + expect(stdout).toContain("\n [Circular]\n [Circular]\n"); + expect(stdout).toContain("
"); + expect(exitCode).toBe(0); +}); + +it.concurrent("jsx with non-object props does not crash", async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + ` + const el = { $$typeof: Symbol.for("react.element"), type: "div", props: 42, key: null }; + console.log(Bun.inspect(el)); + `, + ], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stdout.trim()).toBe("
"); + expect(exitCode).toBe(0); +}); + +it.concurrent("jsx with circular props in test diff formatter", async () => { + using dir = tempDir("jsx-circular-diff", { + "diff.test.js": ` + import { test, expect } from "bun:test"; + test("circular", () => { + const el = { $$typeof: Symbol.for("react.element"), type: "div", props: null, key: null }; + el.props = el; + expect(() => expect(el).toEqual({})).toThrow(); + }); + test("non-object props", () => { + const el = { $$typeof: Symbol.for("react.element"), type: "div", props: 42, key: null }; + expect(() => expect(el).toEqual({})).toThrow(); + }); + `, + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "test", "diff.test.js"], + cwd: String(dir), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toContain("2 pass"); + expect(exitCode).toBe(0); +}); + +it.concurrent("Event in test diff formatter is not spuriously [Circular]", async () => { + using dir = tempDir("event-diff", { + "diff.test.js": ` + import { test, expect } from "bun:test"; + test("close event", () => { + expect(() => expect(new CloseEvent("close", { code: 1000 })).toEqual({})).toThrow(/CloseEvent/); + }); + test("custom event", () => { + expect(() => expect(new CustomEvent("foo")).toEqual({})).toThrow(/CustomEvent/); + }); + test("circular message event still detected", () => { + const ev = new MessageEvent("message"); + Object.defineProperty(ev, "data", { value: ev, configurable: true }); + expect(() => expect(ev).toEqual({})).toThrow(/\\[Circular\\]/); + }); + `, + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "test", "diff.test.js"], + cwd: String(dir), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toContain("3 pass"); + expect(exitCode).toBe(0); +}); + +it.concurrent("deeply nested Proxy chain does not crash", async () => { + // Without the fix, print_proxy recurses on the target without a stack-safety + // check and segfaults; run in a subprocess so a regression fails the suite + // instead of killing the runner. + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + ` + let p = { ok: 1 }; + for (let i = 0; i < 100000; i++) p = new Proxy(p, {}); + console.log(Bun.inspect(p)); + console.log(p); + `, + ], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(normalizeBunSnapshot(stdout)).toMatchInlineSnapshot(` + "{ + ok: 1, + } + { + ok: 1, + }" + `); + expect(proc.signalCode).toBeFalsy(); + expect(exitCode).toBe(0); +}); + +it.concurrent("deeply nested non-cyclic jsx does not segfault", async () => { + // Stack size and release-build frame size vary by platform, so a fixed depth + // may or may not overflow: accept either a clean RangeError or successful + // completion. The regression was SIGSEGV. + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + ` + let el = { $$typeof: Symbol.for("react.element"), type: "div", props: {}, key: null }; + for (let i = 0; i < 20000; i++) + el = { $$typeof: Symbol.for("react.element"), type: "div", props: { children: el }, key: null }; + try { Bun.inspect(el); console.log("ok"); } + catch (e) { console.log(e.constructor.name); } + `, + ], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(["RangeError", "ok"]).toContain(stdout.trim()); + expect(proc.signalCode).toBeFalsy(); + expect(exitCode).toBe(0); +}); + it("inspect", () => { expect(Bun.inspect(new TypeError("what")).includes("TypeError: what")).toBe(true); expect(Bun.inspect("hi")).toBe('"hi"');