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)

Reply via email to