Skip to content

[JSC] Set methods: clamp a set-like's size at UINT32_MAX instead of truncating it - #596

Open
robobun wants to merge 1 commit into
mainfrom
robobun/1332550e/set-like-size-saturate
Open

robobun wants to merge 1 commit into
mainfrom
robobun/1332550e/set-like-size-saturate

Conversation

@robobun

@robobun robobun commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • The ES2025 Set methods give wrong results for a set-like whose size is 2^32 or more. new Set([1]).isSubsetOf({ size: 2 ** 32, has: v => v === 1, keys }) returns false without calling has(). isDisjointFrom returns true, intersection returns an empty Set, difference keeps every element, and isSupersetOf iterates keys() where the spec returns false after reading size. Sizes 2^32 - 1 and Infinity behave correctly. V8 and SpiderMonkey follow the spec for every size.
  • The cause is getSetSizeAsInt() in Source/JavaScriptCore/runtime/SetPrototype.cpp:215. It special-cases Infinity and passes every other double through static_cast<uint32_t>. That cast is undefined for values of 2^32 and more and wraps in practice, so 2^32 compares as 0 and 2^53 + 2 as 2. The callers then take the wrong branch of "thisSize <= otherSize: call other.has(), else iterate other.keys()".

Fix

  • Clamp at UINT32_MAX instead of casting: if (intOrInfSize >= UINT32_MAX) return UINT32_MAX;. This covers Infinity too, so the isinf() branch goes away.
  • Correct because the callers only compare the value with JSSet::size(), a uint32_t that can never reach UINT32_MAX: the backing storage is capped at IndexingHeader::maximumLength (2^28) slots. A clamped size compares the same way the spec's unbounded integer would.
  • Verified: JSTests/stress/set-methods-set-like-size-beyond-uint32.js (new) checks the result and the observable size/has/keys/next call log of all seven methods for sizes from 2^31 through 2^64, 1e300, Number.MAX_VALUE and Infinity, plus the small-size branches and the RangeError/TypeError paths. It fails on the unpatched engine at isSubsetOf size=4294967296 and passes with this change. The 162 existing JSTests/stress/set-* tests and the 764 test262 built-ins/Set cases (strict and sloppy) pass under the built jsc.

Background

  • GetSetRecord reads obj.size, applies ToNumber then ToIntegerOrInfinity, throws on NaN or a negative value, and stores the result as [[Size]] with no upper bound. intersection, difference and isDisjointFrom branch on SetDataSize(O) <= otherRec.[[Size]], isSubsetOf returns false when SetDataSize(O) > [[Size]], and isSupersetOf returns false when SetDataSize(O) < [[Size]]. union and symmetricDifference read size but do not use it.
  • A real Set argument takes the fastSet* paths and never reaches getSetSizeAsInt(). The function only runs for user-supplied set-likes, where a virtual or predicate-backed set ({ size: Number.MAX_SAFE_INTEGER, has }) is the usual reason to report a huge size.
  • Upstream WebKit main has the same code.

@coderabbitai

coderabbitai Bot commented Sep 8, 2026 •

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 10f0e01a-2fda-4136-b116-8a2cb3142704

📥 Commits

Reviewing files that changed from the base of the PR and between cf1b36e and 50c11bf.

📒 Files selected for processing (2)
  • JSTests/stress/set-methods-set-like-size-beyond-uint32.js
  • Source/JavaScriptCore/runtime/SetPrototype.cpp

Included review availability: Your plan provides up to 5 included reviews per hour; 2 remain after this review.


Walkthrough

The change updates JavaScriptCore Set-like size conversion to clamp values at or above UINT32_MAX. A stress test covers Set method behavior for large, small, negative, and invalid sizes.

Changes

Set-like size handling

Layer / File(s) Summary
Clamp large set-like sizes
Source/JavaScriptCore/runtime/SetPrototype.cpp
getSetSizeAsInt now clamps integer-or-infinity sizes at or above UINT32_MAX. Comments document the bounded comparison behavior.
Validate size-dependent Set methods
JSTests/stress/set-methods-set-like-size-beyond-uint32.js
The stress test covers large and small sizes, access patterns, short-circuit branches, and RangeError or TypeError validation.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 50c11

The size clamping change and its boundary coverage do not leave a concrete merge-blocking risk.

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description gives a detailed, relevant explanation of the bug, fix, scope, and validation. However, it does not include the required Bugzilla URL, review status, or the template-formatted changed-… Add the associated Bugzilla link, include the required review line such as "Reviewed by NOBODY (OOPS!).", and list each changed path with the affected function or class in the repository template format. Ensure the pull request is linked fr…
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary change: clamping set-like sizes at UINT32_MAX instead of truncating them.
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.
Full details: Description check

Explanation

The description gives a detailed, relevant explanation of the bug, fix, scope, and validation. However, it does not include the required Bugzilla URL, review status, or the template-formatted changed-file and function list.

Resolution

Add the associated Bugzilla link, include the required review line such as "Reviewed by NOBODY (OOPS!).", and list each changed path with the affected function or class in the repository template format. Ensure the pull request is linked from Bugzilla and has the required labels and assignment.

  • Fix all pre-merge checks with AI

Warning

Git: CodeRabbit could not clone the repository, so clone-backed analysis was skipped and this review may be incomplete. Verify repository clone access, such as SSH credentials, before requesting another full review. If clone access is intentionally unavailable, use path_filters to narrow the review scope.


