From 7dc456208eb8944bca17a7fe9d71ff990e401958 Mon Sep 17 00:00:00 2001 From: Aizal Khan Date: Wed, 14 Oct 2026 22:14:28 +0530 Subject: [PATCH] fix unchecked float to integral conversion in detail::duration_cast --- include/fmt/chrono.h | 35 ++++++++++++++++++++++++++--------- test/chrono-test.cc | 9 +++++++++ 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/include/fmt/chrono.h b/include/fmt/chrono.h index 19571164..40a465a2 100644 --- a/include/fmt/chrono.h +++ b/include/fmt/chrono.h @@ -448,12 +448,6 @@ template <> struct locale_writer { } }; -template -using is_similar_arithmetic_type = - bool_constant<(std::is_integral::value && std::is_integral::value) || - (std::is_floating_point::value && - std::is_floating_point::value)>; - FMT_NORETURN inline void throw_duration_error() { FMT_THROW(format_error("cannot format duration")); } @@ -510,14 +504,37 @@ auto duration_cast(std::chrono::duration from) -> To { #endif } +// Mixed integer <-> float casts are not supported by safe_duration_cast. +// An integer to float conversion is always defined. template ::value)> + FMT_ENABLE_IF(std::is_integral::value&& + std::is_floating_point::value)> auto duration_cast(std::chrono::duration from) -> To { - // Mixed integer <-> float cast is not supported by safe duration_cast. return std::chrono::duration_cast(from); } +// Cast a floating-point duration to an integral one with a range check since +// converting an out-of-range or NaN value to an integer is undefined behavior. +template ::value&& + std::is_integral::value)> +auto duration_cast(std::chrono::duration from) -> To { +#if FMT_SAFE_DURATION_CAST + using to_rep = typename To::rep; + auto count = detail::duration_cast< + std::chrono::duration>(from) + .count(); + // max + 1 is a power of 2 so the cast to FromRep is exact even when max + // itself is rounded up by the conversion; the comparison is false for NaN. + auto upper = static_cast(max_value()) + 1; + auto lower = static_cast((std::numeric_limits::min)()); + if (!(count >= lower && count < upper)) throw_duration_error(); + return To(static_cast(count)); +#else + return std::chrono::duration_cast(from); +#endif +} + template auto to_time_t(sys_time time_point) -> std::time_t { // Cannot use std::chrono::system_clock::to_time_t since this would first diff --git a/test/chrono-test.cc b/test/chrono-test.cc index 818ec42f..fe9b4fa8 100644 --- a/test/chrono-test.cc +++ b/test/chrono-test.cc @@ -995,6 +995,15 @@ TEST(chrono_test, out_of_range) { // A floating-point day count that doesn't fit in int. auto fd = std::chrono::duration(1e300); EXPECT_THROW((void)fmt::format("{:%j}", fd), fmt::format_error); + // A floating-point time point whose seconds don't fit in time_t. + auto ftp = sys_time>(fd); + EXPECT_THROW((void)fmt::format("{:%T}", ftp), fmt::format_error); + auto fnan = sys_time>( + std::chrono::duration(std::numeric_limits::quiet_NaN())); + EXPECT_THROW((void)fmt::format("{:%T}", fnan), fmt::format_error); + auto flocal = fmt::local_time>( + std::chrono::duration(-1e300)); + EXPECT_THROW((void)fmt::format("{:%F}", flocal), fmt::format_error); } TEST(chrono_test, calendar_padding) {