Skip to content

Fix negative subsec for time_point - #3261

Merged
vitaut merged 6 commits into
fmtlib:masterfrom
ShawnZhong:subsec
Jan 11, 2023
Merged

vitaut merged 6 commits into
fmtlib:masterfrom
ShawnZhong:subsec

Conversation

@ShawnZhong

@ShawnZhong ShawnZhong commented Jan 4, 2023

Copy link
Copy Markdown
Contributor

Fix #3117

The following piece of code (https://godbolt.org/z/4qr5389ns) used to print 59.000 00.750 00.500 00.250 00.000 00.250 00.500 00.750 01.000, which is wrong. After this PR, it now correctly prints 59.000 59.250 59.500 59.750 00.000 00.250 00.500 00.750 01.000

const auto epoch = std::chrono::time_point<std::chrono::system_clock,
                                           std::chrono::milliseconds>();
const auto d = std::chrono::milliseconds(250);
for (int i = -4; i <= 4; i++) {
  fmt::print("{:%S} ", epoch + i * d);
}

Comment thread include/fmt/chrono.h Outdated
val -= std::chrono::seconds(1);
}


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: extra newline

Comment thread test/chrono-test.cc Outdated
Comment on lines +888 to +891
EXPECT_EQ("59.000", fmt::format("{:%S}", epoch - 4 * d));
EXPECT_EQ("59.250", fmt::format("{:%S}", epoch - 3 * d));
EXPECT_EQ("59.500", fmt::format("{:%S}", epoch - 2 * d));
EXPECT_EQ("59.750", fmt::format("{:%S}", epoch - 1 * d));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we need all four of these checks because they are effectively testing the same thing. One is probably enough.

Comment thread test/chrono-test.cc Outdated
Comment on lines +893 to +896
EXPECT_EQ("00.250", fmt::format("{:%S}", epoch + 1 * d));
EXPECT_EQ("00.500", fmt::format("{:%S}", epoch + 2 * d));
EXPECT_EQ("00.750", fmt::format("{:%S}", epoch + 3 * d));
EXPECT_EQ("01.000", fmt::format("{:%S}", epoch + 4 * d));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

@vitaut vitaut left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! Overall looks good, just some minor comments inline.

@vitaut
vitaut merged commit 676c2a1 into fmtlib:master Jan 11, 2023
@vitaut

vitaut commented Jan 11, 2023

Copy link
Copy Markdown
Contributor

Thank you!

@ShawnZhong
ShawnZhong deleted the subsec branch January 13, 2023 11:09
@vitaut

vitaut commented Jan 15, 2023

Copy link
Copy Markdown
Contributor

This change triggers a ubsan error on the following case:

#include <fmt/chrono.h>

int main() {
  auto d = std::chrono::system_clock::duration(-9223372036854767584);
  fmt::print("{:}", std::chrono::system_clock::time_point(d));
}
.../chrono:1111:111: runtime error: signed integer overflow: -9223372036854767584 - 1000000 cannot be represented in type 'long long'

