Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix negative subsec for time_point #3261

Merged
merged 6 commits into from
Jan 11, 2023
Merged

Fix negative subsec for time_point #3261

merged 6 commits into from
Jan 11, 2023

Conversation

ShawnZhong
Copy link
Contributor

@ShawnZhong ShawnZhong commented Jan 4, 2023

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);
}

val -= std::chrono::seconds(1);
}


Copy link
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 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
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 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
Contributor

Choose a reason for hiding this comment

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

Same here.

Copy link
Contributor

@vitaut vitaut left a comment

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
Copy link
Contributor

vitaut commented Jan 11, 2023

Thank you!

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

vitaut commented Jan 15, 2023

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
Copy link
Contributor Author

ShawnZhong commented Jan 16, 2023

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
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
Copy link
Contributor

vitaut commented Jan 17, 2023

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