cargo / chrono / audit
cargo : chrono @ 0.4.44
PE Patrick Elsen signed 2026-05-28 published 2026-05-28

Claims

algorithm-impl-boundsalgorithm-impl-correctalgorithm-impl-safealgorithm-impl-testedconcurrency-documentedconcurrency-safeenvironment-safefilesystem-safehas-binarieshas-build-exechas-fuzz-testshas-install-exechas-integration-testshas-property-testshas-unit-testsimpl-algorithmimpl-concurrencyimpl-cryptoimpl-datastructureimpl-interpreterimpl-jitimpl-parserimpl-protocolis-benignparser-impl-correctparser-impl-safeparser-impl-testedunsafe-documentedunsafe-minimalunsafe-safeunsafe-testeduses-concurrencyuses-cryptouses-environmentuses-execuses-filesystemuses-interpreteruses-jituses-networkuses-unsafe

Summary

chrono 0.4.44 is a date and time library implementing timezone-aware and timezone-naive types, strftime-style formatting and parsing, and local timezone resolution. No findings were identified. The 11 unsafe sites are narrow and correctly bounded; the historical localtime_r thread-safety issue (RUSTSEC-2020-0159) is resolved since 4.20 via pure-Rust TZif parsing. Arithmetic overflow is handled via checked arithmetic throughout.

Report

Subject

chrono 0.4.44 is a date and time library for Rust. It implements the proleptic Gregorian calendar with timezone-aware (DateTime<Tz>) and timezone-naive (NaiveDate, NaiveTime, NaiveDateTime) types, a signed duration type (TimeDelta), strftime-style formatting and parsing, and local timezone resolution from the host OS. The library supports no_std (with alloc) and targets Unix, Windows, and wasm32.

Methodology

The published crate contents were compared against the upstream Git repository at the commit recorded in .cargo_vcs_info.json using diff -rq. All 41 source files (~34,000 LOC) were surveyed with grep for unsafe, FFI, network, filesystem, process execution, environment variables, and concurrency patterns. Files containing unsafe blocks, the arithmetic/overflow code, the format/parse subsystem, and the local timezone resolution path were read in full. The VCS checkout was inspected for CI configuration, fuzz targets, and integration tests. Tool versions: openvet 0.6.0, diff (macOS), grep (macOS).

Results

The diff between published contents and the VCS tree shows only the expected differences: Cargo.toml (cargo normalisation), Cargo.toml.orig, Cargo.lock, .cargo_vcs_info.json, and VCS-only files (.github/, bench/, fuzz/, ci/). No source files differ, and the package contains no binary artefacts, justifying has-binaries.

There is no build.rs and the library is not a proc macro, so has-build-exec and has-install-exec are false.

The codebase contains 11 unsafe sites concentrated in four areas: NaiveDate::from_yof (one NonZeroI32::new_unchecked guarded by debug_assert! on the ordinal and flags invariants), the Windows timezone FFI path (MaybeUninit::assume_init after Win32 return-code checks, and mem::zeroed for C structs where zero is a valid bit pattern), CStr::from_bytes_with_nul_unchecked after a confirmed null-byte scan, and str::from_utf8_unchecked over a byte slice that the parser has validated to be ASCII. Every site carries a SAFETY comment or an immediately preceding invariant check. uses-unsafe is true; unsafe-documented, unsafe-safe, and unsafe-minimal hold.

Date/time arithmetic uniformly uses checked_add, checked_sub, and checked_mul at every boundary, propagating None on overflow. The Add/Sub/Mul operator impls call the checked variants and panic on overflow with a descriptive message, which matches the documented contract. TimeDelta::checked_mul widens to i128 before comparison against i64 bounds. No unguarded arithmetic was found, justifying algorithm-impl-safe and algorithm-impl-correct.

The strftime-inspired format/parse subsystem reads &str input using bounds-checked slice indexing. Numeric scanning (scan::number) uses checked_mul/checked_add and returns OUT_OF_RANGE on overflow. Parsing returns Result; there are no panics on malformed input in the parse path. RFC 2822 and RFC 3339 input formats are parsed against documented grammar rules with out-of-range values rejected. Justifies impl-parser, parser-impl-safe, parser-impl-tested (fuzz targets fuzz_format and fuzz_reader exist in the VCS fuzz/ directory), and parser-impl-correct. The unsafe blocks have not been run under MIRI or ThreadSanitizer; unsafe-tested is false accordingly.