Stack trace:

  * frame #0: 0x000000010037ea20 libclang_rt.ubsan_osx_dynamic.dylib`__ubsan_handle_sub_overflow
    frame #1: 0x000000010000b5bd a.out`std::__1::chrono::duration<long long, std::__1::ratio<1l, 1000000l>>::operator-=(this=0x00007ff7bfefd190, __d=0x00007ff7bfefd140) at chrono:1111:111
    frame #2: 0x000000010000a835 a.out`std::__1::chrono::time_point<std::__1::chrono::system_clock, std::__1::chrono::duration<long long, std::__1::ratio<1l, 1000000l>>>::operator-=(this=0x00007ff7bfefd190, __d=0x00007ff7bfefd140) at chrono:1390:111
    frame #3: 0x0000000100005b18 a.out`decltype(fp0.out()) fmt::v9::formatter<std::__1::chrono::time_point<std::__1::chrono::system_clock, std::__1::chrono::duration<long long, std::__1::ratio<1l, 1000000l>>>, char, void>::format<fmt::v9::basic_format_context<fmt::v9::appender, char>>(this=0x00007ff7bfefd1f0, val=time_point<std::__1::chrono::system_clock, std::__1::chrono::duration<long long, std::__1::ratio<1, 1000000> > > @ 0x00007ff7bfefd190, ctx=0x00007ff7bfefef40) const at chrono.h:2113:13
    frame #4: 0x0000000100005802 a.out`void fmt::v9::detail::value<fmt::v9::basic_format_context<fmt::v9::appender, char>>::format_custom_arg<std::__1::chrono::time_point<std::__1::chrono::system_clock, std::__1::chrono::duration<long long, std::__1::ratio<1l, 1000000l>>>, fmt::v9::formatter<std::__1::chrono::time_point<std::__1::chrono::system_clock, std::__1::chrono::duration<long long, std::__1::ratio<1l, 1000000l>>>, char, void>>(arg=0x00007ff7bfeff4c8, parse_ctx=0x00007ff7bfefef28, ctx=0x00007ff7bfefef40) at core.h:1332:22
    frame #5: 0x000000010006a839 a.out`fmt::v9::basic_format_arg<fmt::v9::basic_format_context<fmt::v9::appender, char>>::handle::format(this=0x00007ff7bfefd2e0, parse_ctx=0x00007ff7bfefef28, ctx=0x00007ff7bfefef40) const at core.h:1595:7
    frame #6: 0x0000000100079db8 a.out`fmt::v9::detail::custom_formatter<char>::operator()(this=0x00007ff7bfefdb70, h=handle @ 0x00007ff7bfefd2e0) const at format.h:3603:7
    frame #7: 0x000000010006f4ca a.out`void fmt::v9::detail::vformat_to<char>(fmt::v9::detail::buffer<char>&, fmt::v9::basic_string_view<char>, fmt::v9::detail::vformat_args<char>::type, fmt::v9::detail::locale_ref)::format_handler::on_format_specs(int, char const*, char const*) [inlined] decltype(fp(0)) fmt::v9::visit_format_arg<fmt::v9::detail::custom_formatter<char>, fmt::v9::basic_format_context<fmt::v9::appender, char>>(vis=0x00007ff7bfefdb70, arg=0x00007ff7bfefe2f0) at core.h:1659:12
    frame #8: 0x000000010006e35d a.out`void fmt::v9::detail::vformat_to<char>(fmt::v9::detail::buffer<char>&, fmt::v9::basic_string_view<char>, fmt::v9::detail::vformat_args<char>::type, fmt::v9::detail::locale_ref)::format_handler::on_format_specs(this=0x00007ff7bfefef28, id=0, begin="}", end="") at format.h:4205:9
    frame #9: 0x000000010006d9b7 a.out`char const* fmt::v9::detail::parse_replacement_field<char, void fmt::v9::detail::vformat_to<char>(fmt::v9::detail::buffer<char>&, fmt::v9::basic_string_view<char>, fmt::v9::detail::vformat_args<char>::type, fmt::v9::detail::locale_ref)::format_handler&>(begin=":}", end="", handler=0x00007ff7bfefef28) at core.h:2558:23
    frame #10: 0x000000010002e93c a.out`void fmt::v9::detail::vformat_to<char>(fmt::v9::detail::buffer<char>&, fmt::v9::basic_string_view<char>, fmt::v9::detail::vformat_args<char>::type, fmt::v9::detail::locale_ref) at core.h:2583:21
    frame #11: 0x000000010002e674 a.out`void fmt::v9::detail::vformat_to<char>(buf=0x00007ff7bfeff1e0, fmt=(data_ = "{:}", size_ = 3), args=fmt::v9::detail::vformat_args<char>::type @ 0x00007ff7bfefefb8, loc=(locale_ = 0x0000000000000000)) at format.h:4221:3
    frame #12: 0x000000010002f44c a.out`fmt::v9::vprint(f=0x00007ff858747c98, format_str=(data_ = "{:}", size_ = 3), args=fmt::v9::format_args @ 0x00007ff7bfeff1b0) at format-inl.h:1531:3
    frame #13: 0x000000010002f6af a.out`fmt::v9::vprint(format_str=(data_ = "{:}", size_ = 3), args=fmt::v9::format_args @ 0x00007ff7bfeff430) at format-inl.h:1547:3
    frame #14: 0x0000000100004325 a.out`main [inlined] void fmt::v9::print<std::__1::chrono::time_point<std::__1::chrono::system_clock, std::__1::chrono::duration<long long, std::__1::ratio<1l, 1000000l>>>>(fmt=fmt::v9::format_string<std::__1::chrono::time_point<std::__1::chrono::system_clock, std::__1::chrono::duration<long long, std::__1::ratio<1, 1000000> > > > @ 0x00007ff7bfeff578, args=0x00007ff7bfeff4c8) at core.h:2983:30
    frame #15: 0x000000010000406d a.out`main at test.cc:5:3

