Skip to content

[JSC] BigInt64Array / BigUint64Array indexOf, lastIndexOf and includes: an out-of-range BigInt needle matches nothing - #607

Closed
robobun wants to merge 2 commits into
mainfrom
robobun/a30e3802/bigint-typed-array-indexof-range
Closed

robobun wants to merge 2 commits into
mainfrom
robobun/a30e3802/bigint-typed-array-indexof-range

Conversation

@robobun

@robobun robobun commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • BigInt64Array / BigUint64Array .indexOf, .lastIndexOf and .includes match a BigInt needle that no element equals when it is congruent to an element modulo 2^64. new BigInt64Array([3n, -1n]).indexOf(2n ** 64n - 1n) is 1 and new BigUint64Array([0n]).includes(-(2n ** 64n)) is true. V8, SpiderMonkey and JSC's own Array.prototype.indexOf.call(array, needle) give -1 / false.
  • The cause is toNativeFromValueWithoutCoercion() (Source/JavaScriptCore/runtime/ToNativeFromValue.h:61). The three functions convert the needle to the element type once, then compare raw elements. The BigInt adaptors converted with JSBigInt::toBigInt64 / toBigUInt64, which are ToBigInt64 / ToBigUint64: the value modulo 2^64.

Fix

  • Add JSBigInt::tryGetAsInt64(JSValue) / tryGetAsUint64(JSValue) next to the wrapping conversions. They return nullopt when the BigInt is not representable. toNativeFromValueWithoutCoercion() calls them, so such a needle gives -1 / false, like a needle that is not a BigInt.
  • Correct because the spec compares the needle with each element's BigInt value (IsStrictlyEqual, SameValueZero), and every element is in the element type's range. A needle in range converts to the same bits as before. The Number adaptors already reject a needle the element type cannot hold.
  • Verified: JSTests/stress/bigint-typed-array-index-of-out-of-range.js (new) fails on the unpatched jsc (needle 18446744073709551615n indexOf: expected -1 but got 1) and passes with the change, also with --useJIT=0 and eager tier-up. Related stress tests and test262 directories pass (see Notes).

Background

  • A heap JSBigInt is a sign bit plus little-endian Digits with no leading zero digit. More digits than fit in 64 bits means a magnitude of at least 2^64. With USE(BIGINT32) a small BigInt is an immediate int32_t instead.
  • For a magnitude m < 2^64 the wrapped value is m (positive) or 2^64 - m (negative). As int64_t it has the BigInt's sign exactly when the BigInt is in [-2^63, 2^63 - 1]. The DFG's DataViewSet path for setBigInt64 makes the same test.
  • Upstream WebKit main has the same code.
Notes
  • The DFG inlines indexOf / includes only for JSArray shapes (ArrayIndexOfIntrinsic), so these C++ prototype functions are the only path for typed arrays. That matches the report: the result is the same with every JIT tier off.
  • The new stress test covers both constructors through a plain view, a subarray, a length-tracking and a fixed-length view of a resizable buffer, and a SharedArrayBuffer view, with and without fromIndex, for needles that wrap to an element (2^64 - 1, 2^64 + 3, ±2^64, 2^128, 2^63, -2^63 - 1, ±(2^200 + 5), -5) and for the representable boundaries (0n, -1n, ±2^32, 2^63 - 1, -2^63, 2^63, 2^64 - 1), and checks that Array.prototype.indexOf / includes agree.
  • Stress tests run against the patched jsc: typedarray-indexOf, typedarray-lastIndexOf, typedarray-includes, typedarray-resize-includes, non-suitable-typed-array-index-of, typed-array-lastIndexOf-exception-check, bigint-typed-array-*, bigint-* (with their requireOptions), array-indexof-*, array-includes-*.
  • test262 directories run (harness files concatenated, module and async tests skipped): built-ins/TypedArray/prototype/{indexOf,lastIndexOf,includes} and built-ins/Array/prototype/{indexOf,lastIndexOf,includes}: 559 pass, 0 fail. built-ins/BigInt, built-ins/TypedArrayConstructors/Big{Int,Uint}64Array, built-ins/DataView/prototype/{get,set}BigInt64, built-ins/Atomics: the same 458 pass / 12 fail as the unpatched build (immutable ArrayBuffer and Atomics.wait agent tests).
  • I first wrote the check as a round-trip through JSBigInt::compare(value, int64). That helper's Int64BigIntImpl reports length() == 2 for any non-zero value on 32-bit targets, so it would compare 5n unequal to 5 there. The dedicated helpers avoid it and are cheaper.

…s: an out-of-range BigInt needle matches nothing