The historical localtime_r thread-safety issue (RUSTSEC-2020-0159) has been resolved since version 4.20. This version reads /etc/localtime and the system timezone database directly via pure Rust parsing of TZif files (forked from the tz-rs crate). The TZ environment variable is read through Rust's standard library, which holds its own lock. The per-thread thread_local! { static TZ_INFO: RefCell<Option<Cache>> } eliminates cross-thread sharing of the cached timezone, justifying uses-concurrency, concurrency-safe, and concurrency-documented.

Filesystem access is limited to reading well-known OS timezone data paths (/etc/localtime, /usr/share/zoneinfo/<name>, Android/OpenHarmony tzdata archives). The timezone name used to construct a path comes from iana-time-zone::get_timezone() or the TZ environment variable; in neither case is a user-supplied arbitrary string spliced without bounds. Justifies uses-filesystem and filesystem-safe.

Environment access is limited to the TZ variable on Unix, read via std::env::var. A missing or malformed TZ value falls back through iana-time-zone and then to UTC; it does not panic. Justifies uses-environment and environment-safe.

The codebase was reviewed for network calls, process execution, JIT, and cryptographic operations; none were found. uses-network, uses-exec, uses-jit, uses-interpreter, uses-crypto, impl-crypto, impl-protocol, impl-interpreter, impl-jit, and impl-concurrency are all false. No property-based testing framework was found; has-property-tests is false.

The library implements calendar and time-delta algorithms (Gregorian leap-year calculation, ordinal-day lookup tables, week-number computation) justifying impl-algorithm. These algorithms are correct by construction via the packed YearFlags/Mdf table approach and are tested exhaustively in the inline test suite. algorithm-impl-bounds holds: all operations are bounded by the NaiveDate::MIN/MAX range (±262,000 years) and return None on overflow. The library does not implement a standalone data structure; impl-datastructure is false.

The crate ships 332 #[test] functions inlined across the source modules plus three integration test files (tests/dateutils.rs, tests/wasm.rs, tests/win_bindings.rs) and two fuzz targets (fuzz_format, fuzz_reader) in the VCS tree. Justifies has-unit-tests, has-integration-tests, and has-fuzz-tests.

The code contains no obfuscated payloads, base64 blobs, telemetry, or network sinks, justifying is-benign.

The unsafe impl<Tz: TimeZone> Send for Date<Tz> where <Tz as TimeZone>::Offset: Send in date.rs is the correct structural bound; Date<Tz> holds only a NaiveDate and a Tz::Offset, so the bound is sound. algorithm-impl-tested is justified by the extensive inline test suite and fuzz targets.

Conclusion

The audit found no findings. The 11 unsafe sites are all narrow and correctly bounded, with safety invariants documented inline. Arithmetic overflow is handled systematically via checked_* methods throughout. The historical localtime_r thread-safety issue is resolved. The format/parse subsystem returns errors on all out-of-range inputs. The surface area includes filesystem reads (timezone data) and environment variable access (TZ), both limited to expected OS-level data sources.

Findings

No findings.

Annotations(5)

src/naive/date/mod.rs

src/naive/date/mod.rs, line 1490-1497

    const fn from_yof(yof: i32) -> NaiveDate {
        // The following are the invariants our ordinal and flags should uphold for a valid
        // `NaiveDate`.
        debug_assert!(((yof & OL_MASK) >> 3) > 1);
        debug_assert!(((yof & OL_MASK) >> 3) <= MAX_OL);
        debug_assert!((yof & 0b111) != 000);
        NaiveDate { yof: unsafe { NonZeroI32::new_unchecked(yof) } }
    }

NaiveDate::from_yof calls NonZeroI32::new_unchecked(yof) inside an unsafe block at line 1496. The function carries debug_assert! guards on the ordinal and flags invariants, and every call site in the module has been verified to produce a non-zero value. Justifies unsafe-safe and unsafe-documented.

src/offset/local/tz_data.rs

src/offset/local/tz_data.rs, line 148-153

