From 300f8d3dbb60e7c7f982037b1a73df49920ea752 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Tue, 13 Aug 2024 12:36:06 -0400 Subject: [PATCH] LibJS: Impose limits on a valid duration This is a normative change in the Temporal proposal. See: https://github.com/tc39/proposal-temporal/commit/1104cad https://github.com/tc39/proposal-temporal/commit/45462c4 Although our Temporal implementation is wildly out of date, this AO and the changes to it are relied on in Intl.DurationFormat. --- .../LibJS/Runtime/Temporal/Duration.cpp | 39 +++++++++++++++++++ .../Temporal/Duration/Duration.from.js | 22 +++++++++++ 2 files changed, 61 insertions(+) diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/Duration.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/Duration.cpp index 04539e2f882..27abb6a88f7 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/Duration.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/Duration.cpp @@ -6,6 +6,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include @@ -256,6 +257,44 @@ bool is_valid_duration(double years, double months, double weeks, double days, d return false; } + // 3. If abs(years) ≥ 2**32, return false. + if (AK::fabs(years) > NumericLimits::max()) + return false; + + // 4. If abs(months) ≥ 2**32, return false. + if (AK::fabs(months) > NumericLimits::max()) + return false; + + // 5. If abs(weeks) ≥ 2**32, return false. + if (AK::fabs(weeks) > NumericLimits::max()) + return false; + + // 6. Let normalizedSeconds be days × 86,400 + hours × 3600 + minutes × 60 + seconds + ℝ(𝔽(milliseconds)) × 10**-3 + ℝ(𝔽(microseconds)) × 10**-6 + ℝ(𝔽(nanoseconds)) × 10**-9. + // 7. NOTE: The above step cannot be implemented directly using floating-point arithmetic. Multiplying by 10**-3, 10**-6, and 10**-9 respectively may be imprecise when milliseconds, microseconds, or nanoseconds is an unsafe integer. This multiplication can be implemented in C++ with an implementation of std::remquo() with sufficient bits in the quotient. String manipulation will also give an exact result, since the multiplication is by a power of 10. + static Crypto::SignedBigInteger days_to_nanoseconds { 8.64e13 }; + static Crypto::SignedBigInteger hours_to_nanoseconds { 3.6e12 }; + static Crypto::SignedBigInteger minutes_to_nanoseconds { 6e10 }; + static Crypto::SignedBigInteger seconds_to_nanoseconds { 1e9 }; + static Crypto::SignedBigInteger milliseconds_to_nanoseconds { 1e6 }; + static Crypto::SignedBigInteger microseconds_to_nanoseconds { 1e3 }; + + auto normalized_nanoseconds = Crypto::SignedBigInteger { days }.multiplied_by(days_to_nanoseconds); + normalized_nanoseconds = normalized_nanoseconds.plus(Crypto::SignedBigInteger { hours }.multiplied_by(hours_to_nanoseconds)); + normalized_nanoseconds = normalized_nanoseconds.plus(Crypto::SignedBigInteger { minutes }.multiplied_by(minutes_to_nanoseconds)); + normalized_nanoseconds = normalized_nanoseconds.plus(Crypto::SignedBigInteger { seconds }.multiplied_by(seconds_to_nanoseconds)); + normalized_nanoseconds = normalized_nanoseconds.plus(Crypto::SignedBigInteger { milliseconds }.multiplied_by(milliseconds_to_nanoseconds)); + normalized_nanoseconds = normalized_nanoseconds.plus(Crypto::SignedBigInteger { microseconds }.multiplied_by(microseconds_to_nanoseconds)); + normalized_nanoseconds = normalized_nanoseconds.plus(Crypto::SignedBigInteger { nanoseconds }); + + // 8. If abs(normalizedSeconds) ≥ 2**53, return false. + static auto maximum_time = Crypto::SignedBigInteger { MAX_ARRAY_LIKE_INDEX }.plus(Crypto::SignedBigInteger { 1 }).multiplied_by(seconds_to_nanoseconds); + + if (normalized_nanoseconds.is_negative()) + normalized_nanoseconds.negate(); + + if (normalized_nanoseconds >= maximum_time) + return false; + // 3. Return true. return true; } diff --git a/Userland/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.from.js b/Userland/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.from.js index 941e2a549d7..3a314e877c8 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.from.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.from.js @@ -108,4 +108,26 @@ describe("errors", () => { ); } }); + + test("invalid duration string: exceed duration limits", () => { + const values = [ + "P4294967296Y", // abs(years) >= 2**32 + "P4294967296M", // abs(months) >= 2**32 + "P4294967296W", // abs(weeks) >= 2**32 + "P104249991375D", // days >= 2*53 seconds + "PT2501999792984H", // hours >= 2*53 seconds + "PT150119987579017M", // minutes >= 2*53 seconds + "PT9007199254740992S", // seconds >= 2*53 seconds + ]; + + for (const value of values) { + expect(() => { + Temporal.Duration.from(value); + }).toThrowWithMessage(RangeError, `Invalid duration`); + + expect(() => { + Temporal.Duration.from("-" + value); + }).toThrowWithMessage(RangeError, `Invalid duration`); + } + }); });