On Thu, May 7, 2026 at 9:58 AM Jonathan Wakely <[email protected]> wrote:
> I think we might as well just move the whole function into the library, > and only have one place that keeps the list of leap seconds and the expiry. > My suggestion is to simplify your patch (no need to read expires), and just add one parameter. So I am not sure we are adding much complexity, compared to before. > > That way we don't need to care about what's in the header. > This prevents the risk of someone slapping constexpr, doing one test that is before hardcoded expiry, and proposing to do so in standard :) > > On Thu, 7 May 2026, 07:15 Tomasz Kaminski, <[email protected]> wrote: > >> >> >> 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 >>>> >> >>>> >>>>
