From bb622e7bf525f8ffea4c392c3a4c9314ae982927 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 8 Sep 2026 22:18:58 +0000 Subject: [PATCH 1/2] [JSC] BigInt64Array / BigUint64Array indexOf, lastIndexOf and includes: 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. --- ...igint-typed-array-index-of-out-of-range.js | 124 ++++++++++++++++++ Source/JavaScriptCore/runtime/JSBigInt.h | 4 + .../JavaScriptCore/runtime/JSBigIntInlines.h | 34 +++++ .../runtime/ToNativeFromValue.h | 5 +- 4 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 JSTests/stress/bigint-typed-array-index-of-out-of-range.js diff --git a/JSTests/stress/bigint-typed-array-index-of-out-of-range.js b/JSTests/stress/bigint-typed-array-index-of-out-of-range.js new file mode 100644 index 0000000000000..c1e392eb9e3d8 --- /dev/null +++ b/JSTests/stress/bigint-typed-array-index-of-out-of-range.js @@ -0,0 +1,124 @@ +function shouldBe(actual, expected, message) { + if (actual !== expected) + throw new Error(message + ": expected " + expected + " but got " + actual); +} + +// %TypedArray%.prototype.indexOf / lastIndexOf compare with IsStrictlyEqual and includes with SameValueZero +// against the element's BigInt value. A BigInt that is not representable in the element type can never match, +// even when it is congruent to an element modulo 2^64. + +function test(array, needle, expectedIndex) { + let name = array.constructor.name + "(" + array.length + ") needle " + needle + "n"; + shouldBe(array.indexOf(needle), expectedIndex, name + " indexOf"); + shouldBe(array.lastIndexOf(needle), expectedIndex, name + " lastIndexOf"); + shouldBe(array.includes(needle), expectedIndex !== -1, name + " includes"); + shouldBe(array.indexOf(needle, 1), expectedIndex >= 1 ? expectedIndex : -1, name + " indexOf fromIndex 1"); + shouldBe(array.lastIndexOf(needle, -2), expectedIndex <= array.length - 2 ? expectedIndex : -1, name + " lastIndexOf fromIndex -2"); + shouldBe(array.includes(needle, -1), expectedIndex === array.length - 1, name + " includes fromIndex -1"); + shouldBe(Array.prototype.indexOf.call(array, needle), expectedIndex, name + " Array.prototype.indexOf"); + shouldBe(Array.prototype.includes.call(array, needle), expectedIndex !== -1, name + " Array.prototype.includes"); +} + +const int64Max = 2n ** 63n - 1n; +const int64Min = -(2n ** 63n); +const uint64Max = 2n ** 64n - 1n; + +const int64Cases = [ + // [elements, needle, expected index] + [[3n, -1n, 7n], 2n ** 64n - 1n, -1], // wraps to -1n + [[3n, -1n, 7n], 2n ** 64n + 3n, -1], // wraps to 3n + [[3n, -1n, 7n], -(2n ** 64n) + 7n, -1], // wraps to 7n + [[3n, 0n, 7n], 2n ** 64n, -1], // wraps to 0n + [[3n, 0n, 7n], -(2n ** 64n), -1], // wraps to 0n + [[3n, 0n, 7n], 2n ** 128n, -1], // wraps to 0n + [[3n, int64Min, 7n], 2n ** 63n, -1], // wraps to -2^63 + [[3n, int64Max, 7n], int64Min - 1n, -1], // wraps to 2^63 - 1 + [[3n, 5n, 7n], 2n ** 200n + 5n, -1], // many digits, low 64 bits are 5 + [[3n, -5n, 7n], -(2n ** 200n) - 5n, -1], + // Representable needles still match, including the boundaries. + [[3n, -1n, 7n], -1n, 1], + [[3n, -1n, 7n], 3n, 0], + [[3n, -1n, 7n], 7n, 2], + [[3n, 0n, 7n], 0n, 1], + [[3n, 0n, 7n], -0n, 1], + [[3n, int64Max, 7n], int64Max, 1], + [[3n, int64Min, 7n], int64Min, 1], + [[3n, 2n ** 32n, 7n], 2n ** 32n, 1], + [[3n, -(2n ** 32n), 7n], -(2n ** 32n), 1], + [[3n, -1n, 7n], 4n, -1], +]; + +const uint64Cases = [ + [[3n, uint64Max, 7n], -1n, -1], // wraps to 2^64 - 1 + [[3n, int64Max, 7n], int64Min - 1n, -1], // wraps to 2^63 - 1 + [[3n, 2n ** 63n, 7n], int64Min, -1], // wraps to 2^63 + [[3n, 0n, 7n], 2n ** 64n, -1], // wraps to 0n + [[3n, 0n, 7n], -(2n ** 64n), -1], // wraps to 0n + [[3n, 0n, 7n], 2n ** 128n, -1], // wraps to 0n + [[3n, 5n, 7n], 2n ** 64n + 5n, -1], // wraps to 5n + [[3n, 5n, 7n], -(2n ** 64n) + 5n, -1], // wraps to 5n + [[3n, 5n, 7n], 2n ** 200n + 5n, -1], + [[3n, uint64Max - 4n, 7n], -5n, -1], // wraps to 2^64 - 5 + // Representable needles still match, including the boundaries. + [[3n, uint64Max, 7n], uint64Max, 1], + [[3n, 0n, 7n], 0n, 1], + [[3n, 0n, 7n], -0n, 1], + [[3n, 2n ** 63n, 7n], 2n ** 63n, 1], + [[3n, int64Max, 7n], int64Max, 1], + [[3n, 2n ** 32n, 7n], 2n ** 32n, 1], + [[3n, 5n, 7n], 3n, 0], + [[3n, 5n, 7n], 7n, 2], + [[3n, 5n, 7n], 4n, -1], +]; + +function makeViews(constructor, elements) { + let views = []; + + views.push(new constructor(elements)); + + let padded = new constructor(elements.length + 2); + padded.set(elements, 1); + views.push(padded.subarray(1, elements.length + 1)); + + let resizable = new ArrayBuffer(elements.length * 8, { maxByteLength: elements.length * 8 + 64 }); + let tracking = new constructor(resizable); + tracking.set(elements); + views.push(tracking); + + let fixedWindow = new constructor(resizable, 0, elements.length); + views.push(fixedWindow); + + let shared = new constructor(new SharedArrayBuffer(elements.length * 8)); + shared.set(elements); + views.push(shared); + + return views; +} + +function run() { + for (let [elements, needle, expected] of int64Cases) { + for (let view of makeViews(BigInt64Array, elements)) + test(view, needle, expected); + } + for (let [elements, needle, expected] of uint64Cases) { + for (let view of makeViews(BigUint64Array, elements)) + test(view, needle, expected); + } +} + +for (let i = 0; i < 200; ++i) + run(); + +// A non-BigInt needle never matches a BigInt element, and a BigInt needle never matches a Number element. +{ + let i64 = new BigInt64Array([0n, 1n, -1n]); + shouldBe(i64.indexOf(1), -1, "BigInt64Array indexOf Number"); + shouldBe(i64.includes(-1), false, "BigInt64Array includes Number"); + shouldBe(i64.lastIndexOf("1"), -1, "BigInt64Array lastIndexOf String"); + let u64 = new BigUint64Array([0n, 1n, uint64Max]); + shouldBe(u64.indexOf(2 ** 64), -1, "BigUint64Array indexOf Number 2^64"); + shouldBe(u64.includes(18446744073709551615), false, "BigUint64Array includes Number 2^64-1"); + let f64 = new Float64Array([0, 1, 2 ** 64]); + shouldBe(f64.indexOf(2n ** 64n), -1, "Float64Array indexOf BigInt"); + shouldBe(f64.includes(1n), false, "Float64Array includes BigInt"); +} diff --git a/Source/JavaScriptCore/runtime/JSBigInt.h b/Source/JavaScriptCore/runtime/JSBigInt.h index 5be165fb18c96..49034dbc3efae 100644 --- a/Source/JavaScriptCore/runtime/JSBigInt.h +++ b/Source/JavaScriptCore/runtime/JSBigInt.h @@ -457,8 +457,12 @@ class JSBigInt final : public JSCell { static JSValue asUintN(JSGlobalObject*, uint64_t numberOfBits, int32_t bigIntAsInt32); #endif + // ToBigUint64 / ToBigInt64: the value modulo 2^64. inline static uint64_t toBigUInt64(JSValue); // Defined in JSBigIntInlines.h inline static int64_t toBigInt64(JSValue); // Defined in JSBigIntInlines.h + // nullopt instead of wrapping when the value is not representable. + inline static std::optional tryGetAsUint64(JSValue); // Defined in JSBigIntInlines.h + inline static std::optional tryGetAsInt64(JSValue); // Defined in JSBigIntInlines.h Digit digit(unsigned) const; void setDigit(unsigned, Digit); // Use only when initializing. diff --git a/Source/JavaScriptCore/runtime/JSBigIntInlines.h b/Source/JavaScriptCore/runtime/JSBigIntInlines.h index 38862358fde91..a6d9d80c9f5ef 100644 --- a/Source/JavaScriptCore/runtime/JSBigIntInlines.h +++ b/Source/JavaScriptCore/runtime/JSBigIntInlines.h @@ -60,6 +60,40 @@ inline int64_t JSBigInt::toBigInt64(JSValue bigInt) return static_cast(toBigUInt64Heap(bigInt.asHeapBigInt())); } +inline std::optional JSBigInt::tryGetAsUint64(JSValue bigInt) +{ + ASSERT(bigInt.isBigInt()); +#if USE(BIGINT32) + if (bigInt.isBigInt32()) { + int32_t value = bigInt.bigInt32AsInt32(); + if (value < 0) + return std::nullopt; + return static_cast(value); + } +#endif + JSBigInt* heapBigInt = bigInt.asHeapBigInt(); + if (heapBigInt->sign() || heapBigInt->length() > sizeof(uint64_t) / sizeof(Digit)) + return std::nullopt; + return toBigUInt64Heap(heapBigInt); +} + +inline std::optional JSBigInt::tryGetAsInt64(JSValue bigInt) +{ + ASSERT(bigInt.isBigInt()); +#if USE(BIGINT32) + if (bigInt.isBigInt32()) + return static_cast(bigInt.bigInt32AsInt32()); +#endif + JSBigInt* heapBigInt = bigInt.asHeapBigInt(); + if (heapBigInt->length() > sizeof(uint64_t) / sizeof(Digit)) + return std::nullopt; + // The magnitude is below 2^64 here, so the wrapped value has the BigInt's sign exactly when the BigInt is in [-2^63, 2^63 - 1]. + int64_t value = static_cast(toBigUInt64Heap(heapBigInt)); + if ((value < 0) != heapBigInt->sign()) + return std::nullopt; + return value; +} + ALWAYS_INLINE std::optional JSBigInt::tryExtractDouble(JSValue value) { if (value.isNumber()) diff --git a/Source/JavaScriptCore/runtime/ToNativeFromValue.h b/Source/JavaScriptCore/runtime/ToNativeFromValue.h index 3c4c5d42727e6..73ef1762fd6fd 100644 --- a/Source/JavaScriptCore/runtime/ToNativeFromValue.h +++ b/Source/JavaScriptCore/runtime/ToNativeFromValue.h @@ -63,10 +63,11 @@ std::optional toNativeFromValueWithoutCoercion(JSValue v if constexpr (std::is_same_v || std::is_same_v) { if (!value.isBigInt()) return std::nullopt; + // Not toBigInt64 / toBigUInt64: a BigInt the element type cannot represent matches no element, even one it is congruent to modulo 2^64. if constexpr (std::is_same_v) - return JSBigInt::toBigInt64(value); + return JSBigInt::tryGetAsInt64(value); else - return JSBigInt::toBigUInt64(value); + return JSBigInt::tryGetAsUint64(value); } else { if (!value.isNumber()) return std::nullopt; From 85d30e9dc0afa321899ce461bdbd59d302ec7c37 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 8 Sep 2026 23:27:31 +0000 Subject: [PATCH 2/2] Test: iterate with testLoopCount and cover a repeated element --- ...igint-typed-array-index-of-out-of-range.js | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/JSTests/stress/bigint-typed-array-index-of-out-of-range.js b/JSTests/stress/bigint-typed-array-index-of-out-of-range.js index c1e392eb9e3d8..f1f0c328dee83 100644 --- a/JSTests/stress/bigint-typed-array-index-of-out-of-range.js +++ b/JSTests/stress/bigint-typed-array-index-of-out-of-range.js @@ -95,19 +95,34 @@ function makeViews(constructor, elements) { return views; } -function run() { - for (let [elements, needle, expected] of int64Cases) { - for (let view of makeViews(BigInt64Array, elements)) - test(view, needle, expected); - } - for (let [elements, needle, expected] of uint64Cases) { - for (let view of makeViews(BigUint64Array, elements)) - test(view, needle, expected); - } +let rows = []; +for (let [elements, needle, expected] of int64Cases) { + for (let view of makeViews(BigInt64Array, elements)) + rows.push([view, needle, expected]); +} +for (let [elements, needle, expected] of uint64Cases) { + for (let view of makeViews(BigUint64Array, elements)) + rows.push([view, needle, expected]); } -for (let i = 0; i < 200; ++i) - run(); +for (let i = 0; i < Math.max(testLoopCount, rows.length); ++i) { + let [view, needle, expected] = rows[i % rows.length]; + test(view, needle, expected); +} + +// A repeated element: indexOf finds the first match, lastIndexOf the last, and a wrapped needle neither. +for (let constructor of [BigInt64Array, BigUint64Array]) { + for (let view of makeViews(constructor, [7n, 3n, 7n, 7n, 3n])) { + let name = constructor.name + " repeated 7n"; + shouldBe(view.indexOf(7n), 0, name + " indexOf"); + shouldBe(view.lastIndexOf(7n), 3, name + " lastIndexOf"); + shouldBe(view.indexOf(7n, 1), 2, name + " indexOf fromIndex 1"); + shouldBe(view.lastIndexOf(7n, -3), 2, name + " lastIndexOf fromIndex -3"); + shouldBe(view.indexOf(7n + 2n ** 64n), -1, name + " wrapped indexOf"); + shouldBe(view.lastIndexOf(7n + 2n ** 64n), -1, name + " wrapped lastIndexOf"); + shouldBe(view.includes(7n + 2n ** 64n), false, name + " wrapped includes"); + } +} // A non-BigInt needle never matches a BigInt element, and a BigInt needle never matches a Number element. {