[PATCH] libstdc++: Use realpath for /etc/localtime symlink [PR125467]

Torbjorn SVENSSON torbjorn.svensson@foss.st.com
Tue Aug 11 11:50:54 GMT 2026


Hi,

I believe that this commit is introducing a regression for arm-none-eabi,
possibly other targets, that use semihosting (--specs=rdimon.spec).
The reason for the regression is that the symlink check is implemented
using calls to std::filesystem::canonical that calls _stat.

In the arm-none-eabi implementation in newlib, _stat is implemented like
this:

int __attribute__((weak))
_stat (const char *fname, struct stat *st)
{
   int fd, res;
   memset (st, 0, sizeof (* st));
   /* The best we can do is try to open the file readonly.  If it exists,
      then we can guess a few things about it.  */
   if ((fd = _open (fname, O_RDONLY)) == -1)
     return -1;
   st->st_mode |= S_IFREG | S_IREAD;
   res = _swistat (fd, st);
   /* Not interested in the error.  */
   _close (fd);
   return res;
}


In _swistat, the following statements are of interrest:

   /* Always assume a character device,
      with 1024 byte blocks. */
   st->st_mode |= S_IFCHR;
   st->st_blksize = 1024;

This, together with

   st->st_mode |= S_IFREG | S_IREAD;

from _stat will set the bits in a way that makes libstdc++ think that the
checked path is a symlink, even if the checked path actually a regular
directory.

Here is a link to the full semihosting implementation for arm-none-eabi:
https://sourceware.org/git?p=newlib-cygwin.git;a=blob;f=libgloss/arm/syscalls.c;hb=HEAD#l741


