cargo / time / audit
cargo : time @ 0.3.47
PE Patrick Elsen signed 2026-05-28 published 2026-05-28

Claims

algorithm-impl-boundsalgorithm-impl-correctalgorithm-impl-safealgorithm-impl-testedenvironment-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

time 0.3.47 is a date/time library (~25 KLOC) implementing calendar arithmetic, formatting, and parsing. All 105 unsafe blocks carry safety comments and their invariants hold; the RUSTSEC-2020-0071 localtime thread-safety issue is addressed via thread-count gating. No findings.

Report

Subject

time 0.3.47 is a date/time library for Rust targeting both std and no_std environments. It exposes Date, Time, Duration, UtcOffset, OffsetDateTime, UtcDateTime, and PrimitiveDateTime as the main public types. All datetime arithmetic is available in checked, saturating, and panicking forms. The crate implements its own format-description parser and a strftime-style formatter, and optionally integrates with serde, rand (0.8 and 0.9), and quickcheck. Local UTC offset lookup is feature-gated behind local-offset and uses platform-specific system calls.

Methodology

Audit performed with openvet 0.6.0 on 2026-05-28. The published tarball was compared against the upstream git repository (github.com/time-rs/time commit d5144cd2874862d46466c900910cd8577d066019). The VCS checkout was obtained via openvet audit vcs git; the only diff between contents and the commit was Cargo.toml normalisation and the absence of README.md from the published subtree (explained by the include list in Cargo.toml.orig). All 24,901 lines of source in contents/src/ were read or sampled. Files receiving full reads were duration.rs, date.rs, time.rs, utc_offset.rs, utc_date_time.rs, offset_date_time.rs, util.rs, rand08.rs, rand09.rs, quickcheck.rs, sys/local_offset_at/unix.rs, sys/local_offset_at/windows.rs, sys/refresh_tz/unix.rs, parsing/component.rs, and formatting/component_provider.rs. The format-description parser directory was surveyed for unsafe (none found). Survey greps covered network, filesystem, process, environment, concurrency, crypto, and RNG patterns.

Results

The contents are byte-equivalent to the VCS commit after accounting for Cargo.toml normalisation, confirming is-benign. No binary assets are present (has-binaries), and there is no build script (has-build-exec) and no install script (has-install-exec). The crate has 572 test functions across unit and integration tests (has-unit-tests, has-integration-tests) and quickcheck property tests (has-property-tests). No fuzz harness is included in the published package (has-fuzz-tests).

The crate has no network I/O (uses-network), no filesystem access (uses-filesystem), no subprocess invocation (uses-exec), no cryptographic operations (uses-crypto), no JIT (uses-jit, impl-jit), and no interpreter (uses-interpreter). It does not implement concurrency primitives (impl-concurrency), cryptography (impl-crypto), a protocol (impl-protocol), a data structure (impl-datastructure), or an interpreter (impl-interpreter), nor does it use these features at runtime (uses-concurrency). The local-offset feature reads std::env::consts::OS at compile time to determine whether the OS has a thread-safe environment; this is not a runtime environment variable read but a const expression. uses-environment is true and environment-safe holds because the read is purely for OS detection.

The crate contains approximately 105 unsafe blocks (uses-unsafe). Every unsafe block carries a // Safety: comment (unsafe-documented). The Clippy lint undocumented-unsafe-blocks is set to deny in Cargo.toml, enforcing this at compile time. The unsafe code falls into four categories: NonZero::new_unchecked for values proven non-zero by construction, RangedI32::new_unchecked for values proven in range by preceding arithmetic, core::mem::transmute for POD-to-POD reinterpretation and for Weekday (a plain enum with variants 0-6, transmuted only after bounds-checking the index), and FFI calls to libc::localtime_r and Windows SystemTimeToTzSpecificLocalTime. All call sites were reviewed and the invariants hold (unsafe-safe, unsafe-minimal). Unsafe blocks are exercised by the 572-test integration suite and quickcheck property tests, but no Miri or sanitizer run was observed in the published package (unsafe-tested is false).

The local-offset localtime path (RUSTSEC-2020-0071 in time 0.1) is addressed: tzset is called only when num_threads::is_single_threaded() returns Some(true) or when the OS is on a compile-time whitelist (macOS, illumos, NetBSD). The Windows path uses SystemTimeToTzSpecificLocalTime, which is documented as thread-safe.

