Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Temporal: Prevent arbitrary loops in NormalizedTimeDurationToDays #3999

Merged
merged 1 commit into from
Feb 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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");
Original file line number Diff line number Diff line change
@@ -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");
Original file line number Diff line number Diff line change
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)
Original file line number Diff line number Diff line change
@@ -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");
Original file line number Diff line number Diff line change
@@ -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");
Original file line number Diff line number Diff line change
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
Loading
Loading