Skip to content

Commit 891769a

Browse files
committed
Normative: Prevent arbitrary loops in NormalizedTimeDurationToDays
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.
1 parent 45b5e1f commit 891769a

File tree

3 files changed

+67
-34
lines changed

3 files changed

+67
-34
lines changed

polyfill/lib/ecmascript.mjs

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* global __debug__ */
22

33
const ArrayIncludes = Array.prototype.includes;
4+
const ArrayPrototypeMap = Array.prototype.map;
45
const ArrayPrototypePush = Array.prototype.push;
56
const ArrayPrototypeSort = Array.prototype.sort;
67
const ArrayPrototypeFind = Array.prototype.find;
@@ -2422,6 +2423,10 @@ export function DisambiguatePossibleInstants(possibleInstants, timeZoneRec, date
24222423
const offsetBefore = GetOffsetNanosecondsFor(timeZoneRec, dayBefore);
24232424
const offsetAfter = GetOffsetNanosecondsFor(timeZoneRec, dayAfter);
24242425
const nanoseconds = offsetAfter - offsetBefore;
2426+
if (MathAbs(nanoseconds) > DAY_NANOS) {
2427+
throw new RangeError('bad return from getOffsetNanosecondsFor: UTC offset shift longer than 24 hours');
2428+
}
2429+
24252430
switch (disambiguation) {
24262431
case 'earlier': {
24272432
const norm = TimeDuration.normalize(0, 0, 0, 0, 0, -nanoseconds);
@@ -2479,6 +2484,17 @@ export function GetPossibleInstantsFor(timeZoneRec, dateTime) {
24792484
}
24802485
Call(ArrayPrototypePush, result, [instant]);
24812486
}
2487+
2488+
const numResults = result.length;
2489+
if (numResults > 1) {
2490+
const mapped = Call(ArrayPrototypeMap, result, [(i) => GetSlot(i, EPOCHNANOSECONDS)]);
2491+
const min = bigInt.min(...mapped);
2492+
const max = bigInt.max(...mapped);
2493+
if (bigInt(max).subtract(min).abs().greater(DAY_NANOS)) {
2494+
throw new RangeError('bad return from getPossibleInstantsFor: UTC offset shift longer than 24 hours');
2495+
}
2496+
}
2497+
24822498
return result;
24832499
}
24842500

@@ -3260,20 +3276,34 @@ export function NormalizedTimeDurationToDays(norm, zonedRelativeTo, timeZoneRec,
32603276
// back inside the period where it belongs. Note that this case only can
32613277
// happen for positive durations because the only direction that
32623278
// `disambiguation: 'compatible'` can change clock time is forwards.
3263-
if (sign === 1) {
3264-
while (days > 0 && relativeResult.epochNs.greater(endNs)) {
3265-
days--;
3266-
relativeResult = AddDaysToZonedDateTime(start, dtStart, timeZoneRec, calendar, days);
3267-
// may do disambiguation
3279+
if (sign === 1 && days > 0 && relativeResult.epochNs.greater(endNs)) {
3280+
days--;
3281+
relativeResult = AddDaysToZonedDateTime(start, dtStart, timeZoneRec, calendar, days);
3282+
// may do disambiguation
3283+
if (days > 0 && relativeResult.epochNs.greater(endNs)) {
3284+
throw new RangeError('inconsistent result from custom time zone getInstantFor()');
32683285
}
32693286
}
32703287
norm = TimeDuration.fromEpochNsDiff(endNs, relativeResult.epochNs);
32713288

3272-
let isOverflow = false;
3273-
let dayLengthNs;
3274-
do {
3275-
// calculate length of the next day (day that contains the time remainder)
3276-
const oneDayFarther = AddDaysToZonedDateTime(
3289+
// calculate length of the next day (day that contains the time remainder)
3290+
let oneDayFarther = AddDaysToZonedDateTime(
3291+
relativeResult.instant,
3292+
relativeResult.dateTime,
3293+
timeZoneRec,
3294+
calendar,
3295+
sign
3296+
);
3297+
let dayLengthNs = TimeDuration.fromEpochNsDiff(oneDayFarther.epochNs, relativeResult.epochNs);
3298+
const oneDayLess = norm.subtract(dayLengthNs);
3299+
let isOverflow = oneDayLess.sign() * sign >= 0;
3300+
if (isOverflow) {
3301+
norm = oneDayLess;
3302+
relativeResult = oneDayFarther;
3303+
days += sign;
3304+
3305+
// ensure there was no more overflow
3306+
oneDayFarther = AddDaysToZonedDateTime(
32773307
relativeResult.instant,
32783308
relativeResult.dateTime,
32793309
timeZoneRec,
@@ -3282,14 +3312,9 @@ export function NormalizedTimeDurationToDays(norm, zonedRelativeTo, timeZoneRec,
32823312
);
32833313

32843314
dayLengthNs = TimeDuration.fromEpochNsDiff(oneDayFarther.epochNs, relativeResult.epochNs);
3285-
const oneDayLess = norm.subtract(dayLengthNs);
3286-
isOverflow = oneDayLess.sign() * sign >= 0;
3287-
if (isOverflow) {
3288-
norm = oneDayLess;
3289-
relativeResult = oneDayFarther;
3290-
days += sign;
3291-
}
3292-
} while (isOverflow);
3315+
isOverflow = norm.subtract(dayLengthNs).sign() * sign >= 0;
3316+
if (isOverflow) throw new RangeError('inconsistent result from custom time zone getPossibleInstantsFor()');
3317+
}
32933318
if (days !== 0 && MathSign(days) != sign) {
32943319
throw new RangeError('Time zone or calendar converted nanoseconds into a number of days with the opposite sign');
32953320
}

spec/timezone.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -937,6 +937,7 @@ <h1>
937937
1. Let _offsetBefore_ be ? GetOffsetNanosecondsFor(_timeZoneRec_, _dayBefore_).
938938
1. Let _offsetAfter_ be ? GetOffsetNanosecondsFor(_timeZoneRec_, _dayAfter_).
939939
1. Let _nanoseconds_ be _offsetAfter_ - _offsetBefore_.
940+
1. If abs(_nanoseconds_) > nsPerDay, throw a *RangeError* exception.
940941
1. If _disambiguation_ is *"earlier"*, then
941942
1. Let _norm_ be NormalizeTimeDuration(0, 0, 0, 0, 0, -_nanoseconds_).
942943
1. Let _earlierTime_ be AddTime(_dateTime_.[[ISOHour]], _dateTime_.[[ISOMinute]], _dateTime_.[[ISOSecond]], _dateTime_.[[ISOMillisecond]], _dateTime_.[[ISOMicrosecond]], _dateTime_.[[ISONanosecond]], _norm_).
@@ -978,6 +979,14 @@ <h1>
978979
1. Repeat,
979980
1. Let _value_ be ? IteratorStepValue(_iteratorRecord_).
980981
1. If _value_ is ~done~, then
982+
1. Let _numResults_ be _list_'s length.
983+
1. If _numResults_ &gt; 1, then
984+
1. Let _epochNs_ be a new empty List.
985+
1. For each value _instant_ in _list_, do
986+
1. Append _instant_.[[EpochNanoseconds]] to the end of the List _epochNs_.
987+
1. Let _min_ be the least element of the List _epochNs_.
988+
1. Let _max_ be the greatest element of the List _epochNs_.
989+
1. If abs(ℝ(_max_ - _min_)) &gt; nsPerDay, throw a *RangeError* exception.
981990
1. Return _list_.
982991
1. If _value_ is not an Object or _value_ does not have an [[InitializedTemporalInstant]] internal slot, then
983992
1. Let _completion_ be ThrowCompletion(a newly created *TypeError* object).

spec/zoneddatetime.html

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1444,23 +1444,22 @@ <h1>
14441444
1. Else if _days_ &lt; 0 and _timeSign_ &lt; 0, then
14451445
1. Set _days_ to _days_ + 1.
14461446
1. Let _relativeResult_ be ? AddDaysToZonedDateTime(_startInstant_, _startDateTime_, _timeZoneRec_, _zonedRelativeTo_.[[Calendar]], _days_).
1447-
1. If _sign_ is 1, then
1448-
1. Repeat, while _days_ &gt; 0 and ℝ(_relativeResult_.[[EpochNanoseconds]]) &gt; _endNs_,
1449-
1. Set _days_ to _days_ - 1.
1450-
1. Set _relativeResult_ to ? AddDaysToZonedDateTime(_startInstant_, _startDateTime_, _timeZoneRec_, _zonedRelativeTo_.[[Calendar]], _days_).
1447+
1. If _sign_ = 1, and _days_ &gt; 0, and ℝ(_relativeResult_.[[EpochNanoseconds]]) &gt; _endNs_, then
1448+
1. Set _days_ to _days_ - 1.
1449+
1. Set _relativeResult_ to ? AddDaysToZonedDateTime(_startInstant_, _startDateTime_, _timeZoneRec_, _zonedRelativeTo_.[[Calendar]], _days_).
1450+
1. If _days_ &gt; 0 and ℝ(_relativeResult_.[[EpochNanoseconds]]) &gt; _endNs_, throw a *RangeError* exception.
14511451
1. Set _norm_ to NormalizedTimeDurationFromEpochNanosecondsDifference(_endNs_, _relativeResult_.[[EpochNanoseconds]]).
1452-
1. Let _done_ be *false*.
1453-
1. Let _dayLengthNs_ be ~unset~.
1454-
1. Repeat, while _done_ is *false*,
1455-
1. Let _oneDayFarther_ be ? AddDaysToZonedDateTime(_relativeResult_.[[Instant]], _relativeResult_.[[DateTime]], _timeZoneRec_, _zonedRelativeTo_.[[Calendar]], _sign_).
1456-
1. Set _dayLengthNs_ to NormalizedTimeDurationFromEpochNanosecondsDifference(_oneDayFarther_.[[EpochNanoseconds]], _relativeResult_.[[EpochNanoseconds]]).
1457-
1. Let _oneDayLess_ be ! SubtractNormalizedTimeDuration(_norm_, _dayLengthNs_).
1458-
1. If NormalizedTimeDurationSign(_oneDayLess_) &times; _sign_ &ge; 0, then
1459-
1. Set _norm_ to _oneDayLess_.
1460-
1. Set _relativeResult_ to _oneDayFarther_.
1461-
1. Set _days_ to _days_ + _sign_.
1462-
1. Else,
1463-
1. Set _done_ to *true*.
1452+
1. Let _oneDayFarther_ be ? AddDaysToZonedDateTime(_relativeResult_.[[Instant]], _relativeResult_.[[DateTime]], _timeZoneRec_, _zonedRelativeTo_.[[Calendar]], _sign_).
1453+
1. Let _dayLengthNs_ be NormalizedTimeDurationFromEpochNanosecondsDifference(_oneDayFarther.[[EpochNanoseconds]], _relativeResult_.[[EpochNanoseconds]]).
1454+
1. Let _oneDayLess_ be ! SubtractNormalizedTimeDuration(_norm_, _dayLengthNs_).
1455+
1. If NormalizedTimeDurationSign(_oneDayLess_) &times; _sign_ &ge; 0, then
1456+
1. Set _norm_ to _oneDayLess_.
1457+
1. Set _relativeResult_ to _oneDayFarther_.
1458+
1. Set _days_ to _days_ + _sign_.
1459+
1. Set _oneDayFarther_ to ? AddDaysToZonedDateTime(_relativeResult_.[[Instant]], _relativeResult_.[[DateTime]], _timeZoneRec_, _zonedRelativeTo_.[[Calendar]], _sign_).
1460+
1. Set _dayLengthNs_ to NormalizedTimeDurationFromEpochNanosecondsDifference(_oneDayFarther.[[EpochNanoseconds]], _relativeResult_.[[EpochNanoseconds]]).
1461+
1. If NormalizedTimeDurationSign(? SubtractNormalizedTimeDuration(_norm_, _dayLengthNs_)) &times; _sign_ &ge; 0, then
1462+
1. Throw a *RangeError* exception.
14641463
1. If _days_ &lt; 0 and _sign_ = 1, throw a *RangeError* exception.
14651464
1. If _days_ &gt; 0 and _sign_ = -1, throw a *RangeError* exception.
14661465
1. If NormalizedTimeDurationSign(_norm_) = -1, then

0 commit comments

Comments
 (0)