The three prototype functions convert the needle to the element type
once and then compare raw elements. For the BigInt adaptors,
toNativeFromValueWithoutCoercion() converted with JSBigInt::toBigInt64 /
toBigUInt64, which are ToBigInt64 / ToBigUint64: the value modulo 2^64.
So new BigInt64Array([3n, -1n]).indexOf(2n ** 64n - 1n) returned 1 and
new BigUint64Array([0n]).includes(-(2n ** 64n)) returned true. The spec
compares with IsStrictlyEqual / SameValueZero against each element's
BigInt value, so a needle outside the element type's range matches no
element. V8 and SpiderMonkey return -1 / false.

Add JSBigInt::tryGetAsInt64 / tryGetAsUint64, which return nullopt
instead of wrapping when the BigInt is not representable, and use them
in toNativeFromValueWithoutCoercion(). The Number adaptors already
reject a needle the element type cannot hold.

@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.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline finding, I also checked that toNativeFromValueWithoutCoercion has no other callers than the three %TypedArray%.prototype search functions (so the semantic change is scoped to indexOf/lastIndexOf/includes), that tryGetAsInt64 handles the INT64_MIN boundary correctly (magnitude 2^63 with sign → wrapped value is negative, sign matches), and that the includes nullopt path with a shrunk resizable buffer still returns false for an out-of-range BigInt since it is not undefined.

Extended reasoning...

The fix itself looks correct and narrowly scoped. I traced toNativeFromValueWithoutCoercion to its only call site (JSGenericTypedArrayView::toAdaptorNativeFromValueWithoutCoercion), which in turn is used only by genericTypedArrayViewProtoFuncIncludes, genericTypedArrayViewProtoFuncIndexOf, and genericTypedArrayViewProtoFuncLastIndexOf in JSGenericTypedArrayViewPrototypeFunctions.h, so switching from wrapping to exact-representation semantics cannot leak into element writes or other conversions. I walked the sign/magnitude cases of tryGetAsInt64 against toBigUInt64Heap's two's-complement output and confirmed the (value < 0) != sign() test admits exactly [-2^63, 2^63 - 1], including the -2^63 corner where the wrapped value is 0x8000… and thus negative. The sizeof(uint64_t) / sizeof(Digit) bound is correct for both 32- and 64-bit Digit given JSBigInt's no-leading-zero-digit invariant. The includes early-return at JSGenericTypedArrayViewPrototypeFunctions.h:526 was checked because it now fires for out-of-range BigInts too; it keys off valueToFind.isUndefined(), which a BigInt never is, so behavior is correct. Since the changed paths are CODEOWNERS-covered (Source/JavaScriptCore and JSTests → WebKit/jsc-reviewers) and there is an unposted verified finding alongside the inline nit, deferring rather than approving.

Comment thread JSTests/stress/bigint-typed-array-index-of-out-of-range.js Outdated
@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: 5e5739b7-3576-4b5a-83d0-55322010a07f

📥 Commits

Reviewing files that changed from the base of the PR and between bb622e7 and 85d30e9.

📒 Files selected for processing (1)
  • JSTests/stress/bigint-typed-array-index-of-out-of-range.js

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


Walkthrough

The change adds checked BigInt-to-64-bit conversions, uses them for typed-array matching, and adds stress coverage for representable, out-of-range, wrapped, boundary, and mismatched typed-array search values.

Changes

BigInt typed-array matching

Layer / File(s) Summary
Checked BigInt conversions
Source/JavaScriptCore/runtime/JSBigInt.h, Source/JavaScriptCore/runtime/JSBigIntInlines.h
tryGetAsUint64 and tryGetAsInt64 return optional values. They reject BigInts outside the target range.
Typed-array conversion integration
Source/JavaScriptCore/runtime/ToNativeFromValue.h
Non-coercing BigInt conversion uses checked extraction instead of modulo-based conversion.
Search behavior validation
JSTests/stress/bigint-typed-array-index-of-out-of-range.js
Tests cover search methods, signed and unsigned boundaries, multiple buffer views, repeated execution, and mismatched needle types.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 85d30

The change rejects out-of-range BigInt typed-array search needles rather than matching modulo-wrapped values, with coverage for boundaries, views, repeated elements, and search indices. No merge-blocking risk is identified.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the affected typed arrays, methods, and out-of-range BigInt behavior.
Description check ✅ Passed The description is detailed and relevant. It explains the problem, root cause, fix, implementation details, affected files, and test results. It does not include the required Bugzilla URL or an explic…
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.

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.

@robobun

robobun commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator Author

