Skip to content

Commit

Permalink
Normative: Limit day length calculations to safe integers
Browse files Browse the repository at this point in the history
Carefully crafted custom time zones can cause NormalizedTimeDurationToDays
to calculate a day length that is arbitrarily long. In order for
implementations not to have to use a normalized time duration or bigint to
represent the day length in nanoseconds, limit it to less than 2⁵³ ns.
This way, it can be stored in a 64-bit float.

This allows time zones to specify day lengths of up to ~104.25 real
24-hour days, which should be more than enough.
  • Loading branch information
ptomato committed Nov 16, 2023
1 parent 070353d commit 6ec8654
Show file tree
Hide file tree
Showing 2 changed files with 16 additions and 9 deletions.
21 changes: 13 additions & 8 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,10 @@ const $TypeError = GetIntrinsic('%TypeError%');
const $isEnumerable = callBound('Object.prototype.propertyIsEnumerable');

const DAY_SECONDS = 86400;
const DAY_NANOS = bigInt(DAY_SECONDS).multiply(1e9);
const DAY_NANOS = DAY_SECONDS * 1e9;
// Instant range is 100 million days (inclusive) before or after epoch.
const NS_MIN = DAY_NANOS.multiply(-1e8);
const NS_MAX = DAY_NANOS.multiply(1e8);
const NS_MIN = bigInt(DAY_NANOS).multiply(-1e8);
const NS_MAX = bigInt(DAY_NANOS).multiply(1e8);
// PlainDateTime range is 24 hours wider (exclusive) than the Instant range on
// both ends, to allow for valid Instant=>PlainDateTime conversion for all
// built-in time zones (whose offsets must have a magnitude less than 24 hours).
Expand Down Expand Up @@ -2874,13 +2874,13 @@ export function GetNamedTimeZoneNextTransition(id, epochNanoseconds) {
// transitions after that.
const now = SystemUTCEpochNanoSeconds();
const base = epochNanoseconds.greater(now) ? epochNanoseconds : now;
const uppercap = base.plus(DAY_NANOS.multiply(366 * 3));
const uppercap = base.plus(bigInt(DAY_NANOS).multiply(366 * 3));
let leftNanos = epochNanoseconds;
let leftOffsetNs = GetNamedTimeZoneOffsetNanoseconds(id, leftNanos);
let rightNanos = leftNanos;
let rightOffsetNs = leftOffsetNs;
while (leftOffsetNs === rightOffsetNs && bigInt(leftNanos).compare(uppercap) === -1) {
rightNanos = bigInt(leftNanos).plus(DAY_NANOS.multiply(2 * 7));
rightNanos = bigInt(leftNanos).plus(bigInt(DAY_NANOS).multiply(2 * 7));
if (rightNanos.greater(NS_MAX)) return null;
rightOffsetNs = GetNamedTimeZoneOffsetNanoseconds(id, rightNanos);
if (leftOffsetNs === rightOffsetNs) {
Expand All @@ -2903,7 +2903,7 @@ export function GetNamedTimeZonePreviousTransition(id, epochNanoseconds) {
// are no transitions between the present day and 3 years from now, assume
// there are none after.
const now = SystemUTCEpochNanoSeconds();
const lookahead = now.plus(DAY_NANOS.multiply(366 * 3));
const lookahead = now.plus(bigInt(DAY_NANOS).multiply(366 * 3));
if (epochNanoseconds.gt(lookahead)) {
const prevBeforeLookahead = GetNamedTimeZonePreviousTransition(id, lookahead);
if (prevBeforeLookahead === null || prevBeforeLookahead.lt(now)) {
Expand Down Expand Up @@ -2931,7 +2931,7 @@ export function GetNamedTimeZonePreviousTransition(id, epochNanoseconds) {
let leftNanos = rightNanos;
let leftOffsetNs = rightOffsetNs;
while (rightOffsetNs === leftOffsetNs && bigInt(rightNanos).compare(BEFORE_FIRST_DST) === 1) {
leftNanos = bigInt(rightNanos).minus(DAY_NANOS.multiply(2 * 7));
leftNanos = bigInt(rightNanos).minus(bigInt(DAY_NANOS).multiply(2 * 7));
if (leftNanos.lesser(BEFORE_FIRST_DST)) return null;
leftOffsetNs = GetNamedTimeZoneOffsetNanoseconds(id, leftNanos);
if (rightOffsetNs === leftOffsetNs) {
Expand Down Expand Up @@ -3297,7 +3297,12 @@ export function NormalizedTimeDurationToDays(norm, zonedRelativeTo, timeZoneRec,
if (norm.abs().cmp(dayLengthNs.abs()) >= 0) {
throw new Error('assert not reached');
}
return { days, norm, dayLengthNs: dayLengthNs.abs().totalNs };
const daylen = dayLengthNs.abs().totalNs.toJSNumber();
if (!NumberIsSafeInteger(daylen)) {
const h = daylen / 3600e9;
throw new RangeError(`Time zone calculated a day length of ${h} h, longer than ~2502 h causes precision loss`);
}
return { days, norm, dayLengthNs: daylen };
}

export function BalanceTimeDuration(norm, largestUnit) {
Expand Down
4 changes: 3 additions & 1 deletion spec/zoneddatetime.html
Original file line number Diff line number Diff line change
Expand Up @@ -1469,10 +1469,12 @@ <h1>
1. Assert: _sign_ is -1.
1. If NormalizedTimeDurationSign(_norm_) = 1 and _sign_ = -1, throw a *RangeError* exception.
1. Assert: CompareNormalizedTimeDuration(NormalizedTimeDurationAbs(_norm_), NormalizedTimeDurationAbs(_dayLengthNs_)) = -1.
1. Let _dayLength_ be abs(ℝ(_dayLengthNs_.[[TotalNanoseconds]])).
1. If _dayLength_ &ge; 2<sup>53</sup>, throw a *RangeError* exception.
1. Return the Record {
[[Days]]: _days_,
[[Remainder]]: _norm_,
[[DayLength]]: ℝ(NormalizedTimeDurationAbs(_dayLengthNs_).[[TotalNanoseconds]])
[[DayLength]]: _dayLength_
}.
</emu-alg>
</emu-clause>
Expand Down

0 comments on commit 6ec8654

Please sign in to comment.