Repository navigation
Some fallible-looking or infallible conversion paths can still panic on extreme inputs #1802
Description
Summary
I found a few chrono panic paths that look worth either hardening or documenting more directly.
I am not including the many deprecated constructors and operator-overflow cases as main findings, because most of those already have # Panics docs or *_opt / checked_* alternatives. The main points below are the ones that surprised me more:
DateTime::<Utc>::from(SystemTime)can panic for extreme but representableSystemTimevalues.TimeDelta::checked_subcan panic instead of returningNoneon some extreme inputs.- Malformed TZif data can panic during local timezone parsing through unchecked count arithmetic.
Tested on:
chrono 0.4.44
Findings
| Area | API / function | Panic site | Trigger |
|---|---|---|---|
| Infallible conversion | DateTime::<Utc>::from(SystemTime) |
src/datetime/mod.rs:1928, src/datetime/mod.rs:1931 |
extreme pre/post epoch SystemTime |
| Checked duration arithmetic | TimeDelta::checked_sub |
src/time_delta.rs:398 |
TimeDelta::MIN.checked_sub(&TimeDelta::MAX) |
| TZif parser | local timezone loading via Local |
src/offset/local/tz_info/parser.rs:136 |
malformed TZif header with huge type_count |
Example 1: DateTime::<Utc>::from(SystemTime) can panic
The From<SystemTime> impl is infallible at the type level, but it unwraps the result of Utc.timestamp_opt:
// src/datetime/mod.rs:1920-1931
impl From<SystemTime> for DateTime<Utc> {
fn from(t: SystemTime) -> DateTime<Utc> {
let (sec, nsec) = match t.duration_since(UNIX_EPOCH) {
Ok(dur) => (dur.as_secs() as i64, dur.subsec_nanos()),
Err(e) => {
let dur = e.duration();
let (sec, nsec) = (dur.as_secs() as i64, dur.subsec_nanos());
if nsec == 0 { (-sec, 0) } else { (-sec - 1, 1_000_000_000 - nsec) }
}
};
Utc.timestamp_opt(sec, nsec).unwrap()
}
}For extreme SystemTime values, the conversion can panic instead of being documented as fallible or saturating.
Reproducer
use chrono::{DateTime, Utc};
use std::time::{Duration, UNIX_EPOCH};
#[test]
#[should_panic]
fn systemtime_to_datetime_utc_can_panic() {
let t = UNIX_EPOCH
.checked_add(Duration::from_secs(i64::MAX as u64 + 1))
.unwrap();
let _: DateTime<Utc> = DateTime::<Utc>::from(t);
}A pre-epoch variant can also panic:
use chrono::{DateTime, Utc};
use std::time::{Duration, UNIX_EPOCH};
#[test]
#[should_panic]
fn pre_epoch_systemtime_to_datetime_utc_can_panic() {
let t = UNIX_EPOCH - Duration::new(1u64 << 63, 1);
let _: DateTime<Utc> = DateTime::<Utc>::from(t);
}Expected:
Either document the panic on out-of-range SystemTime, or expose/point users to a fallible conversion path so callers can handle values outside Chrono's representable range without unwinding.
Example 2: TimeDelta::checked_sub can panic instead of returning None
checked_sub is documented as returning None if overflow occurs, but it first performs unchecked i64 subtraction:
// src/time_delta.rs:393-404
/// Subtract two `TimeDelta`s, returning `None` if overflow occurred.
#[must_use]
pub const fn checked_sub(&self, rhs: &TimeDelta) -> Option<TimeDelta> {
// No overflow checks here because we stay comfortably within the range of an `i64`.
// Range checks happen in `TimeDelta::new`.
let mut secs = self.secs - rhs.secs;
let mut nanos = self.nanos - rhs.nanos;
if nanos < 0 {
nanos += NANOS_PER_SEC;
secs -= 1;
}
TimeDelta::new(secs, nanos as u32)
}That means the "checked" method itself can panic before it reaches TimeDelta::new.
Reproducer
use chrono::TimeDelta;
#[test]
#[should_panic]
fn checked_sub_can_panic_on_extreme_values() {
let _ = TimeDelta::MIN.checked_sub(&TimeDelta::MAX);
}Expected:
checked_sub should return None for this overflow case, or its docs should say it can panic on internal i64 arithmetic overflow.
The same area may be worth reviewing for checked_add, since it also performs unchecked self.secs + rhs.secs before range-checking through TimeDelta::new:
// src/time_delta.rs:381-390
pub const fn checked_add(&self, rhs: &TimeDelta) -> Option<TimeDelta> {
let mut secs = self.secs + rhs.secs;
let mut nanos = self.nanos + rhs.nanos;
...
TimeDelta::new(secs, nanos as u32)
}Example 3: malformed TZif data can panic during local timezone parsing
The TZif parser multiplies counts from the file header before reading sections:
// src/offset/local/tz_info/parser.rs:132-140
Ok(Self {
time_size,
transition_times: cursor.read_exact(header.transition_count * time_size)?,
transition_types: cursor.read_exact(header.transition_count)?,
local_time_types: cursor.read_exact(header.type_count * 6)?,
names: cursor.read_exact(header.char_count)?,
leap_seconds: cursor.read_exact(header.leap_count * (time_size + 4))?,
std_walls: cursor.read_exact(header.std_wall_count)?,
ut_locals: cursor.read_exact(header.ut_local_count)?,
header,
})With a malformed TZif header, header.type_count * 6 can overflow before the parser returns an error.
Reproducer
use chrono::Local;
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
fn crafted_tzif_overflow_header() -> Vec<u8> {
let mut bytes = Vec::with_capacity(44);
bytes.extend_from_slice(b"TZif");
bytes.push(0x00); // V1
bytes.extend_from_slice(&[0u8; 15]);
// ut_local_count, std_wall_count, leap_count, transition_count
bytes.extend_from_slice(&0u32.to_be_bytes());
bytes.extend_from_slice(&0u32.to_be_bytes());
bytes.extend_from_slice(&0u32.to_be_bytes());
bytes.extend_from_slice(&0u32.to_be_bytes());
// type_count chosen to overflow `type_count * 6`
bytes.extend_from_slice(&u32::MAX.to_be_bytes());
// char_count must be non-zero to pass header validation
bytes.extend_from_slice(&1u32.to_be_bytes());
bytes
}
#[test]
#[should_panic]
fn malformed_tzif_header_can_panic() {
let mut path: PathBuf = std::env::temp_dir();
let nanos = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
path.push(format!("chrono_tzif_overflow_{nanos}.tzif"));
fs::write(&path, crafted_tzif_overflow_header()).unwrap();
std::env::set_var("TZ", &path);
let _ = Local::now();
}Expected:
Malformed TZif data should be rejected with an error/fallback path instead of panicking during section-size arithmetic. Using checked multiplication before read_exact would make this path easier to contain.
Additional parser / formatting edge cases from the audit
These are similar robustness edges, but I would keep them secondary unless you want one larger tracking issue.
Parsed::to_naive_date week-date overflow
Parsed has public fields, and to_naive_date returns a parse result. A manually constructed extreme week value can overflow while resolving a week date:
// src/format/parsed.rs:1187
let ordinal = first_week_start + (week as i32 - 1) * 7 + weekday;Reproducer:
use chrono::format::Parsed;
use chrono::Weekday;
#[test]
#[should_panic]
fn parsed_week_date_can_overflow() {
let parsed = Parsed {
year: Some(2024),
week_from_sun: Some(i32::MAX as u32),
weekday: Some(Weekday::Sun),
..Parsed::new()
};
let _ = parsed.to_naive_date();
}Expected: return a parse error such as OUT_OF_RANGE / IMPOSSIBLE, or document that manually inconsistent Parsed values can panic.
Rounding near timestamp limits
duration_trunc / duration_round_up return Result, but they call original +/- TimeDelta::nanoseconds(...) after calculating a delta. Near timestamp boundaries, that addition/subtraction can panic through the underlying Add / Sub implementation instead of returning RoundingError.
Examples from the audit:
use chrono::{DurationRound, NaiveDate, TimeDelta};
#[test]
#[should_panic]
fn duration_trunc_can_panic_near_min_timestamp() {
let dt = NaiveDate::from_ymd_opt(1677, 9, 21)
.unwrap()
.and_hms_nano_opt(0, 12, 43, 145_224_192)
.unwrap();
let _ = dt.duration_trunc(TimeDelta::nanoseconds(i64::MAX));
}use chrono::{DateTime, DurationRound, TimeDelta};
#[test]
#[should_panic]
fn duration_round_up_can_panic_near_max_timestamp() {
let dt = DateTime::from_timestamp_nanos(i64::MAX);
let _ = dt.duration_round_up(TimeDelta::nanoseconds(2));
}Expected: return RoundingError for out-of-range rounded results, or document the remaining panic path.
Mostly documented or deprecated panic paths I would not put in the main report
The audit also found many panics that are real, but mostly already match existing docs or deprecated API behavior:
DateTime<Tz> + TimeDelta,DateTime<Tz> - TimeDelta, and assign variants: docs say the operation panics if the resulting date is out of range and point tochecked_add_signed/checked_sub_signed.Date<Tz>::and_hms*,succ,pred,FixedOffset::east/west,TimeZone::ymd/yo/isoywd/timestamp/timestamp_millis: deprecated APIs with documentedPanicssections and_optreplacements.NaiveDate/NaiveDateTimeoperator overflow paths: same basic overflow behavior as the date/datetime operator impls.TimeDeltaoperator impls like+,-,*,/, unary-: many of these are operator contract panics; the stronger point is thechecked_*behavior above.Datelike/Timelikedefault methods such asquarter,num_days_in_month,num_days_from_ce, andnum_seconds_from_midnight: generated cases use custom trait implementations that return invalid month/year/hour values. Those are better framed as trait-invariant docs, not primary bugs.- Formatting / local-time conversion panics that depend on manually inconsistent datetime/timezone states should be reviewed case-by-case, but I would keep them out of the first issue unless maintainers ask for the full audit list.
Activity
djc commentedon Jun 23, 2026