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).
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.
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.
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.
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
>>