From 4fc12918fdea3a22beb06c7ba3d1fcce092690d8 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Mon, 1 Jun 2026 16:56:28 -0700 Subject: [PATCH 1/9] Fix UB in parseIntResult double-to-int32 conversion static_cast of a double outside the int32 range is undefined behavior. Newer compilers assume the cast is in-range and elide the following round-trip check, so parseInt results >= 2^31 (e.g. parseInt("80000000", 16)) are boxed as a negative int32 instead of the correct double. Use truncateDoubleToInt32() for a defined conversion. --- Source/JavaScriptCore/dfg/DFGOperations.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/JavaScriptCore/dfg/DFGOperations.cpp b/Source/JavaScriptCore/dfg/DFGOperations.cpp index 3e24a3851547..9abfaf5dd153 100644 --- a/Source/JavaScriptCore/dfg/DFGOperations.cpp +++ b/Source/JavaScriptCore/dfg/DFGOperations.cpp @@ -177,7 +177,7 @@ static ALWAYS_INLINE void putWithThis(JSGlobalObject* globalObject, EncodedJSVal static ALWAYS_INLINE EncodedJSValue parseIntResult(double input) { - int asInt = static_cast(input); + int32_t asInt = truncateDoubleToInt32(input); if (static_cast(asInt) == input && (asInt || !std::signbit(input))) [[likely]] return JSValue::encode(jsNumber(asInt)); return JSValue::encode(jsNumber(input)); From e4c9ab175351cd54280b85ded99330fcada83fbf Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Mon, 1 Jun 2026 17:21:35 -0700 Subject: [PATCH 2/9] Fix same UB in double-to-int round-trip checks LiteralParser JSONP index, JSValue::getUInt32, and String.fromCodePoint each cast a double to a narrow int and validate with a round-trip check the compiler may delete (the cast is UB out of range). Make the cast defined / guard the range before narrowing. --- Source/JavaScriptCore/runtime/JSCJSValue.h | 2 ++ Source/JavaScriptCore/runtime/LiteralParser.cpp | 2 +- Source/JavaScriptCore/runtime/StringConstructor.cpp | 6 +++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Source/JavaScriptCore/runtime/JSCJSValue.h b/Source/JavaScriptCore/runtime/JSCJSValue.h index e47a2590b0ee..9e0bfde78889 100644 --- a/Source/JavaScriptCore/runtime/JSCJSValue.h +++ b/Source/JavaScriptCore/runtime/JSCJSValue.h @@ -1352,6 +1352,8 @@ ALWAYS_INLINE bool JSValue::getUInt32(uint32_t& v) const } if (isDouble()) { double d = asDouble(); + if (!(d >= 0 && d <= static_cast(std::numeric_limits::max()))) + return false; v = static_cast(d); return v == d; } diff --git a/Source/JavaScriptCore/runtime/LiteralParser.cpp b/Source/JavaScriptCore/runtime/LiteralParser.cpp index 15a45d8f5d13..b69114a05e7a 100644 --- a/Source/JavaScriptCore/runtime/LiteralParser.cpp +++ b/Source/JavaScriptCore/runtime/LiteralParser.cpp @@ -97,7 +97,7 @@ bool LiteralParser::tryJSONPParse(Vector& resu if (m_lexer.next() != TokNumber) return false; double doubleIndex = m_lexer.currentToken()->numberToken; - int index = (int)doubleIndex; + int index = truncateDoubleToInt32(doubleIndex); if (index != doubleIndex || index < 0) return false; entry.m_pathIndex = index; diff --git a/Source/JavaScriptCore/runtime/StringConstructor.cpp b/Source/JavaScriptCore/runtime/StringConstructor.cpp index 740df0e74a5a..d4bd377f8424 100644 --- a/Source/JavaScriptCore/runtime/StringConstructor.cpp +++ b/Source/JavaScriptCore/runtime/StringConstructor.cpp @@ -129,11 +129,11 @@ JSC_DEFINE_HOST_FUNCTION(stringFromCodePoint, (JSGlobalObject* globalObject, Cal double codePointAsDouble = callFrame->uncheckedArgument(i).toNumber(globalObject); RETURN_IF_EXCEPTION(scope, encodedJSValue()); - uint32_t codePoint = static_cast(codePointAsDouble); - - if (codePoint != codePointAsDouble || codePoint > UCHAR_MAX_VALUE) + if (!(codePointAsDouble >= 0 && codePointAsDouble <= UCHAR_MAX_VALUE) || codePointAsDouble != std::trunc(codePointAsDouble)) return throwVMError(globalObject, scope, createRangeError(globalObject, "Arguments contain a value that is out of range of code points"_s)); + uint32_t codePoint = static_cast(codePointAsDouble); + if (U_IS_BMP(codePoint)) builder.append(static_cast(codePoint)); else { From 2d37191619c6e999634b11b436811b9cc4d2ec53 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Mon, 1 Jun 2026 17:21:38 -0700 Subject: [PATCH 3/9] Fix UB in Number toExponential/toPrecision digit count The ToInteger argument was cast to int before the range check, so an out-of-range or infinite value hit UB. Range-check the double first. --- Source/JavaScriptCore/runtime/NumberPrototype.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Source/JavaScriptCore/runtime/NumberPrototype.cpp b/Source/JavaScriptCore/runtime/NumberPrototype.cpp index 06fbf5303d22..5261115ed4b2 100644 --- a/Source/JavaScriptCore/runtime/NumberPrototype.cpp +++ b/Source/JavaScriptCore/runtime/NumberPrototype.cpp @@ -401,15 +401,16 @@ JSC_DEFINE_HOST_FUNCTION(numberProtoFuncToExponential, (JSGlobalObject* globalOb JSValue arg = callFrame->argument(0); // Perform ToInteger on the argument before remaining steps. - int decimalPlaces = static_cast(arg.toIntegerOrInfinity(globalObject)); + double decimalPlacesDouble = arg.toIntegerOrInfinity(globalObject); RETURN_IF_EXCEPTION(scope, { }); // Handle NaN and Infinity. if (!std::isfinite(x)) return JSValue::encode(jsNontrivialString(vm, String::number(x))); - if (decimalPlaces < 0 || decimalPlaces > 100) + if (decimalPlacesDouble < 0 || decimalPlacesDouble > 100) return throwVMRangeError(globalObject, scope, "toExponential() argument must be between 0 and 100"_s); + int decimalPlaces = static_cast(decimalPlacesDouble); // Round if the argument is not undefined, always format as exponential. NumberToStringBuffer buffer; @@ -477,15 +478,16 @@ JSC_DEFINE_HOST_FUNCTION(numberProtoFuncToPrecision, (JSGlobalObject* globalObje return JSValue::encode(jsString(vm, String::number(x))); // Perform ToInteger on the argument before remaining steps. - int significantFigures = static_cast(arg.toIntegerOrInfinity(globalObject)); + double significantFiguresDouble = arg.toIntegerOrInfinity(globalObject); RETURN_IF_EXCEPTION(scope, { }); // Handle NaN and Infinity. if (!std::isfinite(x)) return JSValue::encode(jsNontrivialString(vm, String::number(x))); - if (significantFigures < 1 || significantFigures > 100) + if (significantFiguresDouble < 1 || significantFiguresDouble > 100) return throwVMRangeError(globalObject, scope, "toPrecision() argument must be between 1 and 100"_s); + int significantFigures = static_cast(significantFiguresDouble); return JSValue::encode(jsString(vm, String::numberToStringFixedPrecision(x, significantFigures, TrailingZerosPolicy::Keep))); } From a86f225ff3dded18b18245bb9815282349639128 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Mon, 1 Jun 2026 17:26:43 -0700 Subject: [PATCH 4/9] Use clampTo for inspector fetch-count narrowing weakMapEntries/iteratorEntries cast a ToIntegerOrInfinity result to unsigned; +Infinity passes the >= 0 guard and makes the cast UB. clampTo defines it. --- Source/JavaScriptCore/inspector/JSInjectedScriptHost.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/JavaScriptCore/inspector/JSInjectedScriptHost.cpp b/Source/JavaScriptCore/inspector/JSInjectedScriptHost.cpp index a22e99811a07..98c9052155b6 100644 --- a/Source/JavaScriptCore/inspector/JSInjectedScriptHost.cpp +++ b/Source/JavaScriptCore/inspector/JSInjectedScriptHost.cpp @@ -613,7 +613,7 @@ JSValue JSInjectedScriptHost::weakMapEntries(JSGlobalObject* globalObject, CallF MarkedArgumentBuffer buffer; auto fetchCount = callFrame->argument(1).toIntegerOrInfinity(globalObject); - weakMap->takeSnapshot(buffer, fetchCount >= 0 ? static_cast(fetchCount) : 0); + weakMap->takeSnapshot(buffer, clampTo(fetchCount)); ASSERT(!buffer.hasOverflowed()); JSArray* array = constructEmptyArray(globalObject, nullptr); @@ -740,7 +740,7 @@ JSValue JSInjectedScriptHost::iteratorEntries(JSGlobalObject* globalObject, Call double fetchDouble = numberToFetchArg.toIntegerOrInfinity(globalObject); RETURN_IF_EXCEPTION(scope, { }); if (fetchDouble >= 0) - numberToFetch = static_cast(fetchDouble); + numberToFetch = clampTo(fetchDouble); JSArray* array = constructEmptyArray(globalObject, nullptr); RETURN_IF_EXCEPTION(scope, { }); From 31ce1df1e0cc10c1bd5e0f595625c3195e98c4b8 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Mon, 1 Jun 2026 17:30:29 -0700 Subject: [PATCH 5/9] getUInt32: drop the added branch, use a defined cast truncateDoubleToInt32 makes the conversion defined for all doubles, so the existing v == d round-trip check is enough on its own. --- Source/JavaScriptCore/runtime/JSCJSValue.h | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Source/JavaScriptCore/runtime/JSCJSValue.h b/Source/JavaScriptCore/runtime/JSCJSValue.h index 9e0bfde78889..ee3ae6d4e695 100644 --- a/Source/JavaScriptCore/runtime/JSCJSValue.h +++ b/Source/JavaScriptCore/runtime/JSCJSValue.h @@ -1352,9 +1352,7 @@ ALWAYS_INLINE bool JSValue::getUInt32(uint32_t& v) const } if (isDouble()) { double d = asDouble(); - if (!(d >= 0 && d <= static_cast(std::numeric_limits::max()))) - return false; - v = static_cast(d); + v = static_cast(truncateDoubleToInt32(d)); return v == d; } return false; From 1d664f45a9575654b43eb4ca36cf3dad60082c52 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Mon, 1 Jun 2026 17:46:53 -0700 Subject: [PATCH 6/9] getUInt32: use truncateDoubleToUint32, not Int32 truncateDoubleToInt32 returns an architecture sentinel for d >= 2^31, so the round-trip check would have rejected valid uint32 indices in [2^31, 2^32). truncateDoubleToUint32 returns d exactly for d in [0, 2^32) and is in scope via the existing MathExtras.h include. --- Source/JavaScriptCore/runtime/JSCJSValue.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/JavaScriptCore/runtime/JSCJSValue.h b/Source/JavaScriptCore/runtime/JSCJSValue.h index ee3ae6d4e695..34ad1c2c9949 100644 --- a/Source/JavaScriptCore/runtime/JSCJSValue.h +++ b/Source/JavaScriptCore/runtime/JSCJSValue.h @@ -1352,7 +1352,7 @@ ALWAYS_INLINE bool JSValue::getUInt32(uint32_t& v) const } if (isDouble()) { double d = asDouble(); - v = static_cast(truncateDoubleToInt32(d)); + v = truncateDoubleToUint32(d); return v == d; } return false; From 0156780da71b60c5c61ed483b59d110321bb5f52 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Mon, 1 Jun 2026 17:46:56 -0700 Subject: [PATCH 7/9] parseIntResult: collapse to jsNumber(double) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit JSValue(double) already calls tryConvertToStrictInt32 and boxes int32 iff exactly representable, else as double — this function was duplicating that. The one-liner is bit-identical and removes the UB by deletion. --- Source/JavaScriptCore/dfg/DFGOperations.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/Source/JavaScriptCore/dfg/DFGOperations.cpp b/Source/JavaScriptCore/dfg/DFGOperations.cpp index 9abfaf5dd153..2119b699a6ce 100644 --- a/Source/JavaScriptCore/dfg/DFGOperations.cpp +++ b/Source/JavaScriptCore/dfg/DFGOperations.cpp @@ -177,9 +177,6 @@ static ALWAYS_INLINE void putWithThis(JSGlobalObject* globalObject, EncodedJSVal static ALWAYS_INLINE EncodedJSValue parseIntResult(double input) { - int32_t asInt = truncateDoubleToInt32(input); - if (static_cast(asInt) == input && (asInt || !std::signbit(input))) [[likely]] - return JSValue::encode(jsNumber(asInt)); return JSValue::encode(jsNumber(input)); } From 04b373846acd16c62c0b28ef1fc7cbfad7c73e6e Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Mon, 1 Jun 2026 17:46:59 -0700 Subject: [PATCH 8/9] fromCodePoint: minimal one-token swap Restore the original cast-then-round-trip structure; only the cast is made defined via truncateDoubleToUint32 so the value is bit-identical for every input the original cast was defined on. --- Source/JavaScriptCore/runtime/StringConstructor.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/JavaScriptCore/runtime/StringConstructor.cpp b/Source/JavaScriptCore/runtime/StringConstructor.cpp index d4bd377f8424..1637552b775c 100644 --- a/Source/JavaScriptCore/runtime/StringConstructor.cpp +++ b/Source/JavaScriptCore/runtime/StringConstructor.cpp @@ -129,10 +129,10 @@ JSC_DEFINE_HOST_FUNCTION(stringFromCodePoint, (JSGlobalObject* globalObject, Cal double codePointAsDouble = callFrame->uncheckedArgument(i).toNumber(globalObject); RETURN_IF_EXCEPTION(scope, encodedJSValue()); - if (!(codePointAsDouble >= 0 && codePointAsDouble <= UCHAR_MAX_VALUE) || codePointAsDouble != std::trunc(codePointAsDouble)) - return throwVMError(globalObject, scope, createRangeError(globalObject, "Arguments contain a value that is out of range of code points"_s)); + uint32_t codePoint = truncateDoubleToUint32(codePointAsDouble); - uint32_t codePoint = static_cast(codePointAsDouble); + if (codePoint != codePointAsDouble || codePoint > UCHAR_MAX_VALUE) + return throwVMError(globalObject, scope, createRangeError(globalObject, "Arguments contain a value that is out of range of code points"_s)); if (U_IS_BMP(codePoint)) builder.append(static_cast(codePoint)); From d19a17e7fb48a5222a9b1f091f4b1470fb8ee09b Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Mon, 1 Jun 2026 20:18:55 -0700 Subject: [PATCH 9/9] Fix same UB in slow_path_switch_imm scrutinee narrowing --- Source/JavaScriptCore/llint/LLIntSlowPaths.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/JavaScriptCore/llint/LLIntSlowPaths.cpp b/Source/JavaScriptCore/llint/LLIntSlowPaths.cpp index e7dc0fca3fab..3a4db00921af 100644 --- a/Source/JavaScriptCore/llint/LLIntSlowPaths.cpp +++ b/Source/JavaScriptCore/llint/LLIntSlowPaths.cpp @@ -1885,7 +1885,7 @@ LLINT_SLOW_PATH_DECL(slow_path_switch_imm) auto bytecode = pc->as(); JSValue scrutinee = getOperand(callFrame, bytecode.m_scrutinee); double value = scrutinee.asNumber(); - int32_t intValue = static_cast(value); + int32_t intValue = truncateDoubleToInt32(value); auto& unlinkedTable = codeBlock->unlinkedSwitchJumpTable(bytecode.m_tableIndex); if (value == intValue) [[likely]] { if (!unlinkedTable.isList()) {