Skip to content

Commit 2b1b525

Browse files
[runtime] Checked math in date calculations which may overflow (#242)
## Summary The fuzzer caught assertion failures due to overflow during some date calculations. Specifically callers to `make_day` could cause an overflow if the year argument is high enough. We can fix this by using checked operations in all functions that could potentially overflow. This could only cause a crash due to the year argument - months and days already have their ranges validated or controlled. I also audited the rest of the date calculations and this was the only potential overflow I found. All other cases operate on f64s or validate the range of their inputs. The primary functions that rely on callers to validate inputs to prevent overflow have been annotated with this fact. ## Tests Added regression test for failing date calculations in the common cases I could trigger it.
1 parent f04d098 commit 2b1b525

3 files changed

Lines changed: 51 additions & 21 deletions

File tree

src/js/runtime/intrinsics/date_object.rs

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -235,36 +235,48 @@ pub fn make_full_year(year: f64) -> f64 {
235235
}
236236
}
237237

238-
// Floor division - round towards negative infintiy
239-
fn floor_div(a: i64, b: i64) -> i64 {
238+
/// Floor division - round towards negative infinity
239+
///
240+
/// Returns None on overflow.
241+
fn floor_div(a: i64, b: i64) -> Option<i64> {
240242
if a >= 0 {
241-
a / b
243+
a.checked_div(b)
242244
} else {
243-
((a + 1) / b) - 1
245+
a.checked_add(1)?.checked_div(b)?.checked_sub(1)
244246
}
245247
}
246248

247249
fn is_leap_year(year: i64) -> bool {
248250
year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
249251
}
250252

251-
// Calculate number of leap years after year 0 but before the given year
252-
fn leap_years_before_year(year: i64) -> i64 {
253-
let year = year - 1;
254-
floor_div(year, 4) - floor_div(year, 100) + floor_div(year, 400)
253+
/// Calculate number of leap years after year 0 but before the given year.
254+
///
255+
/// Returns None on overflow.
256+
fn leap_years_before_year(year: i64) -> Option<i64> {
257+
let year = year.checked_sub(1)?;
258+
floor_div(year, 4)?
259+
.checked_sub(floor_div(year, 100)?)?
260+
.checked_add(floor_div(year, 400)?)
255261
}
256262

257-
// Calculate number of leap years in a time period between two years
258-
fn leap_years_between_years(start_year: i64, end_year: i64) -> i64 {
259-
leap_years_before_year(end_year) - leap_years_before_year(start_year)
263+
/// Calculate number of leap years in a time period between two years
264+
///
265+
/// Returns None on overflow.
266+
fn leap_years_between_years(start_year: i64, end_year: i64) -> Option<i64> {
267+
leap_years_before_year(end_year)?.checked_sub(leap_years_before_year(start_year)?)
260268
}
261269

262-
fn year_to_days_since_unix_epoch(year: i64) -> i64 {
263-
let years_since_epoch = year - 1970;
270+
fn year_to_days_since_unix_epoch(year: i64) -> Option<i64> {
271+
let years_since_epoch = year.checked_sub(1970)?;
264272
if years_since_epoch >= 0 {
265-
years_since_epoch * 365 + leap_years_between_years(1970, year)
273+
years_since_epoch
274+
.checked_mul(365)?
275+
.checked_add(leap_years_between_years(1970, year)?)
266276
} else {
267-
years_since_epoch * 365 - leap_years_between_years(year, 1970)
277+
years_since_epoch
278+
.checked_mul(365)?
279+
.checked_sub(leap_years_between_years(year, 1970)?)
268280
}
269281
}
270282

@@ -300,8 +312,11 @@ fn year_month_day_to_days_since_year_start(year: i64, month: i64, day: i64) -> i
300312

301313
/// Year + month + day to the number of days since the Unix epoch. Months and days are 1-indexed.
302314
/// Month must be between 1 and 12, day is not constrained.
303-
pub fn year_month_day_to_days_since_unix_epoch(year: i64, month: i64, day: i64) -> i64 {
304-
year_to_days_since_unix_epoch(year) + year_month_day_to_days_since_year_start(year, month, day)
315+
///
316+
/// Returns None on overflow.
317+
pub fn year_month_day_to_days_since_unix_epoch(year: i64, month: i64, day: i64) -> Option<i64> {
318+
year_to_days_since_unix_epoch(year)?
319+
.checked_add(year_month_day_to_days_since_year_start(year, month, day))
305320
}
306321

307322
/// MakeDay (https://tc39.es/ecma262/#sec-makeday)
@@ -327,10 +342,13 @@ pub fn make_day(year: f64, month: f64, date: f64) -> f64 {
327342
let calculated_year = calculated_year as i64;
328343
let calculated_month = calculated_month as i64 + 1;
329344

330-
let num_days_until_month_start =
331-
year_month_day_to_days_since_unix_epoch(calculated_year, calculated_month, 1) as f64;
345+
let Some(num_days_until_month_start) =
346+
year_month_day_to_days_since_unix_epoch(calculated_year, calculated_month, 1)
347+
else {
348+
return f64::NAN;
349+
};
332350

333-
num_days_until_month_start + date - 1.0
351+
(num_days_until_month_start as f64) + date - 1.0
334352
}
335353

336354
/// MakeDate (https://tc39.es/ecma262/#sec-makedate)

src/js/runtime/string_parsing.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,7 @@ fn parse_string_to_iso_date(mut lexer: StringLexer) -> Option<f64> {
804804
return None;
805805
}
806806

807+
// Unchecked operations since ranges have already been validated
807808
sign * (timezone_hour * MS_PER_HOUR as i64 + timezone_minute * MS_PER_MINUTE as i64)
808809
} else if has_time {
809810
// TODO: Use current time zone offset
@@ -829,6 +830,8 @@ fn parse_string_to_iso_date(mut lexer: StringLexer) -> Option<f64> {
829830
)
830831
}
831832

833+
/// Callers must ensure that the date parts are in valid ranges so that overflow does not occur
834+
/// when calculating the time value.
832835
fn utc_time_from_full_date_parts(
833836
year: i64,
834837
month: i64,
@@ -840,7 +843,7 @@ fn utc_time_from_full_date_parts(
840843
timezone_offset_milliseconds: i64,
841844
) -> Option<f64> {
842845
let date_part_milliseconds =
843-
year_month_day_to_days_since_unix_epoch(year, month, day) * MS_PER_DAY as i64;
846+
year_month_day_to_days_since_unix_epoch(year, month, day).unwrap() * MS_PER_DAY as i64;
844847

845848
let time_part_milliseconds = hour * MS_PER_HOUR as i64
846849
+ minute * MS_PER_MINUTE as i64
@@ -935,6 +938,7 @@ fn parse_string_to_utc_or_default_date(mut lexer: StringLexer) -> Option<f64> {
935938
return None;
936939
}
937940

941+
// Unchecked operations since ranges have already been validated
938942
sign * (timezone_hour * MS_PER_HOUR as i64 + timezone_minute * MS_PER_MINUTE as i64)
939943
};
940944

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/*---
2+
description: Crash due to overflow during date calculations.
3+
---*/
4+
5+
assert.sameValue(new Date(1.0e+308, 0).toString(), "Invalid Date");
6+
assert(Number.isNaN(Date.UTC(1.0e+308)));
7+
assert(Number.isNaN(new Date().setFullYear(1.0e+308)));
8+
assert(Number.isNaN(new Date().setUTCFullYear(1.0e+308)));

0 commit comments

Comments
 (0)