[gcc r17-4804] libstdc++: Fix remove_all without unlinkat or dirent::d_type [PR127555]

Jonathan Wakely redi@gcc.gnu.org
Wed Sep 30 18:02:02 GMT 2026


https://gcc.gnu.org/g:f7094fe5ff882f0c206d51ebe93f33f0193ce636

commit r17-4804-gf7094fe5ff882f0c206d51ebe93f33f0193ce636
Author: Gokbey Gazi Keskin <gokbeykeskin@gmail.com>
Date:   Mon Sep 28 14:42:20 2026 +0300

    libstdc++: Fix remove_all without unlinkat or dirent::d_type [PR127555]
    
    When neither unlinkat nor dirent::d_type is available, _Dir::advance
    leaves every entry with file_type::none, so __erase takes the unlink
    path.  Without unlinkat _Dir::do_unlink ignores its is_directory
    argument and always calls fs::remove, i.e. ::remove, which retries as
    rmdir for a directory.  Unlinking a non-empty directory therefore
    reports the rmdir errno for "not empty" rather than the EPERM or EISDIR
    that unlink on a directory would give, the retry-as-directory path is
    not taken, and remove_all fails with (uintmax_t)-1 on the first
    non-empty sub-directory.  POSIX allows either ENOTEMPTY or EEXIST for
    that condition, so accept both, but only when unlinkat is unavailable.
    
    An empty directory of unknown type is still removed correctly, because
    ::remove falls back to rmdir and succeeds, so reproducing this needs a
    tree at least two levels deep.
    
            PR libstdc++/127555
    
    libstdc++-v3/ChangeLog:
    
            * src/c++17/fs_dir.cc (recursive_directory_iterator::__erase):
            Retry as a directory when unlink fails with ENOTEMPTY or EEXIST
            and unlinkat is not available.
    
    Signed-off-by: Gokbey Gazi Keskin <gokbeykeskin@gmail.com>

Diff:
---
 libstdc++-v3/src/c++17/fs_dir.cc | 14 ++++++++++++--
 1 file changed, 12 insertions(+), 2 deletions(-)

diff --git a/libstdc++-v3/src/c++17/fs_dir.cc b/libstdc++-v3/src/c++17/fs_dir.cc
index f6860e264110..7cced0db823b 100644
--- a/libstdc++-v3/src/c++17/fs_dir.cc
+++ b/libstdc++-v3/src/c++17/fs_dir.cc
@@ -514,13 +514,23 @@ fs::recursive_directory_iterator::__erase(error_code* ecptr)
 	{
 	  // We did not have a cached type, so it's possible that top.entry
 	  // is actually a directory, and that's why the unlink above failed.
+	  const int err = ec.value();
 #ifdef EPERM
 	  // POSIX.1-2017 says unlink on a directory returns EPERM,
 	  // but LSB allows EISDIR too. Some targets don't even define EPERM.
-	  if (ec.value() == EPERM || ec.value() == EISDIR)
+	  bool is_dir = err == EPERM || err == EISDIR;
 #else
-	  if (ec.value() == EISDIR)
+	  bool is_dir = err == EISDIR;
 #endif
+#if ! _GLIBCXX_HAVE_UNLINKAT
+	  // Without unlinkat, _Dir::unlink uses ::remove, which retries as
+	  // rmdir for a directory. Unlinking a *non-empty* directory then
+	  // reports "directory not empty" instead of the errno for unlink
+	  // on a directory, so treat that as a directory too. POSIX allows
+	  // either ENOTEMPTY or EEXIST for that condition.
+	  is_dir = is_dir || err == ENOTEMPTY || err == EEXIST;
+#endif
+	  if (is_dir)
 	    {
 	      // Retry, treating it as a directory.
 	      top.entry._M_type = file_type::directory;


More information about the Libstdc++-cvs mailing list