Cross-reference: #609 fixes the same toNativeFromValueWithoutCoercion helper for all three needle representations (this path, plus the Uint8Clamped double path from the original report). The two PRs touch the same lines, so only one shape should merge. See the note on #609.

@github-actions

github-actions Bot commented Sep 8, 2026 •

Copy link
Copy Markdown

Preview Builds

Commit Release Date
85d30e9d autobuild-preview-pr-607-85d30e9d 2026-09-08 23:55:53 UTC
bb622e7b autobuild-preview-pr-607-bb622e7b 2026-09-08 22:51:48 UTC

@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.

LGTM — the testLoopCount nit from the previous review is addressed, and the added repeated-element cases are a nice extra.

What was reviewed:

  • Verified tryGetAsInt64/tryGetAsUint64 range logic against the heap-BigInt digit representation on both 32- and 64-bit Digit sizes, including the -2^63 and zero edge cases.
  • Confirmed toNativeFromValueWithoutCoercion reaches only the three typed-array search prototypes (includes/indexOf/lastIndexOf in JSGenericTypedArrayViewPrototypeFunctions.h), so no store or coercion path picks up the non-wrapping behavior.
  • Checked the new stress test against JSTests/README.md conventions: throws on failure, no logging, now driven by testLoopCount.
Extended reasoning...

Overview

This PR fixes a spec-compliance bug where BigInt64Array/BigUint64Array indexOf/lastIndexOf/includes would wrap an out-of-range BigInt needle modulo 2^64 and spuriously match a congruent element. It adds two non-wrapping helpers (JSBigInt::tryGetAsUint64, JSBigInt::tryGetAsInt64) in JSBigIntInlines.h that return std::nullopt when the value does not fit, and switches toNativeFromValueWithoutCoercion in ToNativeFromValue.h to call them for the BigInt adaptors. A new stress test covers wrapped needles, boundaries, multiple view kinds (plain, subarray, resizable length-tracking and fixed-window, SharedArrayBuffer), repeated elements, and cross-type Number/BigInt mismatches. Since the previous review, commit 85d30e9 replaced the hard-coded 200-iteration loop with Math.max(testLoopCount, rows.length) and added repeated-element first/last-match cases.

Security risks

None identified. The change narrows behavior (returns nullopt in more cases) rather than widening it, and the affected function is used only to convert a search needle for equality comparison — not for indexing, bounds checks, or memory writes. No allocator, pointer arithmetic, or GC interaction is touched. The static_cast<int64_t> of a uint64_t is implementation-defined wrapping that JSC already relies on in the adjacent toBigInt64.

Level of scrutiny

Moderate. This is JSC runtime code, so I walked the range logic by hand: for tryGetAsUint64, negative sign or length() > 64/sizeof(Digit) (1 on 64-bit, 2 on 32-bit) correctly rejects anything ≥ 2^64 or < 0. For tryGetAsInt64, once the magnitude is < 2^64, comparing the sign of the wrapped int64_t against heapBigInt->sign() accepts exactly [-2^63, 2^63 - 1]; the -2^63 case (magnitude 2^63 → wrapped uint64 2^63 → int64 INT64_MIN, negative, matches sign()==true) and the zero case (length()==0, wrapped 0, sign()==false) both pass. I grepped all callers of toNativeFromValueWithoutCoercion / toAdaptorNativeFromValueWithoutCoercion and confirmed only the three search prototypes reach it, so set, fill, DataView, and Atomics paths keep their spec-required wrapping semantics.

Other factors

The new helpers follow the exact structure of the existing toBigUInt64/toBigInt64 immediately above them, and the DFG's DataViewSet int64 path uses the same sign-mismatch test the PR notes. The stress test now conforms to JSTests/README.md (throws on failure, no output, testLoopCount-driven), addressing the only point from the prior review. The change is small, self-contained, spec-aligned, and well-tested; approving.

@robobun

robobun commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #609. That change replaces the modular toBigInt64 / toBigUInt64 in toNativeFromValueWithoutCoercion with range-checked JSBigInt::tryExtractInt64 / tryExtractUInt64, equivalent to the tryGetAsInt64 / tryGetAsUint64 here, and also fixes the Float32 / Float16 int32 and Uint8Clamped paths of the same helper. Its stress test JSTests/stress/typed-array-index-of-unrepresentable-needle.js covers the out-of-range BigInt64 / BigUint64 rows (2^64 +/- 1, 2^63, -(2^63) - 1, 2^128, 2^200). The Bun-side bump is oven-sh/bun#42093.

@robobun robobun closed this Sep 9, 2026
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.

2 participants