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

Reply via email to