From 7a274ac46e3d9c07d24b8260a00d7cf47fdc4da7 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 8 Sep 2026 22:31:53 +0000 Subject: [PATCH 1/2] [JSC] %TypedArray%.prototype.indexOf / lastIndexOf / includes: never wrap, clamp or round the search value indexOf and lastIndexOf compare the search value with each element by IsStrictlyEqual, and includes by SameValueZero. JSC converts the search value to the element type once (toNativeFromValueWithoutCoercion) and then scans the raw storage. That conversion must fail when the element type cannot represent the value exactly. Three of its paths did not: * A BigInt needle went through JSBigInt::toBigInt64 / toBigUInt64, which are the modular ToBigInt64 / ToBigUint64 used for stores. So new BigInt64Array([1n]).includes(2n ** 64n + 1n) and new BigUint64Array([2n ** 64n - 1n]).includes(-1n) were true. * An int32 needle for Float32Array / Float16Array was cast to the element type with no round-trip check, so new Float32Array([16777216]) .indexOf(16777217) and new Float16Array([Infinity]).indexOf(65536) were 0. * A double needle for Uint8ClampedArray was converted with static_cast(double), which is undefined behavior outside (-1, 256). LLVM 22 (bun links its LTO build with rust-lld) folds the round-trip check uint8 -> double == value into trunc(value) == value, so every integral double matched the low byte of cvttsd2si(value): -1 found 255, and 256, 1e10, 2 ** 31 and Infinity found 0. oven-sh#205 fixed the same cast in IntegralTypedArrayAdaptor but not here. Add JSBigInt::tryExtractInt64 / tryExtractUInt64 next to tryExtractDouble and use them for the BigInt path. Route the int32 path of the two float adaptors through their checked double path. Convert the Uint8Clamped double needle through truncateDoubleToInt64, as IntegralTypedArrayAdaptor does. Also do the range check before the narrowing cast in the float adaptors' double path, since converting a finite double beyond FLT_MAX (or 65504) to float (or _Float16) is undefined behavior as well. * JSTests/stress/typed-array-index-of-unrepresentable-needle.js: Added. * Source/JavaScriptCore/runtime/JSBigInt.h: * Source/JavaScriptCore/runtime/JSBigIntInlines.h: (JSC::JSBigInt::absoluteAsUInt64): (JSC::JSBigInt::tryExtractDouble): Share the digit gathering with the new helpers. (JSC::JSBigInt::tryExtractInt64): (JSC::JSBigInt::tryExtractUInt64): * Source/JavaScriptCore/runtime/ToNativeFromValue.h: (JSC::toNativeFromValueWithoutCoercion): * Source/JavaScriptCore/runtime/TypedArrayAdaptors.h: (JSC::FloatTypedArrayAdaptor::toNativeFromInt32WithoutCoercion): (JSC::FloatTypedArrayAdaptor::toNativeFromDoubleWithoutCoercion): (JSC::Uint8ClampedAdaptor::toNativeFromDoubleWithoutCoercion): (JSC::Float16Adaptor::toNativeFromInt32WithoutCoercion): (JSC::Float16Adaptor::toNativeFromDoubleWithoutCoercion): --- ...d-array-index-of-unrepresentable-needle.js | 251 ++++++++++++++++++ Source/JavaScriptCore/runtime/JSBigInt.h | 5 + .../JavaScriptCore/runtime/JSBigIntInlines.h | 77 ++++-- .../runtime/ToNativeFromValue.h | 6 +- .../runtime/TypedArrayAdaptors.h | 25 +- 5 files changed, 336 insertions(+), 28 deletions(-) create mode 100644 JSTests/stress/typed-array-index-of-unrepresentable-needle.js diff --git a/JSTests/stress/typed-array-index-of-unrepresentable-needle.js b/JSTests/stress/typed-array-index-of-unrepresentable-needle.js new file mode 100644 index 0000000000000..ae2a3e9818105 --- /dev/null +++ b/JSTests/stress/typed-array-index-of-unrepresentable-needle.js @@ -0,0 +1,251 @@ +// %TypedArray%.prototype.indexOf / lastIndexOf compare with IsStrictlyEqual and includes with SameValueZero, +// against the element values as Numbers / BigInts. A needle the element type cannot represent exactly +// matches nothing, whatever its representation (int32, double, BigInt). It must not be wrapped, clamped +// or rounded into a value that does exist in the array. + +function shouldBe(actual, expected, message) { + if (actual !== expected) + throw new Error(message + ": expected " + expected + " but got " + actual); +} + +function expectFound(array, needle, index, description) { + shouldBe(array.indexOf(needle), index, description + " indexOf"); + shouldBe(array.lastIndexOf(needle), index, description + " lastIndexOf"); + shouldBe(array.includes(needle), true, description + " includes"); +} + +function expectMissing(array, needle, description) { + shouldBe(array.indexOf(needle), -1, description + " indexOf"); + shouldBe(array.lastIndexOf(needle), -1, description + " lastIndexOf"); + shouldBe(array.includes(needle), false, description + " includes"); +} + +// A Float64Array element read always produces a double-encoded JSValue, even for an integral value that +// would otherwise be stored as an int32. This is how we get e.g. -1 and 256 onto the double path. +const doubleBox = new Float64Array(1); +function asDouble(number) { + doubleBox[0] = number; + return doubleBox[0]; +} + +const bitsBox = new BigUint64Array(doubleBox.buffer); +function nextDoubleUp(number) { + doubleBox[0] = number; + bitsBox[0] = bitsBox[0] + 1n; + return doubleBox[0]; +} + +function testUint8Clamped() { + const array = new Uint8ClampedArray([7, 0, 255]); + const name = "Uint8ClampedArray([7, 0, 255])"; + + expectFound(array, 7, 0, name + " int32 7"); + expectFound(array, 0, 1, name + " int32 0"); + expectFound(array, 255, 2, name + " int32 255"); + expectFound(array, asDouble(7), 0, name + " double 7"); + expectFound(array, asDouble(0), 1, name + " double 0"); + expectFound(array, -0, 1, name + " -0"); + expectFound(array, asDouble(255), 2, name + " double 255"); + + for (const needle of [-1, 256, 300, 511, 263, -249, 0x7fffffff, -0x80000000]) + expectMissing(array, needle, name + " int32 " + needle); + + // Every one of these used to match: the needle was reduced to the low byte of cvttsd2si(needle). + for (const needle of [-1, 256, 263, -249, 1e10, -1e10, 2 ** 31, -(2 ** 31), 2 ** 31 - 1, 2 ** 32, 2 ** 32 + 7, 2 ** 53, -(2 ** 53), 2 ** 64, Number.MAX_VALUE, Infinity, -Infinity]) + expectMissing(array, asDouble(needle), name + " double " + needle); + + for (const needle of [0.5, -0.5, 7.5, 255.5, 254.99999999999997, Number.MIN_VALUE, -Number.MIN_VALUE]) + expectMissing(array, needle, name + " fractional " + needle); + + shouldBe(array.indexOf(NaN), -1, name + " indexOf NaN"); + shouldBe(array.lastIndexOf(NaN), -1, name + " lastIndexOf NaN"); + shouldBe(array.includes(NaN), false, name + " includes NaN"); + + expectMissing(array, 7n, name + " BigInt 7n"); + expectMissing(array, "7", name + " string '7'"); +} + +function testIntegral() { + const cases = [ + [Int8Array, [7, 0, -1, -128, 127], [[255, -1], [128, -1], [-129, -1], [383, -1], [2 ** 31 - 1, -1], [-(2 ** 31), -1]]], + [Uint8Array, [7, 0, 255], [[-1, -1], [256, -1], [511, -1], [-249, -1], [2 ** 31 - 1, -1]]], + [Int16Array, [7, 0, -1, -32768, 32767], [[65535, -1], [32768, -1], [-32769, -1], [65543, -1]]], + [Uint16Array, [7, 0, 65535], [[-1, -1], [65536, -1], [65543, -1], [-65529, -1]]], + [Int32Array, [7, 0, -1, -(2 ** 31), 2 ** 31 - 1], [[2 ** 31, -1], [2 ** 32 - 1, -1], [-(2 ** 31) - 1, -1], [2 ** 32 + 7, -1]]], + [Uint32Array, [7, 0, 2 ** 32 - 1, 2 ** 31], [[-1, -1], [2 ** 32, -1], [2 ** 32 + 7, -1], [-(2 ** 31), -1]]], + ]; + for (const [constructor, elements, missing] of cases) { + const array = new constructor(elements); + const name = constructor.name + "(" + elements + ")"; + elements.forEach((element, index) => { + expectFound(array, element, index, name + " int32-or-double " + element); + expectFound(array, asDouble(element), index, name + " double " + element); + }); + for (const [needle] of missing) { + expectMissing(array, needle, name + " " + needle); + expectMissing(array, asDouble(needle), name + " double " + needle); + } + for (const needle of [1e10, -1e10, 2 ** 53, -(2 ** 53), 2 ** 63, 2 ** 64, -(2 ** 64), Number.MAX_VALUE, Infinity, -Infinity, 0.5, -0.5, 7.5]) + expectMissing(array, asDouble(needle), name + " double " + needle); + expectFound(array, -0, 1, name + " -0"); + shouldBe(array.includes(NaN), false, name + " includes NaN"); + } +} + +function testFloat32() { + const array = new Float32Array([16777216, 0.5, -0, 2 ** 31, Infinity, -Infinity, 0.1, 3.4028234663852886e38]); + const name = "Float32Array"; + + expectFound(array, 16777216, 0, name + " int32 2^24"); + expectFound(array, asDouble(16777216), 0, name + " double 2^24"); + // 2^24 + 1 is the first int32 a float cannot hold. It used to be rounded to 2^24 and "found". + expectMissing(array, 16777217, name + " int32 2^24 + 1"); + expectMissing(array, asDouble(16777217), name + " double 2^24 + 1"); + expectMissing(array, 16777215 + 2, name + " computed int32 2^24 + 1"); + // INT32_MAX rounds to 2^31 as a float. + expectMissing(array, 2147483647, name + " int32 INT32_MAX"); + expectMissing(array, asDouble(2147483647), name + " double INT32_MAX"); + expectFound(array, asDouble(2 ** 31), 3, name + " double 2^31"); + + expectFound(array, 0.5, 1, name + " 0.5"); + expectFound(array, 0, 2, name + " int32 0 matches -0"); + expectFound(array, -0, 2, name + " -0"); + expectFound(array, asDouble(0), 2, name + " double 0"); + expectFound(array, Infinity, 4, name + " Infinity"); + expectFound(array, -Infinity, 5, name + " -Infinity"); + // The element is Math.fround(0.1), which is not the double 0.1. + expectMissing(array, 0.1, name + " 0.1"); + expectFound(array, Math.fround(0.1), 6, name + " Math.fround(0.1)"); + const floatMax = 3.4028234663852886e38; + shouldBe(Math.fround(floatMax), floatMax, "FLT_MAX literal"); + expectFound(array, floatMax, 7, name + " FLT_MAX"); + // Beyond FLT_MAX: must not be converted to float at all (that conversion is undefined behavior in C++). + expectMissing(array, nextDoubleUp(floatMax), name + " the double after FLT_MAX"); + expectMissing(array, 1e300, name + " 1e300"); + expectMissing(array, -1e300, name + " -1e300"); + expectMissing(array, Number.MAX_VALUE, name + " Number.MAX_VALUE"); + + const withNaN = new Float32Array([1, NaN, 3]); + shouldBe(withNaN.indexOf(NaN), -1, name + " indexOf NaN"); + shouldBe(withNaN.lastIndexOf(NaN), -1, name + " lastIndexOf NaN"); + shouldBe(withNaN.includes(NaN), true, name + " includes NaN"); +} + +function testFloat16() { + const array = new Float16Array([2048, 0.5, -0, 65504, Infinity, -Infinity, 0.1, -65504]); + const name = "Float16Array"; + + expectFound(array, 2048, 0, name + " int32 2048"); + expectFound(array, asDouble(2048), 0, name + " double 2048"); + // 2^11 + 1 is the first int32 a Float16 cannot hold. It used to be rounded to 2048 and "found". + expectMissing(array, 2049, name + " int32 2049"); + expectMissing(array, asDouble(2049), name + " double 2049"); + expectMissing(array, 2047 + 2, name + " computed int32 2049"); + expectFound(array, 65504, 3, name + " int32 65504 (max finite)"); + expectFound(array, -65504, 7, name + " int32 -65504"); + // These int32 needles used to become +/-Infinity and match the Infinity elements. + expectMissing(array, 65536, name + " int32 65536"); + expectMissing(array, -65536, name + " int32 -65536"); + expectMissing(array, 65520, name + " int32 65520"); + expectMissing(array, 2147483647, name + " int32 INT32_MAX"); + expectMissing(array, -2147483648, name + " int32 INT32_MIN"); + expectMissing(array, asDouble(65536), name + " double 65536"); + expectMissing(array, asDouble(65520), name + " double 65520"); + expectMissing(array, 1e300, name + " 1e300"); + + expectFound(array, 0.5, 1, name + " 0.5"); + expectFound(array, 0, 2, name + " int32 0 matches -0"); + expectFound(array, -0, 2, name + " -0"); + expectFound(array, Infinity, 4, name + " Infinity"); + expectFound(array, -Infinity, 5, name + " -Infinity"); + expectMissing(array, 0.1, name + " 0.1"); + expectFound(array, array[6], 6, name + " f16round(0.1)"); + + const withNaN = new Float16Array([1, NaN, 3]); + shouldBe(withNaN.indexOf(NaN), -1, name + " indexOf NaN"); + shouldBe(withNaN.includes(NaN), true, name + " includes NaN"); +} + +function testFloat64() { + const array = new Float64Array([16777217, 2147483647, -0, 2 ** 53, 0.1]); + const name = "Float64Array"; + expectFound(array, 16777217, 0, name + " int32 16777217"); + expectFound(array, 2147483647, 1, name + " int32 INT32_MAX"); + expectFound(array, asDouble(2147483647), 1, name + " double INT32_MAX"); + expectFound(array, 0, 2, name + " 0 matches -0"); + expectFound(array, 2 ** 53, 3, name + " 2^53"); + expectFound(array, 0.1, 4, name + " 0.1"); + expectMissing(array, 2 ** 53 + 2, name + " 2^53 + 2"); + expectMissing(array, Infinity, name + " Infinity"); +} + +function testBigInt64() { + const array = new BigInt64Array([1n, 0n, -1n, -(2n ** 63n), 2n ** 63n - 1n]); + const name = "BigInt64Array"; + + expectFound(array, 1n, 0, name + " 1n"); + expectFound(array, 0n, 1, name + " 0n"); + expectFound(array, -1n, 2, name + " -1n"); + expectFound(array, -(2n ** 63n), 3, name + " INT64_MIN"); + expectFound(array, 2n ** 63n - 1n, 4, name + " INT64_MAX"); + + // Each of these used to be reduced modulo 2^64 into one of the elements above. + expectMissing(array, 2n ** 64n + 1n, name + " 2^64 + 1"); + expectMissing(array, 2n ** 64n, name + " 2^64"); + expectMissing(array, -(2n ** 64n), name + " -2^64"); + expectMissing(array, 2n ** 64n - 1n, name + " 2^64 - 1"); + expectMissing(array, 2n ** 63n, name + " 2^63"); + expectMissing(array, -(2n ** 63n) - 1n, name + " INT64_MIN - 1"); + expectMissing(array, 2n ** 128n + 1n, name + " 2^128 + 1"); + expectMissing(array, -(2n ** 128n) - 1n, name + " -2^128 - 1"); + expectMissing(array, (1n << 200n) - 1n, name + " 2^200 - 1"); + + expectMissing(array, 1, name + " Number 1"); + expectMissing(array, -1, name + " Number -1"); + expectMissing(array, "1", name + " string '1'"); +} + +function testBigUint64() { + const array = new BigUint64Array([1n, 0n, 2n ** 64n - 1n, 2n ** 63n]); + const name = "BigUint64Array"; + + expectFound(array, 1n, 0, name + " 1n"); + expectFound(array, 0n, 1, name + " 0n"); + expectFound(array, 2n ** 64n - 1n, 2, name + " UINT64_MAX"); + expectFound(array, 2n ** 63n, 3, name + " 2^63"); + + // Each of these used to be reduced modulo 2^64 into one of the elements above. + expectMissing(array, -1n, name + " -1n"); + expectMissing(array, -(2n ** 63n), name + " -2^63"); + expectMissing(array, 2n ** 64n, name + " 2^64"); + expectMissing(array, 2n ** 64n + 1n, name + " 2^64 + 1"); + expectMissing(array, -(2n ** 64n), name + " -2^64"); + expectMissing(array, -(2n ** 64n) + 1n, name + " -2^64 + 1"); + expectMissing(array, 2n ** 128n, name + " 2^128"); + expectMissing(array, (1n << 200n) + 1n, name + " 2^200 + 1"); + + expectMissing(array, 1, name + " Number 1"); + expectMissing(array, 0, name + " Number 0"); +} + +// Also read needles out of a double-shaped JS array, the way the original report did. +function testDoubleArraySource() { + const array = new Uint8ClampedArray([7, 0, 255]); + const needles = [1e10, -1e10, 2 ** 31, -(2 ** 31), Infinity, -Infinity, 2 ** 53, 2147483647, 256, -1, 300, NaN, 0.5]; + for (let i = 0; i < needles.length; ++i) { + shouldBe(array.indexOf(needles[i]), -1, "double array source indexOf " + needles[i]); + shouldBe(array.lastIndexOf(needles[i]), -1, "double array source lastIndexOf " + needles[i]); + shouldBe(array.includes(needles[i]), false, "double array source includes " + needles[i]); + } +} + +for (let i = 0; i < 200; ++i) { + testUint8Clamped(); + testIntegral(); + testFloat32(); + testFloat16(); + testFloat64(); + testBigInt64(); + testBigUint64(); + testDoubleArraySource(); +} diff --git a/Source/JavaScriptCore/runtime/JSBigInt.h b/Source/JavaScriptCore/runtime/JSBigInt.h index 5be165fb18c96..2d070878730fc 100644 --- a/Source/JavaScriptCore/runtime/JSBigInt.h +++ b/Source/JavaScriptCore/runtime/JSBigInt.h @@ -476,6 +476,9 @@ class JSBigInt final : public JSCell { } inline static std::optional tryExtractDouble(JSValue); // Defined in JSBigIntInlines.h + // Unlike toBigInt64 / toBigUInt64, which wrap modulo 2^64, these fail when the BigInt is out of range. + inline static std::optional tryExtractInt64(JSValue); // Defined in JSBigIntInlines.h + inline static std::optional tryExtractUInt64(JSValue); // Defined in JSBigIntInlines.h inline bool isZero() const { @@ -511,6 +514,8 @@ class JSBigInt final : public JSCell { JS_EXPORT_PRIVATE unsigned NODELETE hashSlow(); + inline static std::optional absoluteAsUInt64(JSBigInt*); // Defined in JSBigIntInlines.h + static JSBigInt* tryCreateFromImpl(JSGlobalObject*, uint64_t value, bool sign); static constexpr int maxInt = 0x7FFFFFFF; diff --git a/Source/JavaScriptCore/runtime/JSBigIntInlines.h b/Source/JavaScriptCore/runtime/JSBigIntInlines.h index 38862358fde91..decafe3ea2c35 100644 --- a/Source/JavaScriptCore/runtime/JSBigIntInlines.h +++ b/Source/JavaScriptCore/runtime/JSBigIntInlines.h @@ -60,6 +60,27 @@ inline int64_t JSBigInt::toBigInt64(JSValue bigInt) return static_cast(toBigUInt64Heap(bigInt.asHeapBigInt())); } +// |bigInt| when it fits in 64 bits. +ALWAYS_INLINE std::optional JSBigInt::absoluteAsUInt64(JSBigInt* bigInt) +{ + unsigned length = bigInt->length(); + if (!length) + return 0; + if constexpr (sizeof(Digit) == 8) { + if (length != 1) + return std::nullopt; + return bigInt->digit(0); + } else { + ASSERT(sizeof(Digit) == 4); + if (length > 2) + return std::nullopt; + uint64_t absolute = bigInt->digit(0); + if (length == 2) + absolute |= static_cast(bigInt->digit(1)) << 32; + return absolute; + } +} + ALWAYS_INLINE std::optional JSBigInt::tryExtractDouble(JSValue value) { if (value.isNumber()) @@ -75,27 +96,51 @@ ALWAYS_INLINE std::optional JSBigInt::tryExtractDouble(JSValue value) ASSERT(value.isHeapBigInt()); JSBigInt* bigInt = value.asHeapBigInt(); - if (!bigInt->length()) - return 0; + std::optional absolute = absoluteAsUInt64(bigInt); + if (!absolute || absolute.value() > maxSafeIntegerAsUInt64()) + return std::nullopt; + return (bigInt->sign()) ? -static_cast(absolute.value()) : static_cast(absolute.value()); +} - uint64_t integer = 0; - if constexpr (sizeof(Digit) == 8) { - if (bigInt->length() != 1) - return std::nullopt; - integer = bigInt->digit(0); - } else { - ASSERT(sizeof(Digit) == 4); - if (bigInt->length() > 2) +ALWAYS_INLINE std::optional JSBigInt::tryExtractInt64(JSValue value) +{ + ASSERT(value.isBigInt()); +#if USE(BIGINT32) + if (value.isBigInt32()) + return value.bigInt32AsInt32(); +#endif + + JSBigInt* bigInt = value.asHeapBigInt(); + std::optional absolute = absoluteAsUInt64(bigInt); + if (!absolute) + return std::nullopt; + constexpr uint64_t int64MinAbsolute = static_cast(1) << 63; + if (bigInt->sign()) { + if (absolute.value() > int64MinAbsolute) return std::nullopt; - integer = bigInt->digit(0); - if (bigInt->length() == 2) - integer |= (static_cast(bigInt->digit(1)) << 32); + return static_cast(~(absolute.value() - 1)); // Two's complement by hand, as in toBigUInt64Heap. } + if (absolute.value() >= int64MinAbsolute) + return std::nullopt; + return static_cast(absolute.value()); +} - if (integer <= maxSafeIntegerAsUInt64()) - return (bigInt->sign()) ? -static_cast(integer) : static_cast(integer); +ALWAYS_INLINE std::optional JSBigInt::tryExtractUInt64(JSValue value) +{ + ASSERT(value.isBigInt()); +#if USE(BIGINT32) + if (value.isBigInt32()) { + int32_t int32 = value.bigInt32AsInt32(); + if (int32 < 0) + return std::nullopt; + return static_cast(int32); + } +#endif - return std::nullopt; + JSBigInt* bigInt = value.asHeapBigInt(); + if (bigInt->sign()) + return std::nullopt; + return absoluteAsUInt64(bigInt); } ALWAYS_INLINE JSValue JSBigInt::makeHeapBigIntOrBigInt32(JSGlobalObject* globalObject, double value) diff --git a/Source/JavaScriptCore/runtime/ToNativeFromValue.h b/Source/JavaScriptCore/runtime/ToNativeFromValue.h index 3c4c5d42727e6..58c2ea20c0c05 100644 --- a/Source/JavaScriptCore/runtime/ToNativeFromValue.h +++ b/Source/JavaScriptCore/runtime/ToNativeFromValue.h @@ -57,6 +57,8 @@ typename Adaptor::Type toNativeFromValue(JSGlobalObject* globalObject, JSValue v } } +// The element value that is strictly equal (and SameValueZero) to |value|, or nullopt when the +// element type cannot represent |value| exactly. Never wraps, clamps or rounds. template std::optional toNativeFromValueWithoutCoercion(JSValue value) { @@ -64,9 +66,9 @@ std::optional toNativeFromValueWithoutCoercion(JSValue v if (!value.isBigInt()) return std::nullopt; if constexpr (std::is_same_v) - return JSBigInt::toBigInt64(value); + return JSBigInt::tryExtractInt64(value); else - return JSBigInt::toBigUInt64(value); + return JSBigInt::tryExtractUInt64(value); } else { if (!value.isNumber()) return std::nullopt; diff --git a/Source/JavaScriptCore/runtime/TypedArrayAdaptors.h b/Source/JavaScriptCore/runtime/TypedArrayAdaptors.h index 983900a0b518b..5595e45c03e19 100644 --- a/Source/JavaScriptCore/runtime/TypedArrayAdaptors.h +++ b/Source/JavaScriptCore/runtime/TypedArrayAdaptors.h @@ -168,7 +168,8 @@ struct FloatTypedArrayAdaptor { static std::optional toNativeFromInt32WithoutCoercion(int32_t value) { - return static_cast(value); + // A float cannot hold every int32 (16777217 becomes 16777216.0f), so take the checked path. + return toNativeFromDoubleWithoutCoercion(value); } static std::optional toNativeFromDoubleWithoutCoercion(double value) @@ -176,12 +177,12 @@ struct FloatTypedArrayAdaptor { if (std::isnan(value) || std::isinf(value)) return static_cast(value); - Type valueResult = static_cast(value); - - if (static_cast(valueResult) != value) + // Range first: converting a finite double beyond the range of Type is undefined behavior. + if (value < minValue || value > maxValue) return std::nullopt; - if (value < minValue || value > maxValue) + Type valueResult = static_cast(value); + if (static_cast(valueResult) != value) return std::nullopt; return valueResult; @@ -336,7 +337,9 @@ struct Uint8ClampedAdaptor { static std::optional toNativeFromDoubleWithoutCoercion(double value) { - uint8_t integer = static_cast(value); + // As in IntegralTypedArrayAdaptor: static_cast(double) is undefined outside (-1, 256), + // which licenses the optimizer to fold the round-trip check below into trunc(value) == value. + uint8_t integer = static_cast(truncateDoubleToInt64(value)); if (static_cast(integer) != value) return std::nullopt; @@ -400,7 +403,8 @@ struct Float16Adaptor { static std::optional toNativeFromInt32WithoutCoercion(int32_t value) { - return static_cast(value); + // A Float16 cannot hold every int32 (2049 becomes 2048, 65536 becomes Infinity), so take the checked path. + return toNativeFromDoubleWithoutCoercion(value); } static std::optional toNativeFromDoubleWithoutCoercion(double value) @@ -408,11 +412,12 @@ struct Float16Adaptor { if (std::isnan(value) || std::isinf(value)) return value; - Type valueResult { value }; - if (static_cast(valueResult) != value) + // Range first: converting a finite double beyond the range of Float16 is undefined behavior. + if (value < static_cast(minValue) || value > static_cast(maxValue)) return std::nullopt; - if (value < static_cast(minValue) || value > static_cast(maxValue)) + Type valueResult { value }; + if (static_cast(valueResult) != value) return std::nullopt; return valueResult; From 1f8219282cb001926b89add9c69f5bb65e3874a6 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 9 Sep 2026 00:22:13 +0000 Subject: [PATCH 2/2] Stress test: drive the outer loop from testLoopCount JSTests/README.md asks new stress tests to iterate by testLoopCount so the harness can scale them per configuration. Each pass makes several hundred builtin calls, so use testLoopCount / 50 (200 passes by default, 31ms in Release; 8ms with --useJIT=0). --- JSTests/stress/typed-array-index-of-unrepresentable-needle.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/JSTests/stress/typed-array-index-of-unrepresentable-needle.js b/JSTests/stress/typed-array-index-of-unrepresentable-needle.js index ae2a3e9818105..672356ded7ab5 100644 --- a/JSTests/stress/typed-array-index-of-unrepresentable-needle.js +++ b/JSTests/stress/typed-array-index-of-unrepresentable-needle.js @@ -239,7 +239,8 @@ function testDoubleArraySource() { } } -for (let i = 0; i < 200; ++i) { +// Each pass makes several hundred calls into the C++ builtins, so scale the iteration count down. +for (let i = 0; i < Math.max(1, testLoopCount / 50); ++i) { testUint8Clamped(); testIntegral(); testFloat32();