Skip to content

Commit

Permalink
Normative: Prevent arbitrary loops in NormalizedTimeDurationToDays
Browse files Browse the repository at this point in the history
It's possible to make at least the second loop continue arbitrarily long
until going out of range, using a contrived custom time zone.

This unrolls the loops and executes them no more than twice.

In order to weed out this situation earlier, when possible, also put a
limit on the maximum possible UTC offset shift:
- For backwards UTC offset shifts, if getPossibleInstantsFor returns more
  than one instant, the difference between the earliest and latest
  instants in the returned array may not be more than 24 hours.
- For forwards UTC offset shifts, if getPossibleInstantsFor returns zero
  instants, the difference between the offsets 24 hours before and after
  returned by getOffsetNanosecondsFor may not be more than 24 hours.
  • Loading branch information
ptomato committed Feb 2, 2024
1 parent 45b5e1f commit 891769a
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 34 deletions.
61 changes: 43 additions & 18 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* global __debug__ */

const ArrayIncludes = Array.prototype.includes;
const ArrayPrototypeMap = Array.prototype.map;
const ArrayPrototypePush = Array.prototype.push;
const ArrayPrototypeSort = Array.prototype.sort;
const ArrayPrototypeFind = Array.prototype.find;
Expand Down Expand Up @@ -2422,6 +2423,10 @@ export function DisambiguatePossibleInstants(possibleInstants, timeZoneRec, date
const offsetBefore = GetOffsetNanosecondsFor(timeZoneRec, dayBefore);
const offsetAfter = GetOffsetNanosecondsFor(timeZoneRec, dayAfter);
const nanoseconds = offsetAfter - offsetBefore;
if (MathAbs(nanoseconds) > DAY_NANOS) {
throw new RangeError('bad return from getOffsetNanosecondsFor: UTC offset shift longer than 24 hours');
}

switch (disambiguation) {
case 'earlier': {
const norm = TimeDuration.normalize(0, 0, 0, 0, 0, -nanoseconds);
Expand Down Expand Up @@ -2479,6 +2484,17 @@ export function GetPossibleInstantsFor(timeZoneRec, dateTime) {
}
Call(ArrayPrototypePush, result, [instant]);
}

const numResults = result.length;
if (numResults > 1) {
const mapped = Call(ArrayPrototypeMap, result, [(i) => GetSlot(i, EPOCHNANOSECONDS)]);
const min = bigInt.min(...mapped);
const max = bigInt.max(...mapped);
if (bigInt(max).subtract(min).abs().greater(DAY_NANOS)) {
throw new RangeError('bad return from getPossibleInstantsFor: UTC offset shift longer than 24 hours');
}
}

return result;
}

Expand Down Expand Up @@ -3260,20 +3276,34 @@ export function NormalizedTimeDurationToDays(norm, zonedRelativeTo, timeZoneRec,
// back inside the period where it belongs. Note that this case only can
// happen for positive durations because the only direction that
// `disambiguation: 'compatible'` can change clock time is forwards.
if (sign === 1) {
while (days > 0 && relativeResult.epochNs.greater(endNs)) {
days--;
relativeResult = AddDaysToZonedDateTime(start, dtStart, timeZoneRec, calendar, days);
// may do disambiguation
if (sign === 1 && days > 0 && relativeResult.epochNs.greater(endNs)) {
days--;
relativeResult = AddDaysToZonedDateTime(start, dtStart, timeZoneRec, calendar, days);
// may do disambiguation
if (days > 0 && relativeResult.epochNs.greater(endNs)) {
throw new RangeError('inconsistent result from custom time zone getInstantFor()');
}
}
norm = TimeDuration.fromEpochNsDiff(endNs, relativeResult.epochNs);

let isOverflow = false;
let dayLengthNs;
do {
// calculate length of the next day (day that contains the time remainder)
const oneDayFarther = AddDaysToZonedDateTime(
// calculate length of the next day (day that contains the time remainder)
let oneDayFarther = AddDaysToZonedDateTime(
relativeResult.instant,
relativeResult.dateTime,
timeZoneRec,
calendar,
sign
);
let dayLengthNs = TimeDuration.fromEpochNsDiff(oneDayFarther.epochNs, relativeResult.epochNs);
const oneDayLess = norm.subtract(dayLengthNs);
let isOverflow = oneDayLess.sign() * sign >= 0;
if (isOverflow) {
norm = oneDayLess;
relativeResult = oneDayFarther;
days += sign;

// ensure there was no more overflow
oneDayFarther = AddDaysToZonedDateTime(
relativeResult.instant,
relativeResult.dateTime,
timeZoneRec,
Expand All @@ -3282,14 +3312,9 @@ export function NormalizedTimeDurationToDays(norm, zonedRelativeTo, timeZoneRec,
);

dayLengthNs = TimeDuration.fromEpochNsDiff(oneDayFarther.epochNs, relativeResult.epochNs);
const oneDayLess = norm.subtract(dayLengthNs);
isOverflow = oneDayLess.sign() * sign >= 0;
if (isOverflow) {
norm = oneDayLess;
relativeResult = oneDayFarther;
days += sign;
}
} while (isOverflow);
isOverflow = norm.subtract(dayLengthNs).sign() * sign >= 0;
if (isOverflow) throw new RangeError('inconsistent result from custom time zone getPossibleInstantsFor()');
}
if (days !== 0 && MathSign(days) != sign) {
throw new RangeError('Time zone or calendar converted nanoseconds into a number of days with the opposite sign');
}
Expand Down
9 changes: 9 additions & 0 deletions spec/timezone.html
Original file line number Diff line number Diff line change
Expand Up @@ -937,6 +937,7 @@ <h1>
1. Let _offsetBefore_ be ? GetOffsetNanosecondsFor(_timeZoneRec_, _dayBefore_).
1. Let _offsetAfter_ be ? GetOffsetNanosecondsFor(_timeZoneRec_, _dayAfter_).
1. Let _nanoseconds_ be _offsetAfter_ - _offsetBefore_.
1. If abs(_nanoseconds_) > nsPerDay, throw a *RangeError* exception.
1. If _disambiguation_ is *"earlier"*, then
1. Let _norm_ be NormalizeTimeDuration(0, 0, 0, 0, 0, -_nanoseconds_).
1. Let _earlierTime_ be AddTime(_dateTime_.[[ISOHour]], _dateTime_.[[ISOMinute]], _dateTime_.[[ISOSecond]], _dateTime_.[[ISOMillisecond]], _dateTime_.[[ISOMicrosecond]], _dateTime_.[[ISONanosecond]], _norm_).
Expand Down Expand Up @@ -978,6 +979,14 @@ <h1>
1. Repeat,
1. Let _value_ be ? IteratorStepValue(_iteratorRecord_).
1. If _value_ is ~done~, then
1. Let _numResults_ be _list_'s length.
1. If _numResults_ &gt; 1, then
1. Let _epochNs_ be a new empty List.
1. For each value _instant_ in _list_, do
1. Append _instant_.[[EpochNanoseconds]] to the end of the List _epochNs_.
1. Let _min_ be the least element of the List _epochNs_.
1. Let _max_ be the greatest element of the List _epochNs_.
1. If abs(ℝ(_max_ - _min_)) &gt; nsPerDay, throw a *RangeError* exception.
1. Return _list_.
1. If _value_ is not an Object or _value_ does not have an [[InitializedTemporalInstant]] internal slot, then
1. Let _completion_ be ThrowCompletion(a newly created *TypeError* object).
Expand Down
31 changes: 15 additions & 16 deletions spec/zoneddatetime.html
Original file line number Diff line number Diff line change
Expand Up @@ -1444,23 +1444,22 @@ <h1>
1. Else if _days_ &lt; 0 and _timeSign_ &lt; 0, then
1. Set _days_ to _days_ + 1.
1. Let _relativeResult_ be ? AddDaysToZonedDateTime(_startInstant_, _startDateTime_, _timeZoneRec_, _zonedRelativeTo_.[[Calendar]], _days_).
1. If _sign_ is 1, then
1. Repeat, while _days_ &gt; 0 and ℝ(_relativeResult_.[[EpochNanoseconds]]) &gt; _endNs_,
1. Set _days_ to _days_ - 1.
1. Set _relativeResult_ to ? AddDaysToZonedDateTime(_startInstant_, _startDateTime_, _timeZoneRec_, _zonedRelativeTo_.[[Calendar]], _days_).
1. If _sign_ = 1, and _days_ &gt; 0, and ℝ(_relativeResult_.[[EpochNanoseconds]]) &gt; _endNs_, then
1. Set _days_ to _days_ - 1.
1. Set _relativeResult_ to ? AddDaysToZonedDateTime(_startInstant_, _startDateTime_, _timeZoneRec_, _zonedRelativeTo_.[[Calendar]], _days_).
1. If _days_ &gt; 0 and ℝ(_relativeResult_.[[EpochNanoseconds]]) &gt; _endNs_, throw a *RangeError* exception.
1. Set _norm_ to NormalizedTimeDurationFromEpochNanosecondsDifference(_endNs_, _relativeResult_.[[EpochNanoseconds]]).
1. Let _done_ be *false*.
1. Let _dayLengthNs_ be ~unset~.
1. Repeat, while _done_ is *false*,
1. Let _oneDayFarther_ be ? AddDaysToZonedDateTime(_relativeResult_.[[Instant]], _relativeResult_.[[DateTime]], _timeZoneRec_, _zonedRelativeTo_.[[Calendar]], _sign_).
1. Set _dayLengthNs_ to NormalizedTimeDurationFromEpochNanosecondsDifference(_oneDayFarther_.[[EpochNanoseconds]], _relativeResult_.[[EpochNanoseconds]]).
1. Let _oneDayLess_ be ! SubtractNormalizedTimeDuration(_norm_, _dayLengthNs_).
1. If NormalizedTimeDurationSign(_oneDayLess_) &times; _sign_ &ge; 0, then
1. Set _norm_ to _oneDayLess_.
1. Set _relativeResult_ to _oneDayFarther_.
1. Set _days_ to _days_ + _sign_.
1. Else,
1. Set _done_ to *true*.
1. Let _oneDayFarther_ be ? AddDaysToZonedDateTime(_relativeResult_.[[Instant]], _relativeResult_.[[DateTime]], _timeZoneRec_, _zonedRelativeTo_.[[Calendar]], _sign_).
1. Let _dayLengthNs_ be NormalizedTimeDurationFromEpochNanosecondsDifference(_oneDayFarther.[[EpochNanoseconds]], _relativeResult_.[[EpochNanoseconds]]).
1. Let _oneDayLess_ be ! SubtractNormalizedTimeDuration(_norm_, _dayLengthNs_).
1. If NormalizedTimeDurationSign(_oneDayLess_) &times; _sign_ &ge; 0, then
1. Set _norm_ to _oneDayLess_.
1. Set _relativeResult_ to _oneDayFarther_.
1. Set _days_ to _days_ + _sign_.
1. Set _oneDayFarther_ to ? AddDaysToZonedDateTime(_relativeResult_.[[Instant]], _relativeResult_.[[DateTime]], _timeZoneRec_, _zonedRelativeTo_.[[Calendar]], _sign_).
1. Set _dayLengthNs_ to NormalizedTimeDurationFromEpochNanosecondsDifference(_oneDayFarther.[[EpochNanoseconds]], _relativeResult_.[[EpochNanoseconds]]).
1. If NormalizedTimeDurationSign(? SubtractNormalizedTimeDuration(_norm_, _dayLengthNs_)) &times; _sign_ &ge; 0, then
1. Throw a *RangeError* exception.
1. If _days_ &lt; 0 and _sign_ = 1, throw a *RangeError* exception.
1. If _days_ &gt; 0 and _sign_ = -1, throw a *RangeError* exception.
1. If NormalizedTimeDurationSign(_norm_) = -1, then
Expand Down

0 comments on commit 891769a

Please sign in to comment.