From 9e50f25ac48177658cc8ad518a7972be1cf83d25 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Sat, 16 Jul 2022 11:39:11 -0400 Subject: [PATCH] LibJS: Prevent i64 overflow when computing large NumberFormat exponents The largest exponents we compute are on the order of 10^21 (governed by the maximumSignificantDigits option, which has a max value of 21). That is too large to fit into the i64 we were using when multiplying this exponent by the value to be formatted. Instead, split up the logic to multiply that value by this exponent based on the value's underlying type: Number: Do not cast the result of pow() to an i64, and perform the follow-up multiplication with doubles. BigInt: Do not use pow(). Instead, compute the exponent as a BigInt from the start, then perform the follow-up multiplication with that BigInt. --- .../LibJS/Runtime/Intl/NumberFormat.cpp | 50 +++++++++++++++---- .../NumberFormat.prototype.format.js | 28 +++++++++++ 2 files changed, 68 insertions(+), 10 deletions(-) diff --git a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp index 9ebdd27d5d..87f2dc2763 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp @@ -375,7 +375,7 @@ static ALWAYS_INLINE int log10floor(Value number) return as_string.length() - 1; } -static Value multiply(GlobalObject& global_object, Value lhs, i64 rhs) +static Value multiply(GlobalObject& global_object, Value lhs, i8 rhs) { if (lhs.is_number()) return Value(lhs.as_double() * rhs); @@ -384,7 +384,7 @@ static Value multiply(GlobalObject& global_object, Value lhs, i64 rhs) return js_bigint(global_object.vm(), lhs.as_bigint().big_integer().multiplied_by(rhs_bigint)); } -static Value divide(GlobalObject& global_object, Value lhs, i64 rhs) +static Value divide(GlobalObject& global_object, Value lhs, i8 rhs) { if (lhs.is_number()) return Value(lhs.as_double() / rhs); @@ -393,18 +393,48 @@ static Value divide(GlobalObject& global_object, Value lhs, i64 rhs) return js_bigint(global_object.vm(), lhs.as_bigint().big_integer().divided_by(rhs_bigint).quotient); } -static ALWAYS_INLINE Value multiply_by_power(GlobalObject& global_object, Value number, i64 exponent) +static Crypto::SignedBigInteger bigint_power(i8 base, i8 exponent) { - if (exponent < 0) - return divide(global_object, number, pow(10, -exponent)); - return multiply(global_object, number, pow(10, exponent)); + VERIFY(exponent >= 0); + + auto base_bigint = Crypto::SignedBigInteger::create_from(base); + auto result = Crypto::SignedBigInteger::create_from(1); + + for (i8 i = 0; i < exponent; ++i) + result = result.multiplied_by(base_bigint); + + return result; } -static ALWAYS_INLINE Value divide_by_power(GlobalObject& global_object, Value number, i64 exponent) +static ALWAYS_INLINE Value multiply_by_power(GlobalObject& global_object, Value number, i8 exponent) { - if (exponent < 0) - return multiply(global_object, number, pow(10, -exponent)); - return divide(global_object, number, pow(10, exponent)); + if (number.is_number()) + return Value(number.as_double() * pow(10, exponent)); + + if (exponent < 0) { + auto exponent_bigint = bigint_power(10, -exponent); + return js_bigint(global_object.vm(), number.as_bigint().big_integer().divided_by(exponent_bigint).quotient); + } + + auto exponent_bigint = bigint_power(10, exponent); + return js_bigint(global_object.vm(), number.as_bigint().big_integer().multiplied_by(exponent_bigint)); +} + +static ALWAYS_INLINE Value divide_by_power(GlobalObject& global_object, Value number, i8 exponent) +{ + if (number.is_number()) { + if (exponent < 0) + return Value(number.as_double() * pow(10, -exponent)); + return Value(number.as_double() / pow(10, exponent)); + } + + if (exponent < 0) { + auto exponent_bigint = bigint_power(10, -exponent); + return js_bigint(global_object.vm(), number.as_bigint().big_integer().multiplied_by(exponent_bigint)); + } + + auto exponent_bigint = bigint_power(10, exponent); + return js_bigint(global_object.vm(), number.as_bigint().big_integer().divided_by(exponent_bigint).quotient); } static ALWAYS_INLINE Value rounded(Value number) diff --git a/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.format.js b/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.format.js index 897ac1a422..fa0fd040d5 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.format.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.format.js @@ -78,6 +78,34 @@ describe("style=decimal", () => { expect(en.format(12.3456)).toBe("12.3456"); expect(en.format(12.34567)).toBe("12.3457"); expect(en.format(12.34561)).toBe("12.3456"); + expect(en.format(0.00000000000000000000000000000123)).toBe( + "0.000000000000000000000000000001230" + ); + expect(en.format(-0.00000000000000000000000000000123)).toBe( + "-0.000000000000000000000000000001230" + ); + expect(en.format(12344501000000000000000000000000000)).toBe( + "12,344,500,000,000,000,000,000,000,000,000,000" + ); + expect(en.format(-12344501000000000000000000000000000)).toBe( + "-12,344,500,000,000,000,000,000,000,000,000,000" + ); + expect(en.format(12344501000000000000000000000000000n)).toBe( + "12,344,500,000,000,000,000,000,000,000,000,000" + ); + expect(en.format(-12344501000000000000000000000000000n)).toBe( + "-12,344,500,000,000,000,000,000,000,000,000,000" + ); + + const enLargeMaxSignificantDigits = new Intl.NumberFormat("en", { + minimumSignificantDigits: 4, + maximumSignificantDigits: 21, + }); + expect(enLargeMaxSignificantDigits.format(1)).toBe("1.000"); + expect(enLargeMaxSignificantDigits.format(1n)).toBe("1.000"); + expect(enLargeMaxSignificantDigits.format(123456789123456789123456789123456789n)).toBe( + "123,456,789,123,456,789,123,000,000,000,000,000" + ); const ar = new Intl.NumberFormat("ar", { minimumSignificantDigits: 4,