fix(bun:test): don't assert when formatting a Set/Map with a non-numeric `size` property

Closed
RO
2mo ago
+96-2

What does this PR do?

Fixes a fuzzer-found assertion failure (ASSERTION FAILED: isInt32() in JSC::JSValue::asInt32(), JSCJSValue.h) triggered while formatting an expect().toEqual() failure diff.

The jest pretty formatter (src/runtime/test_runner/pretty_format.rs) reads the size property of Set/Map/WeakSet/WeakMap values and called JSValue::to_int32() on the result. WeakSet/WeakMap have no size accessor on their prototype (and Set/Map instances can shadow theirs with Object.defineProperty), so a user-defined own size property is returned verbatim. When that value is not a number, to_int32() falls through to JSC__JSValue__toInt32 → JSValue::asInt32():

  • debug builds abort on the isInt32() assertion
  • release builds read a garbage length, then surface a bare TypeError instead of the toEqual diff

Repro (crashes debug builds before this change):

const ws = new WeakSet();
ws.size = BigUint64Array;
Bun.jest(import.meta.path).expect(BigUint64Array).toEqual(ws);

The fix only coerces the value when it actually is a number, so ToNumber is never invoked on an overridden size. A WeakSet/WeakMap always renders as WeakSet {}/WeakMap {}, whatever its own size says: they are not iterable, so the formatter must never try to walk them (a numeric own size such as ws.size = 5 used to surface a bare TypeError from the iteration attempt). On a real Set/Map the formatter ignores a non-numeric size and iterates the collection, so the diff shows the actual entries. The diff always renders:

expect(received).toEqual(expected)

Expected: WeakSet {}
Received: [class BigUint64Array]

How did you verify your code works?

  • The repro above no longer asserts on a debug build; it produces the normal toEqual error.
  • Added a regression test, test/js/bun/test/pretty-format-overridden-size.test.ts, covering WeakSet/WeakMap/Set/Map with non-numeric size values (a constructor, a string, a plain object, and a symbol). It also asserts that the Set/Map diffs show the real entries. It fails on an unmodified debug build (the formatter aborts, so the spawned process never prints the diffs) and passes with this change on both debug and release builds.
  • bun bd test test/js/bun/test/expect.test.js — unchanged results vs. an unmodified debug build (the only failure is the pre-existing deepEquals Set/Map stress test timeout that also occurs without this change on debug+ASAN builds).
  • Formatting of ordinary Set/Map diffs is byte-for-byte identical to the previous output.

[human-review] gate passed · iteration 0 · 2 files touched

fails on main (without fix)
ASAN without fix: 1 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/bun/test/pretty-format-overridden-size.test.ts
bun test v1.4.1 (a6c4cc276)

test/js/bun/test/pretty-format-overridden-size.test.ts:
70 |       stdout: "pipe",
71 |     });
72 | 
73 |     const [stdout, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
74 | 
75 |     expect(stdout.trim().split("\n")).toEqual(Array(7).fill("DIFF OK"));
                                           ^
error: expect(received).toEqual(expected)

  [
-   "DIFF OK",
-   "DIFF OK",
-   "DIFF OK",
-   "DIFF OK",
-   "DIFF OK",
-   "DIFF OK",
-   "DIFF OK",
+   "",
  ]

- Expected  - 7
+ Received  + 1

      at <anonymous> (/workspace/bun/test/js/bun/test/pretty-format-overridden-size.test.ts:75:39)
(fail) pretty_format should handle collections with an overridden `size` property > non-numeric `size` on (Weak)Set/(Weak)Map still produces a toEqual diff [499.57ms]

 0 pass
 1 fail
 1 expect() calls
Ran 1 test across 1 file. [2.42s]
error: script "bd" exited with code 1
__F:1:S:0

release without fix: 1 FAILED
bun test v1.4.1-canary.1 (d27bca5f6)

test/js/bun/test/pretty-format-overridden-size.test.ts:
70 |       stdout: "pipe",
71 |     });
72 | 
73 |     const [stdout, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
74 | 
75 |     expect(stdout.trim().split("\n")).toEqual(Array(7).fill("DIFF OK"));
                                           ^
error: expect(received).toEqual(expected)

  [
    "DIFF OK",
    "DIFF OK",
    "DIFF OK",
    "DIFF OK",
    "DIFF OK",
-   "DIFF OK",
-   "DIFF OK",
+   "UNEXPECTED: Type error",
+   "UNEXPECTED: Type error",
  ]

- Expected  - 2
+ Received  + 2

      at <anonymous> (/workspace/bun/test/js/bun/test/pretty-format-overridden-size.test.ts:75:39)
(fail) pretty_format should handle collections with an overridden `size` property > non-numeric `size` on (Weak)Set/(Weak)Map still produces a toEqual diff [7.14ms]

 0 pass
 1 fail
 1 expect() calls
Ran 1 test across 1 file. [69.00ms]
__F:1:S:0
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/bun/test/pretty-format-overridden-size.test.ts
bun test v1.4.1 (a6c4cc276)

test/js/bun/test/pretty-format-overridden-size.test.ts:
(pass) pretty_format should handle collections with an overridden `size` property > non-numeric `size` on (Weak)Set/(Weak)Map still produces a toEqual diff [336.19ms]

 1 pass
 0 fail
 2 expect() calls
Ran 1 test across 1 file. [2.30s]
__F:0:S:0

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 601ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/6] gen generated_host_exports.rs
generated_host_exports.rs: 122 exports (host=5, lazy=10, generic=107, rust=0); 244 extern-C blocks audited
[1/6] cargo bun_runtime → libbun_runtime.a
   Compiling bun_core v0.0.0 (/workspace/bun/src/bun_core)
   Compiling bun_errno v0.0.0 (/workspace/bun/src/errno)
   Compiling bun_ptr v0.0.0 (/workspace/bun/src/ptr)
   Compiling bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
   Compiling bun_safety v0.0.0 (/workspace/bun/src/safety)
   Compiling bun_base64 v0.0.0 (/workspace/bun/src/base64)
   Compiling bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys)
   Compiling bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys)
   Compiling bun_zstd v0.0.0 (/workspace/bun/src/zstd)
   Compiling bun_paths v0.0.0 (/workspace/bun/src/paths)
   Compiling bun_picohttp v0.0.0 (/workspace/bun/src/picohttp)
   Compiling b
... (truncated)
diff hotspot
src/runtime/test_runner/pretty_format.rs           | 20 +++++-
 .../bun/test/pretty-format-overridden-size.test.ts | 78 ++++++++++++++++++++++
 2 files changed, 96 insertions(+), 2 deletions(-)

gate history · 3 passed · 0 rejected · iteration 0

evidence per changed file
file                                                    reads  edits  tests
src/runtime/test_runner/pretty_format.rs                    2      4      7
test/js/bun/test/pretty-format-overridden-size.test.ts      2      3      7