On 2026-06-30 19:45, Jonathan Wakely wrote:
> Although the systemd docs say that /etc/localtime should be a symlink to
> one of the zoneinfo files, some systems make it a symlink to another
> path, where that second path is a symlink to a zoneinfo file (e.g. if
> /etc is mounted read-only then /etc/localtime can be a symlink to
> another symlink on a writable disk, so that the system timezone can be
> altered by re-pointing the symlink on the writable disk).
> 
> In that case, using readlink would only tell us the location of the
> second symlink, not which zoneinfo file it points to. Therefore, we
> would not be able to extract a valid time zone name from the path, and
> chrono::current_zone() would fail.
> 
> To support multiple symlinks we could recursively keep resolving
> symlinks with readlink until we reach a path from which we can extract a
> zone name. Alternatively, we can just use realpath to resolve all
> symlinks to a physical file (which is what HowardHinnant/date does).
> This means we only need one system call and don't need the extra
> complexity of calling readlink in a loop.
> 
> The realpath system call also removes redunant slashes, so we can remove
> the code that did that manually.
> 
> The possible downsides of this approach that I'm aware of are:
> 
> - When /etc/localtime is a symlink to /invalid/Europe/London but that
>    file doesn't exist. With the previous implementation we would have
>    resolved that symlink to the zone "Europe/London" as long as that name
>    is known to the current chrono::tzdb object. With this change, we
>    won't get a valid zone name and current_zone() will fail. I'm not sure
>    how realistic this case is. It might be plausible if libstdc++ is
>    using the embedded static copy of tzdata.zi and there are no zoneinfo
>    files on disk at all. In that case the system might still use
>    /etc/localtime to name a zone, even though the symlink is dangling.
>    We could fall back to filesystem::weakly_canonical for this case, but
>    this patch leaves that for a future change, if it turns out to be
>    needed by any users.
> 
> - When /etc/localtime is a symlink to /usr/share/zoneinfo/Foo/Bar where
>    "Foo/Bar" is a valid zone in the chrono::tzdb object, but the Bar file
>    is another symlink to ./Baz where "Foo/Bar" is also a valid zone.
>    With the previous implementation current_zone() would have returned
>    the "Foo/Bar" zone. With this change it would return "Foo/Baz". I
>    don't think it's realistic to have two zones which are distinct zones
>    (not a Zone and a Link to it) but where one of them is defined on-disk
>    using a symlink to the other.
> 
> libstdc++-v3/ChangeLog:
> 
> 	PR libstdc++/125467
> 	* src/c++20/tzdb.cc (tzdb::current_zone): Use realpath to
> 	resolve the /etc/localtime symlink instead of readlink.
> ---
> 
> v2: Make the type of 'str' always std::string_view. Check str !=
> "/etc/localtime" so that we don't bother trying to extract a zone name
> from the symlink target if it isn't even a symlink.
> 
> Tested x86_64-linux.
> 
>   libstdc++-v3/src/c++20/tzdb.cc | 76 +++++++++++++---------------------
>   1 file changed, 28 insertions(+), 48 deletions(-)
> 
> diff --git a/libstdc++-v3/src/c++20/tzdb.cc b/libstdc++-v3/src/c++20/tzdb.cc
> index 9e601fc176f3..c658e0c9cd37 100644
> --- a/libstdc++-v3/src/c++20/tzdb.cc
> +++ b/libstdc++-v3/src/c++20/tzdb.cc
> @@ -41,8 +41,13 @@
>   # include <ext/concurrence.h> // __gnu_cxx::__mutex
>   #endif
>   
> -#if defined(_GLIBCXX_HAVE_READLINK) && defined(_GLIBCXX_HAVE_UNISTD_H)
> -# include <unistd.h>  // readlink
> +#ifdef _GLIBCXX_HAVE_UNISTD_H
> +# include <unistd.h> // _XOPEN_VERSION
> +#endif
> +#if defined _GLIBCXX_USE_REALPATH && _XOPEN_VERSION >= 700
> +# include <stdlib.h>   // malloc, free, realpath
> +#else
> +# include <filesystem> // filesystem::canonicalize
>   #endif
>   
>   #ifdef _AIX
> @@ -2098,58 +2103,33 @@ constinit tzdb_list::_Node::NumLeapSeconds tzdb_list::_Node::num_leap_seconds;
>       // to have a way to force a re-read.
>   
>   #if !defined(_AIX) && !defined(_GLIBCXX_HAVE_WINDOWS_H)
> -#if defined(_GLIBCXX_HAVE_READLINK) && defined(_GLIBCXX_HAVE_UNISTD_H)
> -    string_view str;
> -    char buf[128]; // strlen("../usr/share/zoneinfo/...") is usually < 55
> -    string dynbuf;
>       // /etc/localtime should be a symlink that ends with a zone name,
>       // e.g. /etc/localtime -> /usr/share/zoneinfo/Europe/London
>       // https://www.freedesktop.org/software/systemd/man/latest/localtime.html
>       // This should work on GNU/Linux, macOS, NetBSD, and OpenBSD.
> -    // Some FreeBSD systems also use a symlink for /etc/localtime.
> -    // Use readlink directly to avoid std::filesystem overhead.
> -    if (auto n = ::readlink("/etc/localtime", buf, sizeof(buf)); n > 0)
> +    // Some FreeBSD systems also use a symlink for /etc/localtime (since 15.0).
> +
> +    // N.B. we do not support dangling symlinks here. If that becomes necessary
> +    // then after realpath fails we could fallback to using
> +    // filesystem::weakly_canonical(filesystem::read_symlink("etc/localtime")).
> +
> +#if defined _GLIBCXX_USE_REALPATH && _XOPEN_VERSION >= 700
> +    unique_ptr<char[], void(*)(void*)> cbuf{ nullptr, &::free };
> +    string_view str;
> +    // Use realpath directly to avoid std::filesystem overhead.
> +    // We use realpath not readlink to resolve multiple levels of symlinks.
> +    if (char* p = ::realpath("/etc/localtime", nullptr))
>         {
> -	if (static_cast<size_t>(n) < sizeof(buf))
> -	  str = string_view(buf, n);
> -	else [[unlikely]]
> -	  {
> -	    // We read the symlink but it didn't fit in buf[], use dynbuf.
> -	    do
> -	      {
> -		n *= 2;
> -		dynbuf.__resize_and_overwrite(n, [](char* p, size_t len) {
> -		  auto n2 = ::readlink("/etc/localtime", p, len);
> -		  if (n2 == -1) // symlink removed or replaced by file?!
> -		    __throw_runtime_error("tzdb: error reading /etc/localtime");
> -		  const size_t r = n2;
> -		  return r < len ? r : 0;
> -		});
> -	      }
> -	    while (dynbuf.empty());
> -	    str = dynbuf;
> -	  }
> +	cbuf.reset(p);
> +	str = p;
>         }
> +#else
> +    string sbuf = std::filesystem::canonical("/etc/localtime").string();

Based on what I wrote above, the above statement will throw.

This is the full output of the failing test:

Testing tzdb/1.cc,  -std=gnu++20
doing compile
Executing on host: arm-none-eabi-g++ -fmessage-length=0 -fno-show-column  -g -O2 -DLOCALEDIR="."  -Werror -I/build/gcc_src/libstdc++-v3/testsuite/util  name1012763.cc  -mthumb -march=armv7ve+nofp -mcpu=cortex-a7 -mfloat-abi=soft -mfpu=auto  -fdiagnostics-plain-output -Wabi=20 -E     -o /dev/null    (timeout = 360)
spawn -ignore SIGHUP arm-none-eabi-g++ -fmessage-length=0 -fno-show-column -g -O2 -DLOCALEDIR="." -Werror -I/build/gcc_src/libstdc++-v3/testsuite/util name1012763.cc -mthumb -march=armv7ve+nofp -mcpu=cortex-a7 -mfloat-abi=soft -mfpu=auto -fdiagnostics-plain-output -Wabi=20 -E -o /dev/null
pid is 2863538 -2863538
pid is -1
output is  status 0
extra_tool_flags are:  -std=gnu++20
doing compile
Executing on host: arm-none-eabi-g++ -fmessage-length=0 -fno-show-column  -g -O2 -DLOCALEDIR="." -I/build/gcc_src/libstdc++-v3/testsuite/util  /build/gcc_src/libstdc++-v3/testsuite/std/time/tzdb/1.cc libstdc++_tg.o  -mthumb -march=armv7ve+nofp -mcpu=cortex-a7 -mfloat-abi=soft -mfpu=auto   -std=gnu++20   -fdiagnostics-plain-output -Wabi=20 ./libtestc++.a         --specs=rdimon.specs  -Wl,--start-group -lc -lm -Wl,--end-group --specs=nosys.specs -Wl,--allow-multiple-definition -Wl,-u,_isatty,-u,_fstat  -Wl,-wrap,exit -Wl,-wrap,_exit -Wl,-wrap,main -Wl,-wrap,abort -lm -T qemu.ld -o ./1.exe    (timeout = 360)
spawn -ignore SIGHUP arm-none-eabi-g++ -fmessage-length=0 -fno-show-column -g -O2 -DLOCALEDIR="." -I/build/gcc_src/libstdc++-v3/testsuite/util /build/gcc_src/libstdc++-v3/testsuite/std/time/tzdb/1.cc libstdc++_tg.o -mthumb -march=armv7ve+nofp -mcpu=cortex-a7 -mfloat-abi=soft -mfpu=auto -std=gnu++20 -fdiagnostics-plain-output -Wabi=20 ./libtestc++.a --specs=rdimon.specs -Wl,--start-group -lc -lm -Wl,--end-group --specs=nosys.specs -Wl,--allow-multiple-definition -Wl,-u,_isatty,-u,_fstat -Wl,-wrap,exit -Wl,-wrap,_exit -Wl,-wrap,main -Wl,-wrap,abort -lm -T qemu.ld -o ./1.exe
pid is 2863542 -2863542
pid is -1
output is  status 0
PASS: std/time/tzdb/1.cc  -std=gnu++20 (test for excess errors)
spawning command  qemu-system-arm -nographic -machine virt -cpu cortex-a7 -m 256 -semihosting -monitor /dev/null -kernel ./1.exe
spawn qemu-system-arm -nographic -machine virt -cpu cortex-a7 -m 256 -semihosting -monitor /dev/null -kernel ./1.exe
terminate called after throwing an instance of 'std::filesystem::__cxx11::filesystem_error'
   what():  filesystem error: cannot make canonical path: Function not implemented [/etc/localtime]

*** EXIT code 4242

*** EXIT code 1
pid is -1
Shell closed.
Output is terminate called after throwing an instance of 'std::filesystem::__cxx11::filesystem_error'
   what():  filesystem error: cannot make canonical path: Function not implemented [/etc/localtime]

*** EXIT code 4242

*** EXIT code 1

FAIL: std/time/tzdb/1.cc  -std=gnu++20 execution test



I do not know how to actually fix this since semihosting does not provide all the
required pieces to perform a real stat-call.
Regardless, if std::filesystem::canonical can throw, I suppose it should be cought
and the built in default tzdb should be returned or something along that line.

WDYT?

Kind regards,
Torbjörn

> +    string_view str = sbuf;
> +#endif
>   
> -    if (!str.empty())
> +    if (!str.empty() && str != "/etc/localtime")
>         {
> -	// Remove any redundant slashes so we can match zone names.
> -	// e.g. /usr/share/zoneinfo/Europe//London is a valid symlink,
> -	// but won't match against "Europe/London".
> -	if (auto pos = str.rfind("//"); pos != str.npos) [[unlikely]]
> -	  {
> -	    if (str.data() != dynbuf.data())
> -	      dynbuf = str;
> -	    string::size_type spos = pos;
> -	    do
> -	      {
> -		dynbuf.erase(spos, 1);
> -		spos = dynbuf.rfind("//", spos);
> -	      }
> -	    while (spos != dynbuf.npos);
> -	    str = dynbuf;
> -	  }
> -
>   	// Check the trailing components of the path against known zone names.
>   	// Valid IANA times zones can have one, two, or three parts, e.g.
>   	// "UTC", "Europe/London", and "America/Indiana/Indianapolis".
> @@ -2175,10 +2155,10 @@ constinit tzdb_list::_Node::NumLeapSeconds tzdb_list::_Node::num_leap_seconds;
>   				     str.substr(pos + 1)))
>   	  return tz;
>         }
> -#endif
> +
>       // Otherwise, look for a file naming the time zone.
>       string_view files[] {
> -      "/etc/timezone",    // Debian derivates
> +      "/etc/timezone",    // Debian derivates, non-systemd Gentoo
>         "/var/db/zoneinfo", // FreeBSD
>       };
>       for (auto f : files)



More information about the Libstdc++ mailing list