On Thu, May 7, 2026 at 7:58 AM Tomasz Kaminski <[email protected]> wrote:
> > > On Wed, May 6, 2026 at 7:28 PM Jonathan Wakely <[email protected]> wrote: > >> On Wed, 6 May 2026 at 15:47, Tomasz Kaminski <[email protected]> wrote: >> > >> > >> > >> > On Wed, May 6, 2026 at 4:18 PM Jonathan Wakely <[email protected]> >> wrote: >> >> >> >> This change allows the hardcoded list of leap seconds in <chrono> to be >> >> used even when the program is executing after the hardcoded expiry date >> >> in that header. If the OS-provided leapseconds file has a later expiry >> >> date (or contains new leap seconds added after the one in 2017) then >> the >> >> new __detail::__leap_seconds_expiry() function will return that new >> >> dynamically-obtained expiry date. The __detail::__get_leap_second_info >> >> function in the header can check that new expiry date instead of >> relying >> >> only on the hardcoded one. >> >> >> >> This change means that in the worst case we now make two calls into the >> >> library (one to get the dynamic expiry date and then possibly another >> >> one to get the actual list of new leap seconds). Previously we just >> make >> >> one, to get the list. The change seems worthwhile, because it means >> that >> >> in more cases we don't need to increment+decrement the reference count >> >> on a tzdb object and use its leapseconds vector, we can just use the >> >> hardcoded array. >> >> >> >> The new expiry date is stored in a global variable, rather than being >> >> per-tzdb object, but that seems fine because we only ever expect that >> >> expiry date to move forwards in time, not to move forwards and >> backwards >> >> as new tzdb objects are loaded by chrono::reload_tzdb(). Even if a new >> >> list of leap seconds is loaded, we still expect an expiry date that was >> >> loaded previously to be valid. >> > >> > I was wondering about it in connection to the test that overrides the >> zone directory, >> > and may move the date around. But for those already impacted by the >> fact that >> > any date with an expiry date before one that we hardcode is ignored. >> However, >> > for that reason, I would ensure we do not move the date backward; >> suggestion below. >> > (In also could move backward if reading #experies fails). >> > I think I would like to propose a bit of different model. As different GCC > versions will > hardcode different number of leap seconds in the header, we should provide > function looking like > that: > // Returns expiry time for nth leap second. > __expiry_of_leap_second(size_t nth); > This function should return 1s prior the expiry of nth leap second, if > that is know to the > tzdb (as you do), but if current time zone database didn't load leap > second data with it, > we could return infinity (expiry_type max). This is totally fine, as > reload_tzdb call that > will load nth leap second will cause a new value to be returned (this is > checked on each all). > Then in the header, we do something like: f (__ss > __expires && __ss > > __expiry_of_leap_second(std::size(leaps))) > > Now in the source file, we will have: > const sys_seconds static_leaps[] = ....; // leaps second know when compiled > const atomic<expiry_time> next_leap(expiry_max); // There is no next leap > second data at this point > > Implementation of __expiry_of_leap_second will be something like: > if (nth < sizeof(static_leaps)) > return __leaps[_nth]; // this leap second data was compiled into > binary > else if (nth == sizeof(static_leaps)) > return __next_leap.load(relaxed); // time of next leap second not > know by binary > else > // This is caused by linking code compiled with new headers against > // older version of leapstdc++, we know that nth leap will be after > __next_leap, > // so return currently know value of __next_leap. > return __next_leap.load(relaxed); > The above could be simply 0, as we know the expiry time in that header is later date that __next_heap (because it included that leap second). Returning __next_leap however guaratees tht if we didn't load the new data from disk (and it is infinite), we will use header information that have more data that library file. > > The loading leaps seconds file does not need to check expiry, just do > something like: > if (leaps.size() > static_leaps.size()) // we loaded the next leap > second > expiry_time = leaps[static_leaps.size()].time_since_epoch().count() > - 1; > else > expiry_time = expiry_max; // There is no new leap second data. > > And simply update expiry_time to this value. > > > >> There are three problem cases I was worried about. >> >> The happy path is where we read a good expiry time from the >> leapseconds file, so we want to store it in the global variable. >> That's the easy case. >> >> If the file doesn't contain any #expires line so we should not update >> the global. >> > Counterintuitively, I do not think the #experies line is important when > loading dynamically updates the time, so my suggestion solves that. > > >> >> The second problem case is where it contains an #expires line, but we >> fail to parse the value (maybe the format changed in a future version >> of the file?). This should not happen, but is possible for a custom >> leapseconds file provided by the user or their sysadmin, and we should >> not update the global in this case. >> > We do not look at #expires so this is also solved. > >> >> The third problem is where a bad leapseconds file is installed and it >> has a bad #expires line, with a date too far in the future (maybe >> somebody makes a typo in a custom file and it has too many digits). In >> this case, the user/sysadmin might install a fixed leapseconds file, >> then trigger the application to call reload_tzdb() to read the fixed >> file. In this case, we would want to update the global even if it >> means moving backwards in time. >> > This would be equivalent to someone accidentally putting a wrong file > with 28th leap second put very close to the date (between header expiry > and now), that would cause wrong converison, as next_leap will be udpate. > But with my suggestion for reload, putting a correct version will always > fix that, > as we override next_leap: > * if 28th leap was removed, we will update timout to maximium, until it > will show up > * if value was changed, we will also update it to it > > And in my approach the expiry time always corresponds to tzdb latest. > >> >> In all cases, if a 28th leap second has been defined, that becomes the >> new expiry. >> >> > >> >> >> >> >> >> libstdc++-v3/ChangeLog: >> >> >> >> PR libstdc++/123165 >> >> * acinclude.m4 (libtool_VERSION): Bump version. >> >> * config/abi/pre/gnu.ver (GLIBCXX_3.4.36): Add new symbol >> >> version and export new symbol. >> >> * configure: Regenerate. >> >> * include/std/chrono (__detail::__leap_seconds_expiry): >> >> Declare new function. >> >> (__detail::__get_leap_second_info): Use new function. >> >> * src/c++20/tzdb.cc (__detail::__leap_seconds_expiry): Define. >> >> (tzdb_list::_Node::_S_read_leap_seconds): Read 'expires' line >> >> from leapseconds file and optionally update global cache. >> >> * testsuite/std/time/tzdb/leap_seconds.cc: Add expires line to >> >> replacement leapseconds file. >> >> * testsuite/util/testsuite_abi.cc: Update known_versions and >> >> latestp. >> >> --- >> >> >> >> Tested x86_64-linux. >> >> >> >> >> >> libstdc++-v3/acinclude.m4 | 2 +- >> >> libstdc++-v3/config/abi/pre/gnu.ver | 7 ++ >> >> libstdc++-v3/configure | 2 +- >> >> libstdc++-v3/include/std/chrono | 9 +- >> >> libstdc++-v3/src/c++20/tzdb.cc | 88 ++++++++++++++++--- >> >> .../testsuite/std/time/tzdb/leap_seconds.cc | 1 + >> >> libstdc++-v3/testsuite/util/testsuite_abi.cc | 3 +- >> >> 7 files changed, 96 insertions(+), 16 deletions(-) >> >> >> >> diff --git a/libstdc++-v3/acinclude.m4 b/libstdc++-v3/acinclude.m4 >> >> index 8dc9e17b214c..3a4b11a98a28 100644 >> >> --- a/libstdc++-v3/acinclude.m4 >> >> +++ b/libstdc++-v3/acinclude.m4 >> >> @@ -4085,7 +4085,7 @@ changequote([,])dnl >> >> fi >> >> >> >> # For libtool versioning info, format is CURRENT:REVISION:AGE >> >> -libtool_VERSION=6:35:0 >> >> +libtool_VERSION=6:36:0 >> >> >> >> # Everything parsed; figure out what files and settings to use. >> >> case $enable_symvers in >> >> diff --git a/libstdc++-v3/config/abi/pre/gnu.ver >> b/libstdc++-v3/config/abi/pre/gnu.ver >> >> index bd4da6418295..35aaf89984d1 100644 >> >> --- a/libstdc++-v3/config/abi/pre/gnu.ver >> >> +++ b/libstdc++-v3/config/abi/pre/gnu.ver >> >> @@ -2623,6 +2623,13 @@ GLIBCXX_3.4.35 { >> >> >> >> } GLIBCXX_3.4.34; >> >> >> >> +# GCC 17.1.0 >> >> +GLIBCXX_3.4.36 { >> >> + >> >> + _ZNSt6chrono8__detail21__leap_seconds_expiryEv; >> >> + >> >> +} GLIBCXX_3.4.35; >> >> + >> >> # Symbols in the support library (libsupc++) have their own tag. >> >> CXXABI_1.3 { >> >> >> >> diff --git a/libstdc++-v3/configure b/libstdc++-v3/configure >> >> index 6713e4504b1c..013c388b9c2f 100755 >> >> --- a/libstdc++-v3/configure >> >> +++ b/libstdc++-v3/configure >> >> @@ -51418,7 +51418,7 @@ $as_echo "$as_me: WARNING: === Symbol >> versioning will be disabled." >&2;} >> >> fi >> >> >> >> # For libtool versioning info, format is CURRENT:REVISION:AGE >> >> -libtool_VERSION=6:35:0 >> >> +libtool_VERSION=6:36:0 >> >> >> >> # Everything parsed; figure out what files and settings to use. >> >> case $enable_symvers in >> >> diff --git a/libstdc++-v3/include/std/chrono >> b/libstdc++-v3/include/std/chrono >> >> index 674f867dcdc7..228293f12bef 100644 >> >> --- a/libstdc++-v3/include/std/chrono >> >> +++ b/libstdc++-v3/include/std/chrono >> >> @@ -3217,6 +3217,11 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION >> >> >> >> namespace __detail >> >> { >> >> + // The list below is known to be valid until (at least) this date. >> >> + // This value is defined in the library (possibly to a newer >> value than >> >> + // the hardcoded value below) and can change at runtime. >> >> + sys_seconds __leap_seconds_expiry(); >> >> + >> >> inline leap_second_info >> >> __get_leap_second_info(sys_seconds __ss, bool __is_utc) >> >> { >> >> @@ -3252,12 +3257,12 @@ namespace __detail >> >> 1435708800, // 1 Jul 2015 >> >> 1483228800, // 1 Jan 2017 >> >> }; >> >> +#if _GLIBCXX_USE_CXX11_ABI || ! _GLIBCXX_USE_DUAL_ABI // use >> chrono::tzdb >> >> // The list above is known to be valid until (at least) this >> date >> >> // and only contains positive leap seconds. >> >> constexpr sys_seconds __expires(1798416000s); // 2026-12-28 >> 00:00:00 U >> > >> > >> >> >> >> -#if _GLIBCXX_USE_CXX11_ABI || ! _GLIBCXX_USE_DUAL_ABI >> >> - if (__ss > __expires) >> >> + if (__ss > __expires && __ss > __leap_seconds_expiry()) >> >> { >> >> // Use updated leap_seconds from tzdb. >> >> size_t __n = std::size(__leaps); >> >> diff --git a/libstdc++-v3/src/c++20/tzdb.cc >> b/libstdc++-v3/src/c++20/tzdb.cc >> >> index b0fbfc46a6d3..ba0020814ba8 100644 >> >> --- a/libstdc++-v3/src/c++20/tzdb.cc >> >> +++ b/libstdc++-v3/src/c++20/tzdb.cc >> >> @@ -1251,6 +1251,40 @@ namespace std::chrono >> >> } >> >> #endif // TZDB_DISABLED >> >> >> >> +namespace >> >> +{ >> >> +#if ATOMIC_LONG_LOCK_FREE == 2 >> >> + using expiry_type = unsigned long; >> >> +#else >> >> + using expiry_type = unsigned; >> >> +#endif >> >> + // When GCC 16.1 was released with stable C++20 chrono support (in >> 2026), >> >> + // the last leap second in the list was the one in 2017. If another >> leap >> >> + // second is introduced in future, objects compiled by GCC 16.1 >> will not >> >> + // contain that leap second in the hardcoded list in <chrono>. >> >> + // This expiry time must be less than that first post-2017 leap >> second, >> >> + // so that old copies of __get_leap_second_info will use >> tzdb::leap_seconds >> >> + // which will contain the post-2017 leap seconds. >> >> + // If no new leap second is introduced, then this expiry time can >> just be >> >> + // updated to the 'expires' value read from the leapseconds file. >> >> + // tzdata 2026a leapseconds list expires at 2026-12-28 00:00:00 UTC >> >> + constinit std::atomic<expiry_type> leap_seconds_expiry{1798416000u}; >> > >> > Could you extract the __expires from >> tzdb_list::_Node::_S_read_leap_seconds() >> > (in this file) as the global (but TU-local) variables, and use the >> __expires as initializer here, >> > so we do not need to update two prices. This will not add any symbols >> to the library, as we are in source file. >> > (This is why I am not suggesting doing that in header, as we will need >> to make the data inline). >> > >> > >> >> >> >> +} >> >> + >> >> + namespace __detail >> >> + { >> >> + // Called by chrono::__detail::__get_leap_second_info in <chrono>. >> >> + // The value returned by this function determines whether the >> hardcoded >> >> + // list in __get_leap_second_info is used, or if the >> tzdb::leap_seconds >> >> + // vector is used, which might require parsing and constructing a >> tzdb. >> >> + sys_seconds >> >> + __leap_seconds_expiry() >> >> + { >> >> + auto val = leap_seconds_expiry.load(memory_order::relaxed); >> >> + return sys_seconds(seconds(val)); >> >> + } >> >> + } >> >> + >> >> // Return leap_second values, and a bool indicating whether the >> values are >> >> // current (true), or potentially out of date (false). >> >> pair<vector<leap_second>, bool> >> >> @@ -1289,13 +1323,8 @@ namespace std::chrono >> >> (leap_second)1483228800, // 1 Jan 2017 >> >> }; >> >> >> >> -#if 0 >> >> - // This optimization isn't valid if the file has additional leap >> seconds >> >> - // defined since the library was compiled, but the system clock >> has been >> >> - // set to a time before the hardcoded expiration date. >> >> - if (system_clock::now() < expires) >> >> - return {std::move(leaps), true}; >> >> -#endif >> >> + sys_seconds new_expires = expires; >> >> + bool read_new_leaps = true; >> >> >> >> #ifndef TZDB_DISABLED >> >> if (ifstream ls{zoneinfo_file(leaps_file)}) >> >> @@ -1308,7 +1337,16 @@ namespace std::chrono >> >> // Leap YEAR MONTH DAY HH:MM:SS CORR R/S >> >> >> >> if (!s.starts_with("Leap")) >> >> - continue; >> >> + { >> >> + if (s.starts_with("#expires ")) >> >> + __try { >> >> + auto e = >> sys_seconds(seconds(std::stoll(s.substr(9)))); >> >> + if (e > new_expires) >> >> + new_expires = e; >> >> + } __catch (const std::exception&) { /* ignore */ } >> > >> > Note that if reading the expires line fails, new_expires will be moved >> > to build default (__expires), i.e. backward from already been >> successfully loaded >> > tzdb. >> >> Yes, that's wrong, thanks. >> >> >> >> >> + continue; >> >> + } >> >> + >> >> istringstream li(std::move(s)); >> >> li.exceptions(ios::failbit); >> >> li.ignore(4); >> >> @@ -1339,12 +1377,40 @@ namespace std::chrono >> >> leaps.push_back(ls); >> >> } >> >> } >> >> - s = std::move(li).str(); // return storage to s >> >> + s = std::move(li).str(); // give allocated storage back to >> s >> >> } >> >> - return {std::move(leaps), true}; >> >> + >> >> + read_new_leaps = true; >> >> } >> >> #endif >> >> - return {std::move(leaps), false}; >> >> + >> >> + if (leaps.size() > 27) >> > >> > And replaces this wit std::size(__leaps), they are defined in this >> function. >> > No need for magic number. >> >> >> >> + { >> >> + // One or more new leap seconds have been introduced since >> 2026. >> >> + // See comment on __detail::__leap_seconds_expiry() above. >> >> + // Object files compiled by older versions of GCC may not have >> >> + // any leap seconds after 2017 in the hardcoded list in >> <chrono>, >> >> + // so the expiry time that the header code uses must be before >> the >> >> + // new leap seconds. >> >> + new_expires = leaps[27].date() - 1s; >> >> + } >> >> + >> >> + // Should we update the global expiry time? >> >> + const sys_seconds old_expires = __detail::__leap_seconds_expiry(); >> >> + >> >> + if (new_expires != old_expires) >> > >> > This should check if new_expires > old_expires to avoid moving backward. >> > case of tests or failures to load. >> >> >> >> + { >> >> + expiry_type old_exp = old_expires.time_since_epoch().count(); >> >> + expiry_type new_exp = new_expires.time_since_epoch().count(); >> >> + >> >> + // We don't care about this compare-exchange failing. If >> another >> >> + // thread updated the expiry time, just use that value instead. >> >> + leap_seconds_expiry.compare_exchange_strong(old_exp, new_exp, >> >> + >> memory_order::release, >> > >> > Why do you use release here? There is no memory writes (except atomic) >> > that we want to see in other threads. After loading the value we may >> continue >> > to use static data. >> >> We might also *not* use the static data, and use >> get_tzdb_list()->begin()->leapseconds. But that already imposes a >> memory barrier, and there are no loads of the std::atomic that use >> acquire ordering, to synchronize with this release. So I agree it can >> be relaxed here. >> >> >> >> >> >> >> + >> memory_order::relaxed); >> >> + } >> >> + >> >> + return {std::move(leaps), read_new_leaps}; >> >> } >> >> >> >> #ifndef TZDB_DISABLED >> >> diff --git a/libstdc++-v3/testsuite/std/time/tzdb/leap_seconds.cc >> b/libstdc++-v3/testsuite/std/time/tzdb/leap_seconds.cc >> >> index 5999635a89f0..24ef7d44858d 100644 >> >> --- a/libstdc++-v3/testsuite/std/time/tzdb/leap_seconds.cc >> >> +++ b/libstdc++-v3/testsuite/std/time/tzdb/leap_seconds.cc >> >> @@ -52,6 +52,7 @@ Leap 2016 Dec 31 23:59:60 + >> S >> >> # These are fake leap seconds for testing purposes: >> >> Leap 2093 Jun 30 23:59:59 - S >> >> Leap 2093 Dec 31 23:59:60 + S >> >> +#expires 3991680000 (2096-06-28 00:00:00 UTC) >> >> )"; >> >> >> >> const auto& db = std::chrono::get_tzdb(); >> >> diff --git a/libstdc++-v3/testsuite/util/testsuite_abi.cc >> b/libstdc++-v3/testsuite/util/testsuite_abi.cc >> >> index 8fb38355cadd..4e80c5f184a6 100644 >> >> --- a/libstdc++-v3/testsuite/util/testsuite_abi.cc >> >> +++ b/libstdc++-v3/testsuite/util/testsuite_abi.cc >> >> @@ -217,6 +217,7 @@ check_version(symbol& test, bool added) >> >> known_versions.push_back("GLIBCXX_3.4.33"); >> >> known_versions.push_back("GLIBCXX_3.4.34"); >> >> known_versions.push_back("GLIBCXX_3.4.35"); >> >> + known_versions.push_back("GLIBCXX_3.4.36"); >> >> known_versions.push_back("GLIBCXX_LDBL_3.4.31"); >> >> known_versions.push_back("GLIBCXX_IEEE128_3.4.29"); >> >> known_versions.push_back("GLIBCXX_IEEE128_3.4.30"); >> >> @@ -260,7 +261,7 @@ check_version(symbol& test, bool added) >> >> test.version_status = symbol::incompatible; >> >> >> >> // Check that added symbols are added in the latest pre-release >> version. >> >> - bool latestp = (test.version_name == "GLIBCXX_3.4.35" >> >> + bool latestp = (test.version_name == "GLIBCXX_3.4.36" >> >> || test.version_name == "CXXABI_1.3.17" >> >> || test.version_name == "CXXABI_FLOAT128" >> >> || test.version_name == "CXXABI_TM_1"); >> >> -- >> >> 2.54.0 >> >> >> >>
