]> git.ipfire.org Git - thirdparty/gcc.git/commitdiff
libstdc++: Avoid symlink race in filesystem::remove_all [PR104161]
authorJonathan Wakely <jwakely@redhat.com>
Sun, 23 Jan 2022 21:45:16 +0000 (21:45 +0000)
committerJonathan Wakely <jwakely@redhat.com>
Wed, 4 Oct 2023 11:10:03 +0000 (12:10 +0100)
This adds a new internal flag to the filesystem::directory_iterator
constructor that makes it fail if the path is a symlink that resolves to
a directory. This prevents filesystem::remove_all from following a
symlink to a directory, rather than deleting the symlink itself.

We can also use that new flag in recursive_directory_iterator to ensure
that we don't follow symlinks if the follow_directory_symlink option is
not set.

This also moves an error check in filesystem::remove_all after the while
loop, so that errors from the directory_iterator constructor are
reproted, instead of continuing to the filesystem::remove call below.

libstdc++-v3/ChangeLog:

PR libstdc++/104161
* acinclude.m4 (GLIBCXX_CHECK_FILESYSTEM_DEPS): Check for
fdopendir.
* config.h.in: Regenerate.
* configure: Regenerate.
* src/c++17/fs_dir.cc (_Dir): Add nofollow flag to constructor
and pass it to base class constructor.
(directory_iterator): Pass nofollow flag to _Dir constructor.
(fs::recursive_directory_iterator::increment): Likewise.
* src/c++17/fs_ops.cc (do_remove_all): Use nofollow option for
directory_iterator constructor. Move error check outside loop.
* src/filesystem/dir-common.h (_Dir_base): Add nofollow flag to
constructor and when it's set use ::open with O_NOFOLLOW and
O_DIRECTORY.
* src/filesystem/dir.cc (_Dir): Add nofollow flag to constructor
and pass it to base class constructor.
(directory_iterator): Pass nofollow flag to _Dir constructor.
(fs::recursive_directory_iterator::increment): Likewise.
* src/filesystem/ops.cc (remove_all): Use nofollow option for
directory_iterator constructor. Move error check outside loop.

(cherry picked from commit c8bd4dc8212e43b2f9af08b80df97f90cdb0df4f)

libstdc++-v3/acinclude.m4
libstdc++-v3/config.h.in
libstdc++-v3/configure
libstdc++-v3/src/c++17/fs_dir.cc
libstdc++-v3/src/c++17/fs_ops.cc
libstdc++-v3/src/filesystem/dir-common.h
libstdc++-v3/src/filesystem/dir.cc
libstdc++-v3/src/filesystem/ops.cc

index 15c975ff86e51ac8ae854a424d08967a0c41699e..d7d49b02e667aa939044f9b3e39fcd632ebdda97 100644 (file)
@@ -4825,6 +4825,18 @@ dnl
     AC_DEFINE(HAVE_TRUNCATE, 1, [Define if truncate is available in <unistd.h>.])
   fi
   AC_MSG_RESULT($glibcxx_cv_truncate)