Comment @coderabbitai help to get the list of available commands.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this and didn't find any bugs — the clamp is correct and the test is well-formed. Since it changes JSC runtime semantics and both paths are routed to jsc-reviewers in CODEOWNERS, a human sign-off is still worthwhile.

What was reviewed:

  • getSetSizeAsInt: >= compares the double against 4294967295.0 (exactly representable), so +Infinity and every finite value ≥ 2^32-1 clamp; the remaining static_cast<uint32_t> only sees integers in [0, 2^32-2], so the prior UB is gone.
  • Callers only use the result in thisSet->size() <= size / < size comparisons, and a real JSSet cannot reach UINT32_MAX elements, so clamping preserves every branch outcome.
  • New stress test throws on failure, prints nothing on success, has no tier-up loop (pure slow-path C++), and is small enough to stay well under the 200ms budget.
Extended reasoning...

Overview

The PR fixes a spec-compliance bug in the ES2025 Set methods (intersection, difference, union, symmetricDifference, isSubsetOf, isSupersetOf, isDisjointFrom) when the set-like argument reports size >= 2^32. The one-line functional change in Source/JavaScriptCore/runtime/SetPrototype.cpp replaces std::isinf(intOrInfSize) with intOrInfSize >= std::numeric_limits<uint32_t>::max() so that any size at or beyond UINT32_MAX (including +Infinity) is clamped rather than passed to static_cast<uint32_t>, which was undefined behavior for out-of-range doubles and wrapped in practice. A three-line comment documents why clamping is sound. A new 120-line stress test in JSTests/stress/ exercises all seven methods across 13 large sizes plus small-size and error paths, asserting both results and the observable size/has/keys/next call sequence.

Security risks

None identified. This is a pure spec-conformance fix on a slow path that only runs for user-supplied set-like objects (real JSSet arguments take the fast path). No memory management, no allocation sizing, no pointer arithmetic — the returned uint32_t is used solely in <= / < comparisons against JSSet::size() to pick which observable algorithm branch to take. Removing the out-of-range static_cast<uint32_t>(double) actually eliminates a small piece of UB.

Level of scrutiny

Low-to-moderate on the code itself: the diff is a single guard change whose correctness is easy to verify by inspection (the uint32_t limit promotes to an exactly-representable double for the comparison; toIntegerOrInfinity guarantees integral values so the residual cast is well-defined; NaN and negatives are handled earlier). However, .github/CODEOWNERS routes both /Source/JavaScriptCore and /JSTests to WebKit/jsc-reviewers, and per the review guidelines CODEOWNER-covered paths should get a human look even when the change is small.

Other factors

The new test conforms to JSTests/README.md rules: assertions throw, nothing is printed on success, there is no hot loop needing testLoopCount (the fix is in a C++ slow-path helper, not JIT-tiered code), and the total work is a few hundred trivial operations — comfortably under 200ms. The PR description states it fails on an unpatched build and that existing set-* stress tests and test262 built-ins/Set pass; I did not independently run those but the claim is consistent with the diff. No other reviewers have weighed in yet.

@github-actions

github-actions Bot commented Sep 8, 2026 •

Copy link
Copy Markdown

Preview Builds

Commit Release Date
50c11bf6 autobuild-preview-pr-596-50c11bf6 2026-09-11 22:45:47 UTC
a54ec5a2 autobuild-preview-pr-596-a54ec5a2 2026-09-08 21:24:35 UTC

…runcating it

GetSetRecord keeps the set-like's size as an unbounded integer or +Infinity,
and intersection, difference, isSubsetOf, isSupersetOf and isDisjointFrom
compare it with the receiver's size to decide whether to call other.has()
or to iterate other.keys(). getSetSizeAsInt() special-cased Infinity but
passed every other double through static_cast<uint32_t>, which is undefined
for values of 2^32 and more and wraps in practice. A set-like reporting
size 2^32 compared as size 0, so these methods took the wrong branch:
new Set([1]).isSubsetOf({ size: 2 ** 32, has: () => true, keys }) returned
false without calling has(), and isSupersetOf iterated keys() where the
spec returns false after reading size.

Clamp instead. JSSet::size() can never reach UINT32_MAX (the backing
storage is capped at IndexingHeader::maximumLength slots), so a clamped
size compares the same way the exact one would. This also covers Infinity,
so the isinf() branch goes away.

* JSTests/stress/set-methods-set-like-size-beyond-uint32.js: Added.
* Source/JavaScriptCore/runtime/SetPrototype.cpp:
(JSC::getSetSizeAsInt):
@robobun
robobun force-pushed the robobun/1332550e/set-like-size-saturate branch from a54ec5a to 50c11bf Compare September 11, 2026 22:18

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

robobun added a commit to oven-sh/bun that referenced this pull request Sep 11, 2026
…r more (WebKit bump for oven-sh/WebKit#596)

GetSetRecord keeps a set-like's size as an unbounded integer. JSC truncated
it to uint32 before comparing it with the receiver's size, so intersection,
difference, isSubsetOf, isSupersetOf and isDisjointFrom consulted the wrong
side (other.keys() instead of other.has(), or the reverse) once the reported
size reached 2^32. Pin WebKit to the preview build of oven-sh/WebKit#596,
which clamps the size at UINT32_MAX instead, and add coverage for all seven
methods across the size boundary.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant