libstdc++: Fix chrono::current_zone() for three component names [PR122567]

Message ID 20260109173604.2600880-1-jwakely@redhat.com
State New
Headers
Series libstdc++: Fix chrono::current_zone() for three component names [PR122567] |

Commit Message

Jonathan Wakely Jan. 9, 2026, 5:35 p.m. UTC
  chrono::current_zone() fails if /etc/localtime is a symlink to a zone
with three components, like "America/Indiana/Indianapolis", because we
only try to find "Indianapolis" and "Indiana/Indianapolis" but neither
of those exists.

We need to try up to three components to handle all valid cases, such as
"UTC", "America/Indianapolis", and "America/Indiana/Indianapolis".

Since two components is the most common case, we could consider trying
last[-2]/last[-1] first, then if that doesn't match trying only the last
component and the last three components, but this patch doesn't do that.

libstdc++-v3/ChangeLog:

	PR libstdc++/122567
	* src/c++20/tzdb.cc (tzdb::current_zone): Loop over trailing
	components of /etc/localtime path for up to three components.
---

Tested x86_64-linux.

There's no new testcase for this because it requires root access to
change /etc/localtime. I've verified it locally.

 libstdc++-v3/src/c++20/tzdb.cc | 26 ++++++++++++++++++--------
 1 file changed, 18 insertions(+), 8 deletions(-)
  

Comments

Tomasz Kaminski Jan. 9, 2026, 5:58 p.m. UTC | #1
On Fri, Jan 9, 2026 at 6:37 PM Jonathan Wakely <jwakely@redhat.com> wrote:

> chrono::current_zone() fails if /etc/localtime is a symlink to a zone
> with three components, like "America/Indiana/Indianapolis", because we
> only try to find "Indianapolis" and "Indiana/Indianapolis" but neither
> of those exists.
>
> We need to try up to three components to handle all valid cases, such as
> "UTC", "America/Indianapolis", and "America/Indiana/Indianapolis".
>
> Since two components is the most common case, we could consider trying
> last[-2]/last[-1] first, then if that doesn't match trying only the last
> component and the last three components, but this patch doesn't do that.
>
> libstdc++-v3/ChangeLog:
>
>         PR libstdc++/122567
>         * src/c++20/tzdb.cc (tzdb::current_zone): Loop over trailing
>         components of /etc/localtime path for up to three components.
> ---
>
> Tested x86_64-linux.
>
> There's no new testcase for this because it requires root access to
> change /etc/localtime. I've verified it locally.
>
>  libstdc++-v3/src/c++20/tzdb.cc | 26 ++++++++++++++++++--------
>  1 file changed, 18 insertions(+), 8 deletions(-)
>
> diff --git a/libstdc++-v3/src/c++20/tzdb.cc
> b/libstdc++-v3/src/c++20/tzdb.cc
> index 53441880ae6e..44ba3ea890f2 100644
> --- a/libstdc++-v3/src/c++20/tzdb.cc
> +++ b/libstdc++-v3/src/c++20/tzdb.cc
> @@ -1896,14 +1896,24 @@ namespace std::chrono
>         auto first = path.begin(), last = path.end();
>         if (std::distance(first, last) > 2)
>           {
> -           --last;
> -           string name = last->string();
> -           if (auto tz = do_locate_zone(this->zones, this->links, name))
> -             return tz;
> -           --last;
> -           name = last->string() + '/' + name;
> -           if (auto tz = do_locate_zone(this->zones, this->links, name))
> -             return tz;
> +           string name, part;
> +           // Check trailing components of the path against known zone
> names.
> +           // Valid zones can have one, two, or three components, e.g.
> +           // "UTC", "Europe/London", "America/Indiana/Indianapolis"
> +           for (int i = 0; i < 3 && last != first; ++i)
>
Is there any real benefit of hardcoding 3 components here, instead of using
while (last != first), so we would handle any longer names?

> +             {
> +               --last;
> +               part = last->string();
> +               if (name.empty())
> +                 name = std::move(part);
> +               else
> +                 {
> +                   part += '/';
> +                   name = std::move(part) + std::move(name);
> +                 }
> +               if (auto tz = do_locate_zone(this->zones, this->links,
> name))
> +                 return tz;
> +             }
>           }
>        }
>  #endif
> --
> 2.52.0
>
>
  
Tomasz Kaminski Jan. 9, 2026, 6:13 p.m. UTC | #2
On Fri, Jan 9, 2026 at 6:58 PM Tomasz Kaminski <tkaminsk@redhat.com> wrote:

>
>
> On Fri, Jan 9, 2026 at 6:37 PM Jonathan Wakely <jwakely@redhat.com> wrote:
>
>> chrono::current_zone() fails if /etc/localtime is a symlink to a zone
>> with three components, like "America/Indiana/Indianapolis", because we
>> only try to find "Indianapolis" and "Indiana/Indianapolis" but neither
>> of those exists.
>>
>> We need to try up to three components to handle all valid cases, such as
>> "UTC", "America/Indianapolis", and "America/Indiana/Indianapolis".
>>
>> Since two components is the most common case, we could consider trying
>> last[-2]/last[-1] first, then if that doesn't match trying only the last
>> component and the last three components, but this patch doesn't do that.
>>
>> libstdc++-v3/ChangeLog:
>>
>>         PR libstdc++/122567
>>         * src/c++20/tzdb.cc (tzdb::current_zone): Loop over trailing
>>         components of /etc/localtime path for up to three components.
>> ---
>>
>> Tested x86_64-linux.
>>
>> There's no new testcase for this because it requires root access to
>> change /etc/localtime. I've verified it locally.
>>
>>  libstdc++-v3/src/c++20/tzdb.cc | 26 ++++++++++++++++++--------
>>  1 file changed, 18 insertions(+), 8 deletions(-)
>>
>> diff --git a/libstdc++-v3/src/c++20/tzdb.cc
>> b/libstdc++-v3/src/c++20/tzdb.cc
>> index 53441880ae6e..44ba3ea890f2 100644
>> --- a/libstdc++-v3/src/c++20/tzdb.cc
>> +++ b/libstdc++-v3/src/c++20/tzdb.cc
>> @@ -1896,14 +1896,24 @@ namespace std::chrono
>>         auto first = path.begin(), last = path.end();
>>         if (std::distance(first, last) > 2)
>>           {
>> -           --last;
>> -           string name = last->string();
>> -           if (auto tz = do_locate_zone(this->zones, this->links, name))
>> -             return tz;
>> -           --last;
>> -           name = last->string() + '/' + name;
>> -           if (auto tz = do_locate_zone(this->zones, this->links, name))
>> -             return tz;
>> +           string name, part;
>> +           // Check trailing components of the path against known zone
>> names.
>> +           // Valid zones can have one, two, or three components, e.g.
>> +           // "UTC", "Europe/London", "America/Indiana/Indianapolis"
>> +           for (int i = 0; i < 3 && last != first; ++i)
>>
> Is there any real benefit of hardcoding 3 components here, instead of using
> while (last != first), so we would handle any longer names?
>
If we have more than 4 components in future, then such a new timezone
database
can be downloaded by the older version of the library. And we will checking
only more
than tree components, only if the current zone is not something that we
could match.

>
>
+             {
>> +               --last;
>> +               part = last->string();
>> +               if (name.empty())
>> +                 name = std::move(part);
>> +               else
>> +                 {
>> +                   part += '/';
>> +                   name = std::move(part) + std::move(name);
>> +                 }
>> +               if (auto tz = do_locate_zone(this->zones, this->links,
>> name))
>> +                 return tz;
>> +             }
>>           }
>>        }
>>  #endif
>> --
>> 2.52.0
>>
>>
  
Jonathan Wakely Jan. 9, 2026, 6:32 p.m. UTC | #3
On Fri, 9 Jan 2026 at 18:13, Tomasz Kaminski <tkaminsk@redhat.com> wrote:
>
>
>
> On Fri, Jan 9, 2026 at 6:58 PM Tomasz Kaminski <tkaminsk@redhat.com> wrote:
>>
>>
>>
>> On Fri, Jan 9, 2026 at 6:37 PM Jonathan Wakely <jwakely@redhat.com> wrote:
>>>
>>> chrono::current_zone() fails if /etc/localtime is a symlink to a zone
>>> with three components, like "America/Indiana/Indianapolis", because we
>>> only try to find "Indianapolis" and "Indiana/Indianapolis" but neither
>>> of those exists.
>>>
>>> We need to try up to three components to handle all valid cases, such as
>>> "UTC", "America/Indianapolis", and "America/Indiana/Indianapolis".
>>>
>>> Since two components is the most common case, we could consider trying
>>> last[-2]/last[-1] first, then if that doesn't match trying only the last
>>> component and the last three components, but this patch doesn't do that.
>>>
>>> libstdc++-v3/ChangeLog:
>>>
>>>         PR libstdc++/122567
>>>         * src/c++20/tzdb.cc (tzdb::current_zone): Loop over trailing
>>>         components of /etc/localtime path for up to three components.
>>> ---
>>>
>>> Tested x86_64-linux.
>>>
>>> There's no new testcase for this because it requires root access to
>>> change /etc/localtime. I've verified it locally.
>>>
>>>  libstdc++-v3/src/c++20/tzdb.cc | 26 ++++++++++++++++++--------
>>>  1 file changed, 18 insertions(+), 8 deletions(-)
>>>
>>> diff --git a/libstdc++-v3/src/c++20/tzdb.cc b/libstdc++-v3/src/c++20/tzdb.cc
>>> index 53441880ae6e..44ba3ea890f2 100644
>>> --- a/libstdc++-v3/src/c++20/tzdb.cc
>>> +++ b/libstdc++-v3/src/c++20/tzdb.cc
>>> @@ -1896,14 +1896,24 @@ namespace std::chrono
>>>         auto first = path.begin(), last = path.end();
>>>         if (std::distance(first, last) > 2)
>>>           {
>>> -           --last;
>>> -           string name = last->string();
>>> -           if (auto tz = do_locate_zone(this->zones, this->links, name))
>>> -             return tz;
>>> -           --last;
>>> -           name = last->string() + '/' + name;
>>> -           if (auto tz = do_locate_zone(this->zones, this->links, name))
>>> -             return tz;
>>> +           string name, part;
>>> +           // Check trailing components of the path against known zone names.
>>> +           // Valid zones can have one, two, or three components, e.g.
>>> +           // "UTC", "Europe/London", "America/Indiana/Indianapolis"
>>> +           for (int i = 0; i < 3 && last != first; ++i)
>>
>> Is there any real benefit of hardcoding 3 components here, instead of using
>> while (last != first), so we would handle any longer names?

It will slow down the unlikely case, where the symlink refers to a
zone that we don't recognize as a valid name, but that's probably not
a major concern.

The docs say that most zones should have a name of the form
AREA/LOCATION so more than two components is already rare:
https://data.iana.org/time-zones/theory.html#naming
Four or more components isn't actually forbidden, but it seems very unlikely.

>
> If we have more than 4 components in future, then such a new timezone database
> can be downloaded by the older version of the library. And we will checking only more
> than tree components, only if the current zone is not something that we could match.
>>
>>
>>>
>>> +             {
>>> +               --last;
>>> +               part = last->string();
>>> +               if (name.empty())
>>> +                 name = std::move(part);
>>> +               else
>>> +                 {
>>> +                   part += '/';
>>> +                   name = std::move(part) + std::move(name);
>>> +                 }
>>> +               if (auto tz = do_locate_zone(this->zones, this->links, name))
>>> +                 return tz;
>>> +             }
>>>           }
>>>        }
>>>  #endif
>>> --
>>> 2.52.0
>>>
  
Jonathan Wakely Jan. 9, 2026, 6:37 p.m. UTC | #4
On Fri, 9 Jan 2026 at 18:32, Jonathan Wakely <jwakely@redhat.com> wrote:
>
> On Fri, 9 Jan 2026 at 18:13, Tomasz Kaminski <tkaminsk@redhat.com> wrote:
> >
> >
> >
> > On Fri, Jan 9, 2026 at 6:58 PM Tomasz Kaminski <tkaminsk@redhat.com> wrote:
> >>
> >>
> >>
> >> On Fri, Jan 9, 2026 at 6:37 PM Jonathan Wakely <jwakely@redhat.com> wrote:
> >>>
> >>> chrono::current_zone() fails if /etc/localtime is a symlink to a zone
> >>> with three components, like "America/Indiana/Indianapolis", because we
> >>> only try to find "Indianapolis" and "Indiana/Indianapolis" but neither
> >>> of those exists.
> >>>
> >>> We need to try up to three components to handle all valid cases, such as
> >>> "UTC", "America/Indianapolis", and "America/Indiana/Indianapolis".
> >>>
> >>> Since two components is the most common case, we could consider trying
> >>> last[-2]/last[-1] first, then if that doesn't match trying only the last
> >>> component and the last three components, but this patch doesn't do that.
> >>>
> >>> libstdc++-v3/ChangeLog:
> >>>
> >>>         PR libstdc++/122567
> >>>         * src/c++20/tzdb.cc (tzdb::current_zone): Loop over trailing
> >>>         components of /etc/localtime path for up to three components.
> >>> ---
> >>>
> >>> Tested x86_64-linux.
> >>>
> >>> There's no new testcase for this because it requires root access to
> >>> change /etc/localtime. I've verified it locally.
> >>>
> >>>  libstdc++-v3/src/c++20/tzdb.cc | 26 ++++++++++++++++++--------
> >>>  1 file changed, 18 insertions(+), 8 deletions(-)
> >>>
> >>> diff --git a/libstdc++-v3/src/c++20/tzdb.cc b/libstdc++-v3/src/c++20/tzdb.cc
> >>> index 53441880ae6e..44ba3ea890f2 100644
> >>> --- a/libstdc++-v3/src/c++20/tzdb.cc
> >>> +++ b/libstdc++-v3/src/c++20/tzdb.cc
> >>> @@ -1896,14 +1896,24 @@ namespace std::chrono
> >>>         auto first = path.begin(), last = path.end();
> >>>         if (std::distance(first, last) > 2)
> >>>           {
> >>> -           --last;
> >>> -           string name = last->string();
> >>> -           if (auto tz = do_locate_zone(this->zones, this->links, name))
> >>> -             return tz;
> >>> -           --last;
> >>> -           name = last->string() + '/' + name;
> >>> -           if (auto tz = do_locate_zone(this->zones, this->links, name))
> >>> -             return tz;
> >>> +           string name, part;
> >>> +           // Check trailing components of the path against known zone names.
> >>> +           // Valid zones can have one, two, or three components, e.g.
> >>> +           // "UTC", "Europe/London", "America/Indiana/Indianapolis"
> >>> +           for (int i = 0; i < 3 && last != first; ++i)
> >>
> >> Is there any real benefit of hardcoding 3 components here, instead of using
> >> while (last != first), so we would handle any longer names?
>
> It will slow down the unlikely case, where the symlink refers to a
> zone that we don't recognize as a valid name, but that's probably not
> a major concern.
>
> The docs say that most zones should have a name of the form
> AREA/LOCATION so more than two components is already rare:
> https://data.iana.org/time-zones/theory.html#naming
> Four or more components isn't actually forbidden, but it seems very unlikely.

I suppose it's possible that somebody could use an arbitrary number of
filename components in a custom version of the database, replacing or
overriding tzdata.zi with their own file and then setting
/etc/localtime to refer to one of their own zones.


>
> >
> > If we have more than 4 components in future, then such a new timezone database
> > can be downloaded by the older version of the library. And we will checking only more
> > than tree components, only if the current zone is not something that we could match.
> >>
> >>
> >>>
> >>> +             {
> >>> +               --last;
> >>> +               part = last->string();
> >>> +               if (name.empty())
> >>> +                 name = std::move(part);
> >>> +               else
> >>> +                 {
> >>> +                   part += '/';
> >>> +                   name = std::move(part) + std::move(name);
> >>> +                 }
> >>> +               if (auto tz = do_locate_zone(this->zones, this->links, name))
> >>> +                 return tz;
> >>> +             }
> >>>           }
> >>>        }
> >>>  #endif
> >>> --
> >>> 2.52.0
> >>>
  

Patch

diff --git a/libstdc++-v3/src/c++20/tzdb.cc b/libstdc++-v3/src/c++20/tzdb.cc
index 53441880ae6e..44ba3ea890f2 100644
--- a/libstdc++-v3/src/c++20/tzdb.cc
+++ b/libstdc++-v3/src/c++20/tzdb.cc
@@ -1896,14 +1896,24 @@  namespace std::chrono
 	auto first = path.begin(), last = path.end();
 	if (std::distance(first, last) > 2)
 	  {
-	    --last;
-	    string name = last->string();
-	    if (auto tz = do_locate_zone(this->zones, this->links, name))
-	      return tz;
-	    --last;
-	    name = last->string() + '/' + name;
-	    if (auto tz = do_locate_zone(this->zones, this->links, name))
-	      return tz;
+	    string name, part;
+	    // Check trailing components of the path against known zone names.
+	    // Valid zones can have one, two, or three components, e.g.
+	    // "UTC", "Europe/London", "America/Indiana/Indianapolis"
+	    for (int i = 0; i < 3 && last != first; ++i)
+	      {
+		--last;
+		part = last->string();
+		if (name.empty())
+		  name = std::move(part);
+		else
+		  {
+		    part += '/';
+		    name = std::move(part) + std::move(name);
+		  }
+		if (auto tz = do_locate_zone(this->zones, this->links, name))
+		  return tz;
+	      }
 	  }
       }
 #endif