+dnl
+  AC_CACHE_CHECK([for fdopendir],
+    glibcxx_cv_fdopendir, [dnl
+    GCC_TRY_COMPILE_OR_LINK(
+      [#include <dirent.h>],
+      [::fdopendir(1);],
+      [glibcxx_cv_fdopendir=yes],
+      [glibcxx_cv_fdopendir=no])
+  ])
+  if test $glibcxx_cv_truncate = yes; then
+    AC_DEFINE(HAVE_FDOPENDIR, 1, [Define if fdopendir is available in <dirent.h>.])
+  fi
 dnl
   CXXFLAGS="$ac_save_CXXFLAGS"
   AC_LANG_RESTORE
index ea88281f438695f7f9844786aec2f7f0f269d469..9145c154e863108580e2c51b7b2f78dc329738a1 100644 (file)
@@ -93,6 +93,9 @@
 /* Define to 1 if you have the <fcntl.h> header file. */
 #undef HAVE_FCNTL_H
 
+/* Define if fdopendir is available in <dirent.h>. */
+#undef HAVE_FDOPENDIR
+
 /* Define to 1 if you have the <fenv.h> header file. */
 #undef HAVE_FENV_H
 
index 1d5ffd8df111e8721e7a4aabb454cdace548de86..6ff56b7d2d77c1e0bb2685eb897b62fc8ed1be37 100755 (executable)
@@ -76785,6 +76785,61 @@ $as_echo "#define HAVE_TRUNCATE 1" >>confdefs.h
   fi
   { $as_echo "$as_me:${as_lineno-$LINENO}: result: $glibcxx_cv_truncate" >&5
 $as_echo "$glibcxx_cv_truncate" >&6; }
+  { $as_echo "$as_me:${as_lineno-$LINENO}: checking for fdopendir" >&5
+$as_echo_n "checking for fdopendir... " >&6; }
+if ${glibcxx_cv_fdopendir+:} false; then :
+  $as_echo_n "(cached) " >&6
+else
+      if test x$gcc_no_link = xyes; then
+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+#include <dirent.h>
+int
+main ()
+{
+::fdopendir(1);
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_cxx_try_compile "$LINENO"; then :
+  glibcxx_cv_fdopendir=yes
+else
+  glibcxx_cv_fdopendir=no
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+else
+  if test x$gcc_no_link = xyes; then
+  as_fn_error $? "Link tests are not allowed after GCC_NO_EXECUTABLES." "$LINENO" 5
+fi
+cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+#include <dirent.h>
+int
+main ()
+{
+::fdopendir(1);
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_cxx_try_link "$LINENO"; then :
+  glibcxx_cv_fdopendir=yes
+else
+  glibcxx_cv_fdopendir=no
+fi
+rm -f core conftest.err conftest.$ac_objext \
+    conftest$ac_exeext conftest.$ac_ext
+fi
+
+fi
+{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $glibcxx_cv_fdopendir" >&5
+$as_echo "$glibcxx_cv_fdopendir" >&6; }
+  if test $glibcxx_cv_truncate = yes; then
+
+$as_echo "#define HAVE_FDOPENDIR 1" >>confdefs.h
+
+  fi
   CXXFLAGS="$ac_save_CXXFLAGS"
   ac_ext=c
 ac_cpp='$CPP $CPPFLAGS'
index 054d39089915afc1f259a63fc7ecbea7acf222a7..efd5151d32aa5626c002699beb9aad0e4452f7c3 100644 (file)
@@ -44,8 +44,9 @@ template class std::__shared_ptr<fs::recursive_directory_iterator::_Dir_stack>;
 
 struct fs::_Dir : _Dir_base
 {
-  _Dir(const fs::path& p, bool skip_permission_denied, error_code& ec)
-  : _Dir_base(p.c_str(), skip_permission_denied, ec)
+  _Dir(const fs::path& p, bool skip_permission_denied, bool nofollow,
+       error_code& ec)
+  : _Dir_base(p.c_str(), skip_permission_denied, nofollow, ec)
   {
     if (!ec)
       path = p;
@@ -128,11 +129,15 @@ namespace
 fs::directory_iterator::
 directory_iterator(const path& p, directory_options options, error_code* ecptr)
 {
+  // Do not report an error for permission denied errors.
   const bool skip_permission_denied
     = is_set(options, directory_options::skip_permission_denied);
+  // Do not allow opening a symlink (used by filesystem::remove_all)
+  const bool nofollow
+     = is_set(options, __directory_iterator_nofollow);
 
   error_code ec;
-  _Dir dir(p, skip_permission_denied, ec);
+  _Dir dir(p, skip_permission_denied, nofollow, ec);
 
   if (dir.dirp)
     {
@@ -287,7 +292,7 @@ fs::recursive_directory_iterator::increment(error_code& ec)
 
   if (std::exchange(_M_dirs->pending, true) && top.should_recurse(follow, ec))
     {
-      _Dir dir(top.entry.path(), skip_permission_denied, ec);
+      _Dir dir(top.entry.path(), skip_permission_denied, !follow, ec);
       if (ec)
        {
          _M_dirs.reset();
index 1fb295498c6a4b6f1a24f1916fcc284ed3c45f94..03d0da7d87cb418da64b37db704b3ec42b49ce07 100644 (file)
@@ -1340,7 +1340,7 @@ namespace
     uintmax_t count = 0;
     if (s.type() == file_type::directory)
       {
-       directory_iterator d(p, ec), end;
+       directory_iterator d(p, directory_options{99}, ec), end;
        while (d != end)
          {
            const auto removed = fs::do_remove_all(d->path(), err);
@@ -1349,11 +1349,11 @@ namespace
            count += removed;
 
            d.increment(ec);
-           if (ec)
-             {
-               err.report(ec, p);
-               return -1;
-             }
+         }
+       if (ec)
+         {
+           err.report(ec, p);
+           return -1;
          }
       }
 
index a49b8304a29d149f561bb7d088bbff03513ac878..d90ff8938caa21c829990b88112e10c506c9aa9c 100644 (file)
 # endif
 # include <dirent.h>
 #endif
+#ifdef _GLIBCXX_HAVE_FCNTL_H
+# include <fcntl.h> // O_NOFOLLOW, O_DIRECTORY
+# include <unistd.h> // close
+#endif
 
 namespace std _GLIBCXX_VISIBILITY(default)
 {
@@ -76,21 +80,40 @@ struct _Dir_base
   _Dir_base(posix::DIR* dirp = nullptr) : dirp(dirp) { }
 
   // If no error occurs then dirp is non-null,
-  // otherwise null (whether error ignored or not).
+  // otherwise null (even if an EACCES error is ignored).
   _Dir_base(const posix::char_type* pathname, bool skip_permission_denied,
-           error_code& ec) noexcept
-  : dirp(posix::opendir(pathname))
+           [[maybe_unused]] bool nofollow, error_code& ec) noexcept
+  : dirp(nullptr)
   {
-    if (dirp)
+#if defined O_RDONLY && O_NOFOLLOW && defined O_DIRECTORY && defined O_CLOEXEC \
+    && defined _GLIBCXX_HAVE_FDOPENDIR && !_GLIBCXX_FILESYSTEM_IS_WINDOWS
+    if (nofollow)
+      {
+       // Do not allow opening a symlink (used by filesystem::remove_all)
+       const int flags = O_RDONLY | O_NOFOLLOW | O_DIRECTORY | O_CLOEXEC;
+       int fd = ::open(pathname, flags);
+       if (fd != -1)
+         {
+           if ((dirp = ::fdopendir(fd)))
+             {
+               ec.clear();
+               return;
+             }
+         }
+       if (errno == EACCES && skip_permission_denied)
+         ec.clear();
+       else
+         ec.assign(errno, std::generic_category());
+       return;
+      }
+#endif
+
+    if ((dirp = posix::opendir(pathname)))
+      ec.clear();
+    else if (errno == EACCES && skip_permission_denied)
       ec.clear();
     else
-    {
-      const int err = errno;
-      if (err == EACCES && skip_permission_denied)
-       ec.clear();
-      else
-       ec.assign(err, std::generic_category());
-    }
+      ec.assign(errno, std::generic_category());
   }
 
   _Dir_base(_Dir_base&& d) : dirp(std::exchange(d.dirp, nullptr)) { }
@@ -187,6 +210,9 @@ get_file_type(const std::filesystem::__gnu_posix::dirent& d [[gnu::unused]])
   return file_type::none;
 #endif
 }
+
+constexpr directory_options __directory_iterator_nofollow{99};
+
 _GLIBCXX_END_NAMESPACE_FILESYSTEM
 
 _GLIBCXX_END_NAMESPACE_VERSION
index 446ddf0d2f282ba0f96ec240e6cd914bfdffb69f..d9c15aa9c34622bb26a6e901cdaa28099b985151 100644 (file)
@@ -51,8 +51,9 @@ namespace posix = std::filesystem::__gnu_posix;
 
 struct fs::_Dir : std::filesystem::_Dir_base
 {
-  _Dir(const fs::path& p, bool skip_permission_denied, error_code& ec)
-  : _Dir_base(p.c_str(), skip_permission_denied, ec)
+  _Dir(const fs::path& p, bool skip_permission_denied, bool nofollow,
+       error_code& ec)
+  : _Dir_base(p.c_str(), skip_permission_denied, nofollow, ec)
   {
     if (!ec)
       path = p;
@@ -130,11 +131,15 @@ namespace
 fs::directory_iterator::
 directory_iterator(const path& p, directory_options options, error_code* ecptr)
 {
+  // Do not report an error for permission denied errors.
   const bool skip_permission_denied
     = is_set(options, directory_options::skip_permission_denied);
+  // Do not allow opening a symlink (used by filesystem::remove_all)
+  const bool nofollow
+     = is_set(options, __directory_iterator_nofollow);
 
   error_code ec;
-  _Dir dir(p, skip_permission_denied, ec);
+  _Dir dir(p, skip_permission_denied, nofollow, ec);
 
   if (dir.dirp)
     {
@@ -274,7 +279,7 @@ fs::recursive_directory_iterator::increment(error_code& ec) noexcept
 
   if (std::exchange(_M_pending, true) && top.should_recurse(follow, ec))
     {
-      _Dir dir(top.entry.path(), skip_permission_denied, ec);
+      _Dir dir(top.entry.path(), skip_permission_denied, !follow, ec);
       if (ec)
        {
          _M_dirs.reset();
index 33b8206b7d88959a1e4bbc3e906f0c6bc8be7df7..22f1763047090de53347f196ca43cf7cc03d61d6 100644 (file)
@@ -1113,7 +1113,7 @@ fs::remove_all(const path& p, error_code& ec)
   uintmax_t count = 0;
   if (s.type() == file_type::directory)
     {
-      directory_iterator d(p, ec), end;
+      directory_iterator d(p, directory_options{99}, ec), end;
       while (!ec && d != end)
        {
          const auto removed = fs::remove_all(d->path(), ec);
@@ -1121,9 +1121,9 @@ fs::remove_all(const path& p, error_code& ec)
            return -1;
          count += removed;
          d.increment(ec);
-         if (ec)
-           return -1;
        }
+      if (ec)
+       return -1;
     }
 
   if (fs::remove(p, ec))