From 1abfc0ea24764717f3ae9d904ea3a28f788e2819 Mon Sep 17 00:00:00 2001 From: robobun Date: Fri, 17 Oct 2025 14:03:26 -0700 Subject: [PATCH] fix: panic when overriding Set/Map size property with non-numeric value (#23787) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes a panic that occurred when `console.log()` tried to format a Set or Map instance with a non-numeric `size` property. ## Issue When a Set or Map subclass overrides the `size` property with a non-numeric value (like a constructor function, string, or other object), calling `console.log()` on the instance would trigger a panic: ```javascript class C1 extends Set { constructor() { super(); Object.defineProperty(this, "size", { writable: true, enumerable: true, value: Set }); console.log(this); // panic! } } new C1(); ``` ## Root Cause In `src/bun.js/ConsoleObject.zig`, the Map and Set formatting code called `toInt32()` directly on the `size` property value. This function asserts that the value is not a Cell (objects/functions), causing a panic when `size` was overridden with non-numeric values. ## Solution Changed both Map and Set formatting to use `coerce(i32, globalThis)` instead of `toInt32()`. This properly handles non-numeric values using JavaScript's standard type coercion rules and propagates any coercion errors appropriately. ## Test Plan Added regression tests to `test/js/bun/util/inspect.test.js` that verify Set and Map instances with overridden non-numeric `size` properties can be inspected without panicking. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Bot Co-authored-by: Claude --- src/bun.js/ConsoleObject.zig | 4 ++-- test/js/bun/util/inspect.test.js | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/bun.js/ConsoleObject.zig b/src/bun.js/ConsoleObject.zig index 42705bbad3..166a0f027f 100644 --- a/src/bun.js/ConsoleObject.zig +++ b/src/bun.js/ConsoleObject.zig @@ -2717,7 +2717,7 @@ pub const Formatter = struct { }, .Map => { const length_value = try value.get(this.globalThis, "size") orelse jsc.JSValue.jsNumberFromInt32(0); - const length = length_value.toInt32(); + const length = try length_value.coerce(i32, this.globalThis); const prev_quote_strings = this.quote_strings; this.quote_strings = true; @@ -2824,7 +2824,7 @@ pub const Formatter = struct { }, .Set => { const length_value = try value.get(this.globalThis, "size") orelse jsc.JSValue.jsNumberFromInt32(0); - const length = length_value.toInt32(); + const length = try length_value.coerce(i32, this.globalThis); const prev_quote_strings = this.quote_strings; this.quote_strings = true; diff --git a/test/js/bun/util/inspect.test.js b/test/js/bun/util/inspect.test.js index a28e2c6313..02b3be838c 100644 --- a/test/js/bun/util/inspect.test.js +++ b/test/js/bun/util/inspect.test.js @@ -347,6 +347,23 @@ it("inspect", () => { expect(Bun.inspect(new Map())).toBe("Map {}"); expect(Bun.inspect(new Map([["foo", "bar"]]))).toBe('Map(1) {\n "foo": "bar",\n}'); expect(Bun.inspect(new Set(["bar"]))).toBe('Set(1) {\n "bar",\n}'); + + // Regression test: Set/Map with overridden size property should not panic + const setWithOverriddenSize = new Set(); + Object.defineProperty(setWithOverriddenSize, "size", { + writable: true, + enumerable: true, + value: Set, + }); + expect(Bun.inspect(setWithOverriddenSize)).toBe("Set {}"); + + const mapWithOverriddenSize = new Map(); + Object.defineProperty(mapWithOverriddenSize, "size", { + writable: true, + enumerable: true, + value: "not a number", + }); + expect(Bun.inspect(mapWithOverriddenSize)).toBe("Map {}"); expect(Bun.inspect(
foo
)).toBe("
foo
"); expect(Bun.inspect(
foo
)).toBe("
foo
"); expect(Bun.inspect(
foo
)).toBe("
foo
");