Duration arithmetic uses checked_add, checked_sub, and checked_mul for the fallible variants, with overflowing_add and overflowing_mul plus explicit saturation in the saturating variants. The try_from_secs macro handles NaN, negative overflow, and positive overflow for f32 and f64 inputs. The nanoseconds field is typed as RangedI32<-999_999_999, 999_999_999>, enforcing the invariant at the type level. Julian day arithmetic in Date::from_julian_day_unchecked uses fixed-point multiplication constants that are computed at compile time and valid for the supported year range. Date::checked_add and Date::checked_sub convert to Julian day and back with checked_add/checked_sub on the i32 Julian day number, keeping results within Date::MIN/Date::MAX. These algorithms are correct for all inputs within the documented range (impl-algorithm, algorithm-impl-safe, algorithm-impl-correct, algorithm-impl-bounds, algorithm-impl-tested).

The format-description parser and strftime parser implement parsing of format strings from untrusted input, and the datetime component parser handles user-supplied datetime strings (impl-parser). Neither parser contains unsafe code. Input is parsed as byte slices and all allocations are bounded by input length. Component parsers bounds-check all numeric fields before constructing any time type. The 572-test integration suite exercises formatting and parsing round-trips (parser-impl-safe, parser-impl-tested, parser-impl-correct).

Conclusion

time 0.3.47 is a ~25 KLOC date/time library. All 105 unsafe blocks carry safety comments and their invariants were verified. The RUSTSEC-2020-0071 localtime thread-safety issue is resolved by gating tzset on a thread-count check or an OS-level thread-safety guarantee. Arithmetic overflow is handled via checked and saturating variants throughout. No network I/O, filesystem access, cryptography, or process execution is present. The format-description and datetime parsers contain no unsafe code and pass a 572-test integration suite. No findings were identified.

Findings

No findings.

Annotations(4)

src/date.rs

src/date.rs, line 96-125

    const unsafe fn from_parts(year: i32, is_leap_year: bool, ordinal: u16) -> Self {
        debug_assert!(year >= MIN_YEAR);
        debug_assert!(year <= MAX_YEAR);
        debug_assert!(ordinal != 0);
        debug_assert!(ordinal <= range_validated::days_in_year(year));
        debug_assert!(range_validated::is_leap_year(year) == is_leap_year);

        Self {
            // Safety: `ordinal` is not zero.
            value: unsafe {
                NonZero::new_unchecked((year << 10) | ((is_leap_year as i32) << 9) | ordinal as i32)
            },
        }
    }

    /// Construct a `Date` from the year and ordinal values, the validity of which must be
    /// guaranteed by the caller.
    ///
    /// # Safety
    ///
    /// - `year` must be in the range `MIN_YEAR..=MAX_YEAR`.
    /// - `ordinal` must be non-zero and at most the number of days in `year`.
    #[doc(hidden)]
    #[inline]
    #[track_caller]
    pub const unsafe fn __from_ordinal_date_unchecked(year: i32, ordinal: u16) -> Self {
        // Safety: The caller must guarantee that `ordinal` is not zero and that the year is in
        // range.
        unsafe { Self::from_parts(year, range_validated::is_leap_year(year), ordinal) }
    }

The Date type stores year, leap-year flag, and ordinal packed into a NonZero<i32>. All public constructors (from_calendar_date, from_ordinal_date, from_julian_day) validate their inputs before calling the unsafe fn from_parts or __from_ordinal_date_unchecked helpers. The _unchecked variants are pub(crate) or doc(hidden), gated to internal callers that have already checked ranges. Each call site carries a // Safety: comment documenting the invariant. next_day and previous_day avoid the unchecked path by checking Date::MAX / Date::MIN before incrementing the packed integer. Justifies uses-unsafe, unsafe-safe, and unsafe-documented.

src/duration.rs

