Skip to content

jsc: use saturating coerce::<i32> for Map/Set size display - #32279

Closed
robobun wants to merge 2 commits into
mainfrom
claude/farm/33924857/fix-coerce-i32-saturating
Closed

robobun wants to merge 2 commits into
mainfrom
claude/farm/33924857/fix-coerce-i32-saturating

Conversation

@robobun

@robobun robobun commented Jun 15, 2026 •

Copy link
Copy Markdown
Collaborator

Repro

$ bun -e 'const m=new Map([[1,1]]); Object.defineProperty(m,"size",{value:3000000000}); console.log(m)'
Map(-1294967296) {      # before
Map(2147483647) {       # after (matches 1.3.x / Zig)
  1: 1,
}

Cause

ConsoleObject.rs reads the Map/Set .size property and coerces it to i32 via JSValue::coerce_to_i32, which is the JSC ToInt32 path (ECMA modular wrap: 3000000000 becomes -1294967296). The Zig reference (ConsoleObject.zig:2761,2868) used length_value.coerce(i32, ...), which is the saturating-truncation helper (NaN→0, overflow→i32::MIN/MAX). The Rust equivalent of Zig's coerce(i32, ...) is JSValue::coerce::<i32>, not coerce_to_i32.

Low real-world impact since a real Map.prototype.size cannot exceed available memory, but it is observable via defineProperty, a size getter on a subclass, or a Proxy, and the negative number is plainly wrong.

Fix

Switch the two sites in ConsoleObject.rs to coerce::<i32>.

Auditing every remaining coerce_to_i32 call site against its .zig sibling turned up more sites with the same swap; all are corrected to match the Zig reference:

file value zig ref
ConsoleObject.rs Map/Set size ConsoleObject.zig:2761,2868
PasswordObject.rs bcrypt cost, argon2 timeCost/memoryCost PasswordObject.zig:44,63,77
ServerWebSocket.rs close() code ServerWebSocket.zig:1080
JSSourceMap.rs findEntry line/column JSSourceMap.zig:198,199

udp_socket.rs and dns.rs were checked and already match (the Zig reference uses coerceToInt32 there). Reverse-direction spot checks of existing coerce::<i32> sites found no mismatches. node/types.rs FileSystemFlags originally had the same swap but was superseded by #32506 on main (now uses validate_int32); the rebase took main's version.

Note on PasswordObject.rs

For argon2 timeCost/memoryCost, this intentionally reverts to 1.3.x behavior: timeCost: Infinity now saturates to i32::MAX and passes the >= 1 guard instead of wrapping to 0 and throwing. Neither version has an upper-bound check (e.g. timeCost: 1e8 already runs for an impractical duration on both 1.3.x and main), so the lack of an upper bound is a pre-existing, orthogonal concern. bcrypt cost is unaffected since it already has > 31 → throw.

Verification

New assertions in test/js/bun/util/inspect.test.js cover Map/Set with size = 3000000000 and Infinity, expecting Map(2147483647) / Set(2147483647).

  • USE_SYSTEM_BUN=1 bun test test/js/bun/util/inspect.test.js -t "^inspect$" → fail (Map(-1294967296))
  • bun bd test test/js/bun/util/inspect.test.js → 72 pass
  • bun bd test test/js/bun/util/password.test.ts → 68 pass
  • bun bd test test/js/node/module/module-sourcemap.test.js → 3 pass

@robobun

robobun commented Jun 15, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 3:46 AM PT - Jun 19th, 2026

❌ @robobun, your commit 0b4a1a4 has 3 failures in Build #63487 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 32279

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

bun-32279 --bun

@robobun

robobun commented Jun 15, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: diff is ready; CI failures are unrelated. Needs a maintainer.

Reproduced with:

bun -e 'const m=new Map([[1,1]]); Object.defineProperty(m,"size",{value:3000000000}); console.log(m)'

Test: bun bd test test/js/bun/util/inspect.test.js -t "^inspect$" (fails on main with Map(-1294967296), passes with fix).

Rebased onto main at 0b4a1a4 (took main's validate_int32 in FileSystemFlags::from_js from #32506, which supersedes the coercion call this PR was touching there).

CI (build #63487, complete): 284 passed / 2 failed. Both failures are darwin-aarch64 network/external-service flakes unrelated to this diff:

  • darwin-14-aarch64: test-tls-client-destroy-soon.js bytesRead count mismatch (2097152 vs 2048000)
  • darwin-26-aarch64: next-pages/dev-server.test.ts puppeteer chrome-headless-shell download failure; s3.test.ts R2 upload timeouts

inspect.test.js, password.test.ts, and every other lane touching this diff pass. Reviews: coderabbit and claude[bot] both LGTM, all threads resolved.

@coderabbitai

coderabbitai Bot commented Jun 15, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9afd7ae1-73a9-4cec-83ac-0799de378163

📥 Commits

Reviewing files that changed from the base of the PR and between dc3b0da and 0b4a1a4.

📒 Files selected for processing (4)
  • src/jsc/ConsoleObject.rs
  • src/runtime/crypto/PasswordObject.rs
  • src/runtime/server/ServerWebSocket.rs
  • src/sourcemap_jsc/JSSourceMap.rs
💤 Files with no reviewable changes (4)
  • src/jsc/ConsoleObject.rs
  • src/runtime/server/ServerWebSocket.rs
  • src/runtime/crypto/PasswordObject.rs
  • src/sourcemap_jsc/JSSourceMap.rs

Walkthrough

Replaces all coerce_to_i32 calls with the generic coerce::<i32> form across five files: ConsoleObject.rs (Map/Set size), PasswordObject.rs (bcrypt/argon cost fields), ServerWebSocket.rs (WebSocket close code), and JSSourceMap.rs (line/column args). Adds inspect test assertions for Map/Set instances with overridden huge and Infinity size values.

coerce_to_i32 replacement sweep

Layer / File(s) Summary
Console Map/Set size coercion and inspect regression tests
src/jsc/ConsoleObject.rs, test/js/bun/util/inspect.test.js
Formatter::print_map_like and Formatter::print_set switch to coerce::<i32> for the collection size property. Regression tests assert that overridden size values of large integers and Infinity saturate to 2147483647 in Bun.inspect output.
Runtime numeric argument coercion updates
src/runtime/crypto/PasswordObject.rs, src/runtime/server/ServerWebSocket.rs, src/sourcemap_jsc/JSSourceMap.rs
Three further call sites replace coerce_to_i32 with coerce::<i32>: bcrypt cost, argon timeCost and memoryCost in password parsing; WebSocket close() code argument; and sourcemap get_line_column line and column arguments. Surrounding range validation, casting, and control flow remain unchanged.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main change: switching from coerce_to_i32 to saturating coerce:: for Map/Set size display in JSC console output.
Description check ✅ Passed The description thoroughly covers the repro case, root cause analysis, fix details, audit findings across multiple files, and verification with test results. Both template sections are well-addressed.
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.

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

@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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/js/bun/util/inspect.test.js`:
- Around line 377-379: The test file currently has an assertion for Map with
Infinity size saturation at lines 377-379 but lacks the corresponding assertion
for Set. Add a matching test case after the mapWithInfiniteSize test that
creates a Set object with a size property set to Infinity, and verify that
Bun.inspect handles the saturation the same way by outputting the capped maximum
integer value (2147483647) instead of Infinity. This ensures both the Map and
Set formatter paths are tested at this boundary condition.
🪄 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: f19e8ef1-fa81-466d-bf06-457773febb6c

📥 Commits

Reviewing files that changed from the base of the PR and between e0acad3 and dc3b0da.

📒 Files selected for processing (6)
  • src/jsc/ConsoleObject.rs
  • src/runtime/crypto/PasswordObject.rs
  • src/runtime/node/types.rs
  • src/runtime/server/ServerWebSocket.rs
  • src/sourcemap_jsc/JSSourceMap.rs
  • test/js/bun/util/inspect.test.js

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

@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 — mechanical swap to the saturating coercion helper, each site cross-referenced against the Zig original.

Extended reasoning...

Overview

Nine one-line swaps from JSValue::coerce_to_i32 (ECMA ToInt32 modular wrap) to JSValue::coerce::<i32> (saturating truncation, NaN→0) across ConsoleObject.rs, PasswordObject.rs, ServerWebSocket.rs, JSSourceMap.rs, and node/types.rs, plus three new assertions in inspect.test.js. I confirmed in src/jsc/JSValue.rs:1883-1898 that CoerceTo for i32 is exactly the saturating fast-path described (f64 as i32, falling back to coerce_to_i32 only for non-numbers), so for number inputs this is purely a wrap→saturate behavior change and for non-numbers it is identical.

Security risks

None. Inputs are display lengths, hash cost parameters (still range-checked immediately after), a WebSocket close code, sourcemap line/column lookups, and an fs flag already guarded by isInt32. No auth, crypto-key, or memory-safety surface is touched; the argon2/bcrypt cost values are still bounds-checked before use.

Level of scrutiny

Low–medium. The diff is mechanical and each call site is annotated against its .zig sibling in the PR description. The only sites where observable behavior could change for non-pathological inputs are the argon2 timeCost/memoryCost paths (a value ≥ 2³¹ now saturates and passes the >= 1 / >= 8 checks instead of wrapping negative and throwing), which is the documented Zig-parity behavior and still user-intentional. The FileSystemFlags site is gated by is_int32() so it is a no-op.

Other factors

New regression tests cover the headline Map/Set case; the bug-hunting pass found nothing; no CODEOWNERS apply to the touched paths; no outstanding reviewer comments.

@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 — mechanical swap to the saturating coerce::<i32> helper to match the Zig reference, with regression tests for the observable Map/Set case.

Extended reasoning...

Overview

This PR replaces JSValue::coerce_to_i32 (ECMA ToInt32 modular wrap) with JSValue::coerce::<i32> (saturating truncation, NaN→0) at nine call sites across five Rust files, plus four new test assertions in inspect.test.js. I verified in src/jsc/JSValue.rs that coerce::<i32> fast-paths numbers with saturating f64 as i32 and falls through to coerce_to_i32 for non-numbers, exactly as the PR description claims.

Security risks

None. The change only affects how out-of-i32-range or non-finite numeric inputs are clamped before existing range validation. In every touched site the value is either (a) immediately range-checked and rejected (PasswordObject cost/timeCost/memoryCost, all guarded by is_number()), (b) already guarded by is_int32() so the swap is a no-op (FileSystemFlags), (c) a display-only length (ConsoleObject), or (d) a lookup key where wrap vs. saturate both produce a miss (JSSourceMap line/column, ServerWebSocket close code). No new code paths, no auth/crypto algorithm changes — only the integer coercion of already-validated parameters.

Level of scrutiny

Low. This is a mechanical correction aligning the Rust port with its Zig reference (the PR cites each .zig line). The only user-observable change is the headline fix: Map(2147483647) instead of Map(-1294967296) for pathological size overrides, which is covered by the new regression assertions. The other sites differ only in which error message fires for absurd inputs like cost: Infinity.

Other factors

The author audited remaining coerce_to_i32 sites and confirmed udp_socket.rs/dns.rs correctly use the wrapping form per their Zig references. CodeRabbit's only suggestion (add a Set + Infinity parity assertion) was addressed in 9cb92f1 and the thread is resolved. The bug-hunting system found nothing. Test commands in the description show the relevant suites passing.

Comment thread src/runtime/crypto/PasswordObject.rs Resolved
robobun added 2 commits June 19, 2026 10:01
When a Map or Set has a size getter returning a number >= 2^31 (via
defineProperty, proxy, or subclass), Bun.inspect/console.log printed the
ECMA ToInt32 modular-wrapped value (e.g. Map(-1294967296)) instead of
saturating to i32::MAX like the original Zig implementation.

The Rust port called JSValue::coerce_to_i32 (JSC ToInt32, modular wrap)
where the Zig reference called JSValue.coerce(i32, ...) (saturating
truncation: NaN->0, overflow->min/max). The Rust equivalent is
JSValue::coerce::<i32>.

Auditing every coerce_to_i32 call site against its .zig sibling found
seven more sites with the same swap; all are corrected here:

  - ConsoleObject.rs: Map/Set size (the reported bug)
  - PasswordObject.rs: bcrypt cost, argon2 timeCost/memoryCost
  - ServerWebSocket.rs: close code
  - JSSourceMap.rs: findEntry line/column
  - types.rs: FileSystemFlags numeric flag (no behavior change; already
    guarded by isInt32)

udp_socket.rs and dns.rs were checked and already match their Zig
reference (which uses coerceToInt32).
@robobun
robobun force-pushed the claude/farm/33924857/fix-coerce-i32-saturating branch from 76a1d25 to 0b4a1a4 Compare June 19, 2026 10:05

@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 — mechanical coerce_to_i32 → coerce::<i32> swap restoring Zig-reference parity; my earlier note on the argon2 timeCost: Infinity edge case is now documented in the description as intentional.

Extended reasoning...

Overview

Swaps JSValue::coerce_to_i32 (ECMA ToInt32, modular wrap) for JSValue::coerce::<i32> (saturating truncation) at 7 call sites across 4 files, restoring parity with the Zig reference implementation that the Rust port was meant to mirror. Adds 4 test assertions in inspect.test.js covering Map/Set with oversized/infinite size. I verified in src/jsc/JSValue.rs that coerce::<i32> does f64 as i32 (saturating, NaN→0) for number values, matching the PR's claim and the Zig coerce(i32, ...) semantics.

Security risks

None material. PasswordObject.rs is crypto-adjacent but the change only affects argument coercion, not crypto primitives. The argon2 timeCost/memoryCost: Infinity case now saturates to i32::MAX instead of wrapping to 0 and throwing — I flagged this in a prior inline comment, and the author confirmed it's intentional 1.3.x/Zig parity (the missing upper bound is pre-existing and orthogonal; e.g. timeCost: 1e8 already hangs on both old and new). bcrypt cost is unaffected (bounded 4–31). ServerWebSocket close() code and JSSourceMap line/column are both guarded by is_number() and have no security implications from saturation vs. wrap.

Level of scrutiny

Low. This is a mechanical helper-name correction following an established pattern (match the .zig reference), with a thorough per-site audit table in the description. The headline fix (negative Map/Set size in console output) is purely cosmetic. All sites are already gated by is_number() checks, so the only behavioral delta is the saturate-vs-wrap edge for out-of-i32-range numbers.

Other factors

CI build #63487 is green (prior build had only unrelated darwin agent expirations and one streams-leak flake). All review threads are resolved: coderabbit's Set+Infinity test parity nit was addressed in 0b4a1a4, and my PasswordObject behavior-change note was addressed by documenting it in the PR description. No bugs found by the bug-hunting system.

@robobun

robobun commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator Author

Stale PR review: keep open, rework.

The bug is real and still on main. console.log of a Map whose size is overridden to 3000000000 prints Map(-1294967296) (src/jsc/ConsoleObject.rs:4570 and :4712), where 1.3.x printed Map(2147483647). No other open or merged PR fixes those lines. This diff is not the shape to merge. It conflicts with main, and a rework needs these changes:

  1. Drop the PasswordObject.rs hunk. Bun.udpSocket, Bun.password: range-check numeric options before ToInt32 narrowing #36999 made Bun.password range-check in f64 and throw, and test/js/bun/util/password.test.ts:194 asserts that timeCost: Infinity throws. Saturation reverses that.
  2. Drop the ServerWebSocket.close() hunk, or turn it into a range check. Bun.serve: throw RangeError for out-of-range port instead of silently clamping #34957 and udp: reject out-of-range connect.port instead of silently clamping to 0 #34029 reject an out-of-range number. They do not clamp it.
  3. For Map and Set, print the real entry count and not the user-visible size property. Map(2147483647) is an output no runtime prints. Node v26.3.0 and Bun's own node:util inspect both print Map(1) { 1 => 1, ... 2999999900 more items }. JSValue::get_length (src/jsc/JSValue.rs:1418) already returns JSMap::size() and JSSet::size() (src/jsc/bindings/bindings.cpp:2954).
  4. Cover the two copies of the same read that fix(bun:test): handle a non-numeric size property when formatting a Set or Map diff #41148 added in src/runtime/test_runner/pretty_format.rs:1722 and :1770. Don't iterate WeakMap/WeakSet when formatting even if .size is set #30373 edits the same two statements in ConsoleObject.rs, so sequence with it.

The JSSourceMap.rs hunk is fine as written (src/sourcemap_jsc/JSSourceMap.rs:334).

@robobun

robobun commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator Author

Closing as part of a cleanup of stale pull requests. This PR has had no new commits since 2026-06-19, it conflicts with main, and its last CI run failed. This is not a judgment on the fix itself. If the problem still reproduces on a current build, reopen this PR after a rebase or open a new one against main.

@robobun robobun closed this Sep 13, 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