@ShawnZhong

ShawnZhong commented Jan 16, 2023

Copy link
Copy Markdown
Contributor Author

I attempted to resolve this ub by first converting the time_point to use a rep of seconds, and then subtracting 1 second from it. However, I encountered an ub when converting a time_point representing -9223372036855 seconds before epoch to a time_t (, which should contain the same value for almost all platforms).

#include <fmt/chrono.h>

int main() {
  auto val = -9223372036854767584 / 1'000'000 - 1;  // -9223372036855
  auto tp =
      std::chrono::time_point<std::chrono::system_clock, std::chrono::seconds>(
          std::chrono::seconds(val));
  auto time = std::chrono::system_clock::to_time_t(tp);
  fmt::print("{} {}", val, time);  
  // expected: -9223372036855 -9223372036855
  // actual:   -9223372036855 9223372036854
}

The message from the sanitizer is as follows:

/opt/homebrew/opt/llvm/bin/../include/c++/v1/__chrono/duration.h:97:59: runtime error: signed integer overflow: -9223372036855 * 1000000 cannot be represented in type 'long long'
    #0 0x1007d12a8 in std::__1::chrono::__duration_cast<std::__1::chrono::duration<long long, std::__1::ratio<1l, 1l> >, std::__1::chrono::duration<long long, std::__1::ratio<1l, 1000000l> >, std::__1::ratio<1000000l, 1l>, false, true>::operator()[abi:v15006](std::__1::chrono::duration<long long, std::__1::ratio<1l, 1l> > const&) const duration.h:97
    #1 0x1007d1174 in std::__1::enable_if<__is_duration<std::__1::chrono::duration<long long, std::__1::ratio<1l, 1000000l> > >::value, std::__1::chrono::duration<long long, std::__1::ratio<1l, 1000000l> > >::type std::__1::chrono::duration_cast[abi:v15006]<std::__1::chrono::duration<long long, std::__1::ratio<1l, 1000000l> >, long long, std::__1::ratio<1l, 1l> >(std::__1::chrono::duration<long long, std::__1::ratio<1l, 1l> > const&) duration.h:124
    #2 0x1007d10e8 in std::__1::chrono::duration<long long, std::__1::ratio<1l, 1000000l> >::duration[abi:v15006]<long long, std::__1::ratio<1l, 1l> >(std::__1::chrono::duration<long long, std::__1::ratio<1l, 1l> > const&, std::__1::enable_if<(__no_overflow<std::__1::ratio<1l, 1l>, std::__1::ratio<1l, 1000000l> >::value) && ((std::__1::integral_constant<bool, false>::value) || (((__no_overflow<std::__1::ratio<1l, 1l>, std::__1::ratio<1l, 1000000l> >::type::den) == (1)) && (!(treat_as_floating_point<long long>::value)))), void>::type*) duration.h:271
    #3 0x1007d1020 in std::__1::chrono::duration<long long, std::__1::ratio<1l, 1000000l> >::duration[abi:v15006]<long long, std::__1::ratio<1l, 1l> >(std::__1::chrono::duration<long long, std::__1::ratio<1l, 1l> > const&, std::__1::enable_if<(__no_overflow<std::__1::ratio<1l, 1l>, std::__1::ratio<1l, 1000000l> >::value) && ((std::__1::integral_constant<bool, false>::value) || (((__no_overflow<std::__1::ratio<1l, 1l>, std::__1::ratio<1l, 1000000l> >::type::den) == (1)) && (!(treat_as_floating_point<long long>::value)))), void>::type*) duration.h:271
    #4 0x1007d0f24 in std::__1::chrono::time_point<std::__1::chrono::system_clock, std::__1::chrono::duration<long long, std::__1::ratio<1l, 1000000l> > >::time_point[abi:v15006]<std::__1::chrono::duration<long long, std::__1::ratio<1l, 1l> > >(std::__1::chrono::time_point<std::__1::chrono::system_clock, std::__1::chrono::duration<long long, std::__1::ratio<1l, 1l> > > const&, std::__1::enable_if<is_convertible<std::__1::chrono::duration<long long, std::__1::ratio<1l, 1l> >, std::__1::chrono::duration<long long, std::__1::ratio<1l, 1000000l> > >::value, void>::type*) time_point.h:55
    #5 0x1007d0d54 in std::__1::chrono::time_point<std::__1::chrono::system_clock, std::__1::chrono::duration<long long, std::__1::ratio<1l, 1000000l> > >::time_point[abi:v15006]<std::__1::chrono::duration<long long, std::__1::ratio<1l, 1l> > >(std::__1::chrono::time_point<std::__1::chrono::system_clock, std::__1::chrono::duration<long long, std::__1::ratio<1l, 1l> > > const&, std::__1::enable_if<is_convertible<std::__1::chrono::duration<long long, std::__1::ratio<1l, 1l> >, std::__1::chrono::duration<long long, std::__1::ratio<1l, 1000000l> > >::value, void>::type*) time_point.h:55
    #6 0x1007d0670 in main main.cpp:9
    #7 0x1a6487e4c  (<unknown module>)
    #8 0x8a3c7ffffffffffc  (<unknown module>)

SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /opt/homebrew/opt/llvm/bin/../include/c++/v1/__chrono/duration.h:97:59

I don't know why the library would multiply the seconds by 1,000,000 and then convert it to time_t (which again divides by 1,000,000).

@ShawnZhong

Copy link
Copy Markdown
Contributor Author

An alternative solution is to subtract 1 from the resulting time, but that would assume the representation of time_t.

#include <fmt/chrono.h>

int main() {
  auto val = -9223372036854;  // -9223372036854767584 / 1'000'000
  auto tp =
      std::chrono::time_point<std::chrono::system_clock, std::chrono::seconds>(
          std::chrono::seconds(val));

  // This results in integer overflow since to_time_t does some multiplications.
  auto t1 = std::chrono::system_clock::to_time_t(tp - std::chrono::seconds(1));

  // This assumes time_t to represent seconds and is signed, which is not
  // specified by the standard, but true for almost all platforms.
  auto t2 = std::chrono::system_clock::to_time_t(tp) - 1;

  fmt::print("t1: {}, t2: {}\n", t1, t2);
  // t1: 9223372036854, t2: -9223372036855
}

@vitaut

vitaut commented Jan 17, 2023

Copy link
Copy Markdown
Contributor

I turned the UB into an error in 240b728. If you have a better idea, a PR would be welcome.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Formatting std::chrono::time_point with double Rep or before Epoch

2 participants