Skip to content

Commit

Permalink
Temporal: Prevent arbitrary loops in NormalizedTimeDurationToDays
Browse files Browse the repository at this point in the history
Adapts the tests that checked arbitrarily long loops, to now check that an
exception is thrown if the loop would happen.

Adds tests that exercise the newly added checks on return values of
getPossibleInstantsFor and getOffsetNanosecondsFor that limit UTC offset
shifts to 24 hours or less.

Also updates some step numbers in related tests.
  • Loading branch information
ptomato committed Feb 2, 2024
1 parent 584048e commit 3e7938c
Show file tree
Hide file tree
Showing 73 changed files with 2,960 additions and 413 deletions.
@@ -0,0 +1,48 @@
// Copyright (C) 2024 Igalia, S.L. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.

/*---
esid: sec-temporal.duration.prototype.compare
description: >
UTC offset shift returned by getPossibleInstantsFor can be at most 24 hours.
features: [Temporal]
info: |
GetPossibleInstantsFor:
5.b.i. Let _numResults_ be _list_'s length.
ii. If _numResults_ > 1, then
1. Let _epochNs_ be a new empty List.
2. For each value _instant_ in list, do
a. Append _instant_.[[EpochNanoseconds]] to the end of the List _epochNs_.
3. Let _min_ be the least element of the List _epochNs_.
4. Let _max_ be the greatest element of the List _epochNs_.
5. If abs(ℝ(_max_ - _min_)) > nsPerDay, throw a *RangeError* exception.
---*/

class ShiftLonger24Hour extends Temporal.TimeZone {
id = 'TestTimeZone';

constructor() {
super('UTC');
}

getOffsetNanosecondsFor(instant) {
return 0;
}

getPossibleInstantsFor(plainDateTime) {
const utc = new Temporal.TimeZone("UTC");
const [utcInstant] = utc.getPossibleInstantsFor(plainDateTime);
return [
utcInstant.subtract({ hours: 12, nanoseconds: 1 }),
utcInstant.add({ hours: 12 }),
utcInstant, // add a third value in case the implementation doesn't sort
];
}
}

const timeZone = new ShiftLonger24Hour();
const relativeTo = { year: 1970, month: 1, day: 1, hour: 12, timeZone };
const duration1 = new Temporal.Duration(1);
const duration2 = new Temporal.Duration(2);

assert.throws(RangeError, () => Temporal.Duration.compare(duration1, duration2, {relativeTo: relativeTo}), "RangeError should be thrown");
@@ -0,0 +1,43 @@
// Copyright (C) 2024 Igalia, S.L. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.

/*---
esid: sec-temporal.duration.prototype.compare
description: >
UTC offset shift returned by adjacent invocations of getOffsetNanosecondsFor
in DisambiguatePossibleInstants cannot be greater than 24 hours.
features: [Temporal]
info: |
DisambiguatePossibleInstants:
18. If abs(_nanoseconds_) > nsPerDay, throw a *RangeError* exception.
---*/

class ShiftLonger24Hour extends Temporal.TimeZone {
id = 'TestTimeZone';
_shiftEpochNs = 12n * 3600n * 1_000_000_000n; // 1970-01-01T12:00Z

constructor() {
super('UTC');
}

getOffsetNanosecondsFor(instant) {
if (instant.epochNanoseconds < this._shiftEpochNs) return -12 * 3600e9;
return 12 * 3600e9 + 1;
}

getPossibleInstantsFor(plainDateTime) {
const [utcInstant] = super.getPossibleInstantsFor(plainDateTime);
const { year, month, day } = plainDateTime;

if (year < 1970) return [utcInstant.subtract({ hours: 12 })];
if (year === 1970 && month === 1 && day === 1) return [];
return [utcInstant.add({ hours: 12, nanoseconds: 1 })];
}
}

const timeZone = new ShiftLonger24Hour();
const relativeTo = { year: 1970, month: 1, day: 1, hour: 12, timeZone };
const duration1 = new Temporal.Duration(1);
const duration2 = new Temporal.Duration(2);