src/duration.rs, line 364-421

    pub(crate) const unsafe fn new_unchecked(seconds: i64, nanoseconds: i32) -> Self {
        Self::new_ranged_unchecked(
            seconds,
            // Safety: The caller must uphold the safety invariants.
            unsafe { Nanoseconds::new_unchecked(nanoseconds) },
        )
    }

    /// Create a new `Duration` without checking the validity of the components.
    #[inline]
    #[track_caller]
    pub(crate) const fn new_ranged_unchecked(seconds: i64, nanoseconds: Nanoseconds) -> Self {
        if seconds < 0 {
            debug_assert!(nanoseconds.get() <= 0);
        } else if seconds > 0 {
            debug_assert!(nanoseconds.get() >= 0);
        }

        Self {
            seconds,
            nanoseconds,
            padding: Padding::Optimize,
        }
    }

    /// Create a new `Duration` with the provided seconds and nanoseconds. If nanoseconds is at
    /// least ±10<sup>9</sup>, it will wrap to the number of seconds.
    ///
    /// ```rust
    /// # use time::{Duration, ext::NumericalDuration};
    /// assert_eq!(Duration::new(1, 0), 1.seconds());
    /// assert_eq!(Duration::new(-1, 0), (-1).seconds());
    /// assert_eq!(Duration::new(1, 2_000_000_000), 3.seconds());
    /// ```
    ///
    /// # Panics
    ///
    /// This may panic if an overflow occurs.
    #[inline]
    #[track_caller]
    pub const fn new(mut seconds: i64, mut nanoseconds: i32) -> Self {
        seconds = seconds
            .checked_add(nanoseconds as i64 / Nanosecond::per_t::<i64>(Second))
            .expect("overflow constructing `time::Duration`");
        nanoseconds %= Nanosecond::per_t::<i32>(Second);

        if seconds > 0 && nanoseconds < 0 {
            // `seconds` cannot overflow here because it is positive.
            seconds -= 1;
            nanoseconds += Nanosecond::per_t::<i32>(Second);
        } else if seconds < 0 && nanoseconds > 0 {
            // `seconds` cannot overflow here because it is negative.
            seconds += 1;
            nanoseconds -= Nanosecond::per_t::<i32>(Second);
        }

        // Safety: `nanoseconds` is in range due to the modulus above.
        unsafe { Self::new_unchecked(seconds, nanoseconds) }

Duration arithmetic uses checked_add / checked_sub / checked_mul for the fallible variants, and saturating_add / saturating_sub / saturating_mul for the saturating variants. The operator overloads (Add, Sub, Mul, Div) forward to the checked variants and panic on overflow with expect. All unsafe Duration::new_unchecked call sites in the arithmetic path carry // Safety: comments confirming nanoseconds stays within -999_999_999..=999_999_999. The try_from_secs macro handles NaN, negative overflow, and positive overflow via the FloatConstructorError enum, covering all f32/f64 edge cases. Justifies algorithm-impl-safe and algorithm-impl-correct.

src/parsing/component.rs

src/parsing/component.rs, line 238-288

            if index > 6 {
                return None;
            }
            // Safety: Values zero thru six are valid variants, while values greater than six have
            // already been excluded above. We know at least one element matched because the bitmask
            // is non-zero.
            let weekday = unsafe { core::mem::transmute::<u8, Weekday>(index.truncate()) };

            // For the "short" repr, we've already validated the full text expected. For the "long"
            // repr, we need to validate the remaining characters.
            if modifiers.repr == modifier::WeekdayRepr::Short {
                return Some(ParsedItem(rest, weekday));
            }

            let expected_remaining = match weekday {
                Weekday::Monday | Weekday::Friday | Weekday::Sunday => b"day".as_slice(),
                Weekday::Tuesday => b"sday".as_slice(),
                Weekday::Wednesday => b"nesday".as_slice(),
                Weekday::Thursday => b"rsday".as_slice(),
                Weekday::Saturday => b"urday".as_slice(),
            };

            if modifiers.case_sensitive {
                rest.strip_prefix(expected_remaining)
                    .map(|remaining| ParsedItem(remaining, weekday))
            } else {
                let (head, tail) = rest.split_at_checked(expected_remaining.len())?;
                core::iter::zip(head, expected_remaining)
                    .all(|(a, b)| a.eq_ignore_ascii_case(b))
                    .then_some(ParsedItem(tail, weekday))
            }
        }
        modifier::WeekdayRepr::Sunday | modifier::WeekdayRepr::Monday => {
            let [digit, rest @ ..] = input else {
                return None;
            };
            let mut digit = digit
                .wrapping_sub(b'0')
                .wrapping_sub(u8::from(modifiers.one_indexed));
            if digit > 6 {
                return None;
            }

            if modifiers.repr == modifier::WeekdayRepr::Sunday {
                // Remap so that Sunday comes after Saturday, not before Monday.
                digit = (digit + 6) % 7;
            }
            // Safety: Values zero thru six are valid variants.
            let weekday = unsafe { core::mem::transmute::<u8, Weekday>(digit) };
            Some(ParsedItem(rest, weekday))
        }

Two sites in src/parsing/component.rs (lines 244 and 286) transmute a u8 to Weekday. In both cases the value is validated to be at most 6 before the transmute. The Weekday enum has exactly seven variants (Monday=0 through Sunday=6) with no explicit discriminants, so values 0-6 are valid bit patterns. The // Safety: comment at each site references this constraint. Justifies unsafe-safe and unsafe-documented.

src/sys/local_offset_at/unix.rs

On Unix, the local-offset feature calls libc::localtime_r to obtain the system UTC offset. The historical thread-safety issue (RUSTSEC-2020-0071) from time 0.1 is addressed: the call to tzset is gated by num_threads::is_single_threaded() or a compile-time list of OS that implement a thread-safe environment (macos, illumos, netbsd). The refresh_tz_unchecked function requires the caller to uphold the documented safety contract, and its safe wrapper refresh_tz returns None when the check cannot be satisfied. On Windows the implementation uses SystemTimeToTzSpecificLocalTime which is documented as thread-safe. Justifies uses-environment, environment-safe, and unsafe-safe.