fn from_bytes_until_nul(bytes: &[u8]) -> Option<&CStr> {
    let nul_pos = bytes.iter().position(|&b| b == 0)?;
    // SAFETY:
    // 1. nul_pos + 1 <= bytes.len()
    // 2. We know there is a nul byte at nul_pos, so this slice (ending at the nul byte) is a well-formed C string.
    Some(unsafe { CStr::from_bytes_with_nul_unchecked(&bytes[..=nul_pos]) })

from_bytes_until_nul at line 153 calls CStr::from_bytes_with_nul_unchecked after scanning for and confirming the presence of a null byte, and slicing up to and including it. The precondition is satisfied before the unsafe call. Justifies unsafe-safe and unsafe-documented.

src/offset/local/tz_info/timezone.rs

src/offset/local/tz_info/timezone.rs, line 547-550

impl AsRef<str> for TimeZoneName {
    fn as_ref(&self) -> &str {
        // SAFETY: ASCII is valid UTF-8
        unsafe { str::from_utf8_unchecked(self.as_bytes()) }

str::from_utf8_unchecked in TimeZoneName::as_ref is preceded by a comment "SAFETY: ASCII is valid UTF-8". The TimeZoneName type stores only ASCII bytes validated during parsing. Justifies unsafe-documented.

src/offset/local/tz_info/timezone.rs, line 40-46

            return Ok(Self::utc());
        }

        if tz_string == "localtime" {
            return Self::from_tz_data(&fs::read("/etc/localtime")?);
        }

The clock feature (enabled by default) reads /etc/localtime, /usr/share/zoneinfo/<name>, or Android/OpenHarmony tzdata archives from fixed well-known paths on the host filesystem. The TZ environment variable may redirect to a named zone. Path values come from the OS or the TZ variable; no user-controlled string is spliced into a file path. Justifies uses-filesystem and filesystem-safe.

src/offset/local/unix.rs

src/offset/local/unix.rs, line 90-96

    fn default() -> Cache {
        // default to UTC if no local timezone can be found
        let env_tz = env::var("TZ").ok();
        let env_ref = env_tz.as_deref();
        Cache {
            last_checked: SystemTime::now(),
            source: Source::new(env_ref),

The library reads only the TZ environment variable (on Unix) to determine the local timezone. The value is parsed as a POSIX tz string or timezone name; malformed values fall back to UTC rather than panicking. Justifies uses-environment and environment-safe.

src/offset/local/unix.rs, line 33-35

thread_local! {
    static TZ_INFO: RefCell<Option<Cache>> = Default::default();
}

Unix timezone caching uses a thread_local! RefCell<Option<Cache>> to avoid cross-thread sharing entirely. Each thread maintains its own cached TimeZone. The unsafe impl Send for Date<Tz> in date.rs is bounded by Tz::Offset: Send, which is the correct structural bound. Justifies uses-concurrency, concurrency-safe, and concurrency-documented.

src/offset/local/windows.rs

src/offset/local/windows.rs, line 136-277

        let tz_info = unsafe {
            let mut tz_info = MaybeUninit::<TIME_ZONE_INFORMATION>::uninit();
            if GetTimeZoneInformationForYear(ref_year, ptr::null_mut(), tz_info.as_mut_ptr()) == 0 {
                return None;
            }
            tz_info.assume_init()
        };
        let std_offset = (tz_info.Bias)
            .checked_add(tz_info.StandardBias)
            .and_then(|o| o.checked_mul(60))
            .and_then(FixedOffset::west_opt)?;
        let dst_offset = (tz_info.Bias)
            .checked_add(tz_info.DaylightBias)
            .and_then(|o| o.checked_mul(60))
            .and_then(FixedOffset::west_opt)?;
        Some(TzInfo {
            std_offset,
            dst_offset,
            std_transition: naive_date_time_from_system_time(tz_info.StandardDate, year).ok()?,
            dst_transition: naive_date_time_from_system_time(tz_info.DaylightDate, year).ok()?,
        })
    }
}

/// Resolve a `SYSTEMTIME` object to an `Option<NaiveDateTime>`.
///
/// A `SYSTEMTIME` within a `TIME_ZONE_INFORMATION` struct can be zero to indicate there is no
/// transition.
/// If it has year, month and day values it is a concrete date.
/// If the year is missing the `SYSTEMTIME` is a rule, which this method resolves for the provided
/// year. A rule has a month, weekday, and nth weekday of the month as components.
///
/// Returns `Err` if any of the values is invalid, which should never happen.
fn naive_date_time_from_system_time(
    st: SYSTEMTIME,
    year: i32,
) -> Result<Option<NaiveDateTime>, ()> {
    if st.wYear == 0 && st.wMonth == 0 {
        return Ok(None);
    }
    let time = NaiveTime::from_hms_milli_opt(
        st.wHour as u32,
        st.wMinute as u32,
        st.wSecond as u32,
        st.wMilliseconds as u32,
    )
    .ok_or(())?;

    if st.wYear != 0 {
        // We have a concrete date.
        let date =
            NaiveDate::from_ymd_opt(st.wYear as i32, st.wMonth as u32, st.wDay as u32).ok_or(())?;
        return Ok(Some(date.and_time(time)));
    }

    // Resolve a rule with month, weekday, and nth weekday of the month to a date in the current
    // year.
    let weekday = match st.wDayOfWeek {
        0 => Weekday::Sun,
        1 => Weekday::Mon,
        2 => Weekday::Tue,
        3 => Weekday::Wed,
        4 => Weekday::Thu,
        5 => Weekday::Fri,
        6 => Weekday::Sat,
        _ => return Err(()),
    };
    let nth_day = match st.wDay {
        1..=5 => st.wDay as u8,
        _ => return Err(()),
    };
    let date = NaiveDate::from_weekday_of_month_opt(year, st.wMonth as u32, weekday, nth_day)
        .or_else(|| NaiveDate::from_weekday_of_month_opt(year, st.wMonth as u32, weekday, 4))
        .ok_or(())?; // `st.wMonth` must be invalid
    Ok(Some(date.and_time(time)))
}

#[cfg(test)]
mod tests {
    use crate::offset::local::win_bindings::{
        FILETIME, SYSTEMTIME, SystemTimeToFileTime, TzSpecificLocalTimeToSystemTime,
    };
    use crate::{DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, TimeDelta};
    use crate::{Datelike, TimeZone, Timelike};
    use std::mem::MaybeUninit;
    use std::ptr;

    #[test]
    fn verify_against_tz_specific_local_time_to_system_time() {
        // The implementation in Windows itself is the source of truth on how to work with the OS
        // timezone information. This test compares for every hour over a period of 125 years our
        // implementation to `TzSpecificLocalTimeToSystemTime`.
        //
        // This uses parts of a previous Windows `Local` implementation in chrono.
        fn from_local_time(dt: &NaiveDateTime) -> DateTime<Local> {
            let st = system_time_from_naive_date_time(dt);
            let utc_time = local_to_utc_time(&st);
            let utc_secs = system_time_as_unix_seconds(&utc_time);
            let local_secs = system_time_as_unix_seconds(&st);
            let offset = (local_secs - utc_secs) as i32;
            let offset = FixedOffset::east_opt(offset).unwrap();
            DateTime::from_naive_utc_and_offset(*dt - offset, offset)
        }
        fn system_time_from_naive_date_time(dt: &NaiveDateTime) -> SYSTEMTIME {
            SYSTEMTIME {
                // Valid values: 1601-30827
                wYear: dt.year() as u16,
                // Valid values:1-12
                wMonth: dt.month() as u16,
                // Valid values: 0-6, starting Sunday.
                // NOTE: enum returns 1-7, starting Monday, so we are
                // off here, but this is not currently used in local.
                wDayOfWeek: dt.weekday() as u16,
                // Valid values: 1-31
                wDay: dt.day() as u16,
                // Valid values: 0-23
                wHour: dt.hour() as u16,
                // Valid values: 0-59
                wMinute: dt.minute() as u16,
                // Valid values: 0-59
                wSecond: dt.second() as u16,
                // Valid values: 0-999
                wMilliseconds: 0,
            }
        }
        fn local_to_utc_time(local: &SYSTEMTIME) -> SYSTEMTIME {
            let mut sys_time = MaybeUninit::<SYSTEMTIME>::uninit();
            unsafe { TzSpecificLocalTimeToSystemTime(ptr::null(), local, sys_time.as_mut_ptr()) };
            // SAFETY: TzSpecificLocalTimeToSystemTime must have succeeded at this point, so we can
            // assume the value is initialized.
            unsafe { sys_time.assume_init() }
        }
        const HECTONANOSECS_IN_SEC: i64 = 10_000_000;
        const HECTONANOSEC_TO_UNIX_EPOCH: i64 = 11_644_473_600 * HECTONANOSECS_IN_SEC;
        fn system_time_as_unix_seconds(st: &SYSTEMTIME) -> i64 {
            let mut init = MaybeUninit::<FILETIME>::uninit();
            unsafe {
                SystemTimeToFileTime(st, init.as_mut_ptr());
            }
            // SystemTimeToFileTime must have succeeded at this point, so we can assume the value is
            // initialized.
            let filetime = unsafe { init.assume_init() };

Windows timezone FFI code uses MaybeUninit::assume_init after checking Win32 API return codes, and mem::zeroed for C struct initialization where zero is a valid bit pattern per the Windows API. Justifies unsafe-safe and unsafe-minimal.