assert.throws(RangeError, () => Temporal.Duration.compare(duration1, duration2, {relativeTo: relativeTo}), "RangeError should be thrown");
Expand Up @@ -4,72 +4,43 @@
/*---
esid: sec-temporal.duration.prototype.add
description: >
NormalizedTimeDurationToDays can loop arbitrarily up to max safe integer
NormalizedTimeDurationToDays should not be able to loop arbitrarily.
info: |
NormalizedTimeDurationToDays ( norm, zonedRelativeTo, timeZoneRec [ , precalculatedPlainDatetime ] )
...
21. Repeat, while done is false,
a. Let oneDayFarther be ? AddDaysToZonedDateTime(relativeResult.[[Instant]],
relativeResult.[[DateTime]], timeZoneRec, zonedRelativeTo.[[Calendar]], sign).
b. Set dayLengthNs to NormalizedTimeDurationFromEpochNanosecondsDifference(oneDayFarther.[[EpochNanoseconds]],
relativeResult.[[EpochNanoseconds]]).
c. Let oneDayLess be ? SubtractNormalizedTimeDuration(norm, dayLengthNs).
c. If NormalizedTimeDurationSign(oneDayLess) × sign ≥ 0, then
i. Set norm to oneDayLess.
ii. Set relativeResult to oneDayFarther.
iii. Set days to days + sign.
d. Else,
i. Set done to true.
includes: [temporalHelpers.js]
22. If NormalizedTimeDurationSign(_oneDayLess_) × _sign_ ≥ 0, then
a. Set _norm_ to _oneDayLess_.
b. Set _relativeResult_ to _oneDayFarther_.
c. Set _days_ to _days_ + _sign_.
d. Set _oneDayFarther_ to ? AddDaysToZonedDateTime(_relativeResult_.[[Instant]], _relativeResult_.[[DateTime]], _timeZoneRec_, _zonedRelativeTo_.[[Calendar]], _sign_).
e. Set dayLengthNs to NormalizedTimeDurationFromEpochNanosecondsDifference(_oneDayFarther.[[EpochNanoseconds]], relativeResult.[[EpochNanoseconds]]).
f. If NormalizedTimeDurationSign(? SubtractNormalizedTimeDuration(_norm_, _dayLengthNs_)) × _sign_ ≥ 0, then
i. Throw a *RangeError* exception.
features: [Temporal]
---*/

const calls = [];
const duration = Temporal.Duration.from({ days: 1 });

function createRelativeTo(count) {
const dayLengthNs = 86400000000000n;
const dayInstant = new Temporal.Instant(dayLengthNs);
const substitutions = [];
const timeZone = new Temporal.TimeZone("UTC");
// Return constant value for first _count_ calls
TemporalHelpers.substituteMethod(
timeZone,
"getPossibleInstantsFor",
substitutions
);
substitutions.length = count;
let i = 0;
for (i = 0; i < substitutions.length; i++) {
// (this value)
substitutions[i] = [dayInstant];
const dayLengthNs = 86400000000000n;
const dayInstant = new Temporal.Instant(dayLengthNs);
let calls = 0;
const timeZone = new class extends Temporal.TimeZone {
getPossibleInstantsFor() {
calls++;
return [dayInstant];
}
// Record calls in calls[]
TemporalHelpers.observeMethod(calls, timeZone, "getPossibleInstantsFor");
return new Temporal.ZonedDateTime(0n, timeZone);
}
}("UTC");

let zdt = createRelativeTo(50);
calls.splice(0); // Reset calls list after ZonedDateTime construction
duration.add(duration, {
relativeTo: zdt,
});
assert.sameValue(
calls.length,
50 + 1,
"Expected duration.add to call getPossibleInstantsFor correct number of times"
);
const relativeTo = new Temporal.ZonedDateTime(0n, timeZone);

zdt = createRelativeTo(100);
calls.splice(0); // Reset calls list after previous loop + ZonedDateTime construction
duration.add(duration, {
relativeTo: zdt,
});
assert.sameValue(
calls.length,
100 + 1,
"Expected duration.add to call getPossibleInstantsFor correct number of times"
);

zdt = createRelativeTo(107);
assert.throws(RangeError, () => duration.add(duration, { relativeTo: zdt }), "107-2 days > 2⁵³ ns");
assert.throws(RangeError, () => duration.add(duration, { relativeTo }), "arbitrarily long loop is prevented");
assert.sameValue(calls, 5, "getPossibleInstantsFor is not called in an arbitrarily long loop");
// Expected calls:
// AddDuration ->
// AddZonedDateTime (1)
// AddZonedDateTime (2)
// DifferenceZonedDateTime ->
// NormalizedTimeDurationToDays ->
// AddDaysToZonedDateTime (3, step 12)
// AddDaysToZonedDateTime (4, step 15)
// AddDaysToZonedDateTime (5, step 18.d)
@@ -0,0 +1,47 @@
// Copyright (C) 2024 Igalia, S.L. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.

/*---
esid: sec-temporal.duration.prototype.add
description: >
UTC offset shift returned by getPossibleInstantsFor can be at most 24 hours.
features: [Temporal]
info: |
GetPossibleInstantsFor:
5.b.i. Let _numResults_ be _list_'s length.
ii. If _numResults_ > 1, then
1. Let _epochNs_ be a new empty List.
2. For each value _instant_ in list, do
a. Append _instant_.[[EpochNanoseconds]] to the end of the List _epochNs_.
3. Let _min_ be the least element of the List _epochNs_.
4. Let _max_ be the greatest element of the List _epochNs_.
5. If abs(ℝ(_max_ - _min_)) > nsPerDay, throw a *RangeError* exception.
---*/

class ShiftLonger24Hour extends Temporal.TimeZone {
id = 'TestTimeZone';

constructor() {
super('UTC');
}

getOffsetNanosecondsFor(instant) {
return 0;
}

getPossibleInstantsFor(plainDateTime) {
const utc = new Temporal.TimeZone("UTC");
const [utcInstant] = utc.getPossibleInstantsFor(plainDateTime);
return [
utcInstant.subtract({ hours: 12, nanoseconds: 1 }),
utcInstant.add({ hours: 12 }),
utcInstant, // add a third value in case the implementation doesn't sort
];
}
}

const timeZone = new ShiftLonger24Hour();
const relativeTo = { year: 1970, month: 1, day: 1, hour: 12, timeZone };

const instance = new Temporal.Duration(1, 0, 0, 1);
assert.throws(RangeError, () => instance.add(new Temporal.Duration(0, 0, 0, 0, -24), { relativeTo }), "RangeError should be thrown");
@@ -0,0 +1,42 @@
// Copyright (C) 2024 Igalia, S.L. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.

/*---
esid: sec-temporal.duration.prototype.add
description: >
UTC offset shift returned by adjacent invocations of getOffsetNanosecondsFor
in DisambiguatePossibleInstants cannot be greater than 24 hours.
features: [Temporal]
info: |
DisambiguatePossibleInstants:
18. If abs(_nanoseconds_) > nsPerDay, throw a *RangeError* exception.
---*/

class ShiftLonger24Hour extends Temporal.TimeZone {
id = 'TestTimeZone';
_shiftEpochNs = 12n * 3600n * 1_000_000_000n; // 1970-01-01T12:00Z

constructor() {
super('UTC');
}

getOffsetNanosecondsFor(instant) {
if (instant.epochNanoseconds < this._shiftEpochNs) return -12 * 3600e9;
return 12 * 3600e9 + 1;
}

getPossibleInstantsFor(plainDateTime) {
const [utcInstant] = super.getPossibleInstantsFor(plainDateTime);
const { year, month, day } = plainDateTime;

if (year < 1970) return [utcInstant.subtract({ hours: 12 })];
if (year === 1970 && month === 1 && day === 1) return [];
return [utcInstant.add({ hours: 12, nanoseconds: 1 })];
}
}

const timeZone = new ShiftLonger24Hour();
const relativeTo = { year: 1970, month: 1, day: 1, hour: 12, timeZone };

const instance = new Temporal.Duration(1, 0, 0, 1);
assert.throws(RangeError, () => instance.add(new Temporal.Duration(0, 0, 0, 0, -24), { relativeTo }), "RangeError should be thrown");
Expand Up @@ -7,12 +7,12 @@ description: >
RangeErrors.
info: |
NormalizedTimeDurationToDays ( norm, zonedRelativeTo, timeZoneRec [ , precalculatedPlainDateTime ] )
22. If days < 0 and sign = 1, throw a RangeError exception.
23. If days > 0 and sign = -1, throw a RangeError exception.
23. If days < 0 and sign = 1, throw a RangeError exception.
24. If days > 0 and sign = -1, throw a RangeError exception.
...
25. If NormalizedTimeDurationSign(_norm_) = 1 and sign = -1, throw a RangeError exception.
26. If NormalizedTimeDurationSign(_norm_) = 1 and sign = -1, throw a RangeError exception.
...
28. If dayLength ≥ 2⁵³, throw a RangeError exception.
29. If dayLength ≥ 2⁵³, throw a RangeError exception.
features: [Temporal, BigInt]
includes: [temporalHelpers.js]
---*/
Expand All @@ -39,7 +39,7 @@ function timeZoneSubstituteValues(
return tz;
}

// Step 22: days < 0 and sign = 1
// Step 23: days < 0 and sign = 1
let zdt = new Temporal.ZonedDateTime(
-1n, // Set DifferenceZonedDateTime _ns1_
timeZoneSubstituteValues(
Expand All @@ -66,7 +66,7 @@ assert.throws(RangeError, () =>
"days < 0 and sign = 1"
);

// Step 23: days > 0 and sign = -1
// Step 24: days > 0 and sign = -1
zdt = new Temporal.ZonedDateTime(
1n, // Set DifferenceZonedDateTime _ns1_
timeZoneSubstituteValues(
Expand All @@ -93,15 +93,15 @@ assert.throws(RangeError, () =>
"days > 0 and sign = -1"
);

// Step 25: nanoseconds > 0 and sign = -1
// Step 26: nanoseconds > 0 and sign = -1
zdt = new Temporal.ZonedDateTime(
0n, // Set DifferenceZonedDateTime _ns1_
timeZoneSubstituteValues(
[
TemporalHelpers.SUBSTITUTE_SKIP, // Behave normally for first call, AddDuration step 15
[new Temporal.Instant(-1n)], // Returned in AddDuration step 16, setting _endNs_ -> DifferenceZonedDateTime _ns2_
[new Temporal.Instant(-2n)], // Returned in step 16, setting _relativeResult_
[new Temporal.Instant(-4n)], // Returned in step 21.a, setting _oneDayFarther_
[new Temporal.Instant(-4n)], // Returned in step 19, setting _oneDayFarther_
],
[
// Behave normally in 3 calls made prior to NanosecondsToDays
Expand All @@ -121,15 +121,15 @@ assert.throws(RangeError, () =>
"nanoseconds > 0 and sign = -1"
);

// Step 28: day length is an unsafe integer
// Step 29: day length is an unsafe integer
zdt = new Temporal.ZonedDateTime(
0n,
timeZoneSubstituteValues(
[
TemporalHelpers.SUBSTITUTE_SKIP, // Behave normally for AddDuration step 15
TemporalHelpers.SUBSTITUTE_SKIP, // Behave normally for AddDuration step 16
TemporalHelpers.SUBSTITUTE_SKIP, // Behave normally for step 16, setting _relativeResult_
// Returned in step 21.a, making _oneDayFarther_ 2^53 ns later than _relativeResult_
// Returned in step 19, making _oneDayFarther_ 2^53 ns later than _relativeResult_
[new Temporal.Instant(2n ** 53n + 2n * BigInt(dayNs))],
],
[]
Expand Down

0 comments on commit 3e7938c

Please sign in to comment.