Skip to content

Commit

Permalink
libstdc++: Fix filesystem::remove_all races [PR104161]
Browse files Browse the repository at this point in the history
This fixes the remaining filesystem::remove_all race condition by using
POSIX openat to recurse into sub-directories and using POSIX unlinkat to
remove files. This avoids the remaining race where the directory being
removed is replaced with a symlink after the directory has been opened,
so that the filesystem::remove("subdir/file") resolves to "target/file"
instead, because "subdir" has been removed and replaced with a symlink.
The previous patch only fixed the case where the directory was replaced
with a symlink before we tried to open it, but it still used the full
(potentially compromised) path as an argument to filesystem::remove.

The first part of the fix is to use openat when recursing into a
sub-directory with recursive_directory_iterator. This means that opening
"dir/subdir" uses the file descriptor for "dir", and so is sure to open
"dir/subdir" and not "symlink/subdir". (The previous patch to use
O_NOFOLLOW already ensured we won't open "dir/symlink/" here.)

The second part of the fix is to use unlinkat for the remove_all
operation. Previously we used a directory_iterator to get the name of
each file in a directory and then used filesystem::remove(iter->path())
on that name. This meant that any checks (e.g. O_NOFOLLOW) done by the
iterator could be invalidated before the remove operation on that
pathname. The directory iterator contains an open DIR stream, which we
can use to obtain a file descriptor to pass to unlinkat. This ensures
that the file being deleted really is contained within the directory
we're iterating over, rather than using a pathname that could resolve to
some other file.

The filesystem::remove_all function previously used a (non-recursive)
filesystem::directory_iterator for each directory, and called itself
recursively for sub-directories. The new implementation uses a single
filesystem::recursive_directory_iterator object, and calls a new __erase
member function on that iterator. That new __erase member function does
the actual work of removing a file (or a directory after its contents
have been iterated over and removed) using unlinkat. That means we don't
need to expose the DIR stream or its file descriptor to the remove_all
function, it's still encapuslated by the iterator class.

It would be possible to add a __rewind member to directory iterators
too, to call rewinddir after each modification to the directory. That
would make it more likely for filesystem::remove_all to successfully
remove everything even if files are being written to the directory tree
while removing it. It's unclear if that is actually prefereable, or if
it's better to fail and report an error at the first opportunity.

The necessary APIs (openat, unlinkat, fdopendir, dirfd) are defined in
POSIX.1-2008, and in Glibc since 2.10. But if the target doesn't provide
them, the original code (with race conditions) is still used.

This also reduces the number of small memory allocations needed for
std::filesystem::remove_all, because we do not store the full path to
every directory entry that is iterated over. The new filename_only
option means we only store the filename in the directory entry, as that
is all we need in order to use openat or unlinkat.

Finally, rather than duplicating everything for the Filesystem TS, the
std::experimental::filesystem::remove_all implementation now just calls
std::filesystem::remove_all to do the work.

libstdc++-v3/ChangeLog:

	PR libstdc++/104161
	* acinclude.m4 (GLIBCXX_CHECK_FILESYSTEM_DEPS): Check for dirfd
	and unlinkat.
	* config.h.in: Regenerate.
	* configure: Regenerate.
	* include/bits/fs_dir.h (recursive_directory_iterator): Declare
	remove_all overloads as friends.
	(recursive_directory_iterator::__erase): Declare new member
	function.
	* include/bits/fs_fwd.h (remove, remove_all): Declare.
	* src/c++17/fs_dir.cc (_Dir): Add filename_only parameter to
	constructor. Pass file descriptor argument to base constructor.
	(_Dir::dir_and_pathname, _Dir::open_subdir, _Dir::do_unlink)
	(_Dir::unlink, _Dir::rmdir): Define new member functions.
	(directory_iterator): Pass filename_only argument to _Dir
	constructor.
	(recursive_directory_iterator::_Dir_stack): Adjust constructor
	parameters to take a _Dir rvalue instead of creating one.
	(_Dir_stack::orig): Add data member for storing original path.
	(_Dir_stack::report_error): Define new member function.
	(__directory_iterator_nofollow): Move here from dir-common.h and
	fix value to be a power of two.
	(__directory_iterator_filename_only): Define new constant.
	(recursive_directory_iterator): Construct _Dir object and move
	into _M_dirs stack. Pass skip_permission_denied argument to first
	advance call.
	(recursive_directory_iterator::increment): Use _Dir::open_subdir.
	(recursive_directory_iterator::__erase): Define new member
	function.
	* src/c++17/fs_ops.cc (ErrorReporter, do_remove_all): Remove.
	(fs::remove_all): Use new recursive_directory_iterator::__erase
	member function.
	* src/filesystem/dir-common.h (_Dir_base): Add int parameter to
	constructor and use openat to implement nofollow semantics.
	(_Dir_base::fdcwd, _Dir_base::set_close_on_exec, _Dir_base::openat):
	Define new member functions.
	(__directory_iterator_nofollow): Move to fs_dir.cc.
	* src/filesystem/dir.cc (_Dir): Pass file descriptor argument to
	base constructor.
	(_Dir::dir_and_pathname, _Dir::open_subdir): Define new member
	functions.
	(recursive_directory_iterator::_Dir_stack): Adjust constructor
	parameters to take a _Dir rvalue instead of creating one.
	(recursive_directory_iterator): Check for new nofollow option.
	Construct _Dir object and move into _M_dirs stack. Pass
	skip_permission_denied argument to first advance call.
	(recursive_directory_iterator::increment): Use _Dir::open_subdir.
	* src/filesystem/ops.cc (fs::remove_all): Use C++17 remove_all.
  • Loading branch information
jwakely committed Feb 4, 2022
1 parent b28b92b commit ebf6175
Show file tree
Hide file tree
Showing 10 changed files with 573 additions and 231 deletions.
27 changes: 26 additions & 1 deletion libstdc++-v3/acinclude.m4
Original file line number Diff line number Diff line change
Expand Up @@ -4748,13 +4748,38 @@ dnl
glibcxx_cv_fdopendir, [dnl
GCC_TRY_COMPILE_OR_LINK(
[#include <dirent.h>],
[::fdopendir(1);],
[::DIR* dir = ::fdopendir(1);],
[glibcxx_cv_fdopendir=yes],
[glibcxx_cv_fdopendir=no])
])
if test $glibcxx_cv_fdopendir = yes; then
AC_DEFINE(HAVE_FDOPENDIR, 1, [Define if fdopendir is available in <dirent.h>.])
fi
dnl
AC_CACHE_CHECK([for dirfd],
glibcxx_cv_dirfd, [dnl
GCC_TRY_COMPILE_OR_LINK(
[#include <dirent.h>],
[int fd = ::dirfd((::DIR*)0);],
[glibcxx_cv_dirfd=yes],
[glibcxx_cv_dirfd=no])
])
if test $glibcxx_cv_dirfd = yes; then
AC_DEFINE(HAVE_DIRFD, 1, [Define if dirfd is available in <dirent.h>.])
fi
dnl
AC_CACHE_CHECK([for unlinkat],
glibcxx_cv_unlinkat, [dnl
GCC_TRY_COMPILE_OR_LINK(
[#include <fcntl.h>
#include <unistd.h>],
[::unlinkat(AT_FDCWD, "", AT_REMOVEDIR);],
[glibcxx_cv_unlinkat=yes],
[glibcxx_cv_unlinkat=no])
])
if test $glibcxx_cv_unlinkat = yes; then
AC_DEFINE(HAVE_UNLINKAT, 1, [Define if unlinkat is available in <fcntl.h>.])
fi
dnl
CXXFLAGS="$ac_save_CXXFLAGS"
AC_LANG_RESTORE
Expand Down
6 changes: 6 additions & 0 deletions libstdc++-v3/config.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@
/* Define to 1 if you have the <dirent.h> header file. */
#undef HAVE_DIRENT_H

/* Define if dirfd is available in <dirent.h>. */
#undef HAVE_DIRFD

/* Define to 1 if you have the <dlfcn.h> header file. */
#undef HAVE_DLFCN_H

Expand Down Expand Up @@ -486,6 +489,9 @@
/* Define to 1 if you have the <unistd.h> header file. */
#undef HAVE_UNISTD_H

/* Define if unlinkat is available in <fcntl.h>. */
#undef HAVE_UNLINKAT

/* Define to 1 if you have the `uselocale' function. */
#undef HAVE_USELOCALE

Expand Down
116 changes: 114 additions & 2 deletions libstdc++-v3/configure
Original file line number Diff line number Diff line change
Expand Up @@ -77077,7 +77077,7 @@ else
int
main ()
{
::fdopendir(1);
::DIR* dir = ::fdopendir(1);
;
return 0;
}
Expand All @@ -77098,7 +77098,7 @@ cat confdefs.h - <<_ACEOF >conftest.$ac_ext
int
main ()
{
::fdopendir(1);
::DIR* dir = ::fdopendir(1);
;
return 0;
}
Expand All @@ -77119,6 +77119,118 @@ $as_echo "$glibcxx_cv_fdopendir" >&6; }

$as_echo "#define HAVE_FDOPENDIR 1" >>confdefs.h

fi
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for dirfd" >&5
$as_echo_n "checking for dirfd... " >&6; }
if ${glibcxx_cv_dirfd+:} 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 ()
{
int fd = ::dirfd((::DIR*)0);
;
return 0;
}
_ACEOF
if ac_fn_cxx_try_compile "$LINENO"; then :
glibcxx_cv_dirfd=yes
else
glibcxx_cv_dirfd=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 ()
{
int fd = ::dirfd((::DIR*)0);
;
return 0;
}
_ACEOF
if ac_fn_cxx_try_link "$LINENO"; then :
glibcxx_cv_dirfd=yes
else
glibcxx_cv_dirfd=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_dirfd" >&5
$as_echo "$glibcxx_cv_dirfd" >&6; }
if test $glibcxx_cv_dirfd = yes; then

$as_echo "#define HAVE_DIRFD 1" >>confdefs.h

fi
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for unlinkat" >&5
$as_echo_n "checking for unlinkat... " >&6; }
if ${glibcxx_cv_unlinkat+:} 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 <fcntl.h>
#include <unistd.h>
int
main ()
{
::unlinkat(AT_FDCWD, "", AT_REMOVEDIR);
;
return 0;
}
_ACEOF
if ac_fn_cxx_try_compile "$LINENO"; then :
glibcxx_cv_unlinkat=yes
else
glibcxx_cv_unlinkat=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 <fcntl.h>
#include <unistd.h>
int
main ()
{
::unlinkat(AT_FDCWD, "", AT_REMOVEDIR);
;
return 0;
}
_ACEOF
if ac_fn_cxx_try_link "$LINENO"; then :
glibcxx_cv_unlinkat=yes
else
glibcxx_cv_unlinkat=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_unlinkat" >&5
$as_echo "$glibcxx_cv_unlinkat" >&6; }
if test $glibcxx_cv_unlinkat = yes; then

$as_echo "#define HAVE_UNLINKAT 1" >>confdefs.h

fi
CXXFLAGS="$ac_save_CXXFLAGS"
ac_ext=c
Expand Down
8 changes: 8 additions & 0 deletions libstdc++-v3/include/bits/fs_dir.h
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,14 @@ _GLIBCXX_BEGIN_NAMESPACE_CXX11

struct _Dir_stack;
std::__shared_ptr<_Dir_stack> _M_dirs;

recursive_directory_iterator&
__erase(error_code* = nullptr);

friend uintmax_t
filesystem::remove_all(const path&, error_code&);
friend uintmax_t
filesystem::remove_all(const path&);
};

/// @relates std::filesystem::recursive_directory_iterator @{
Expand Down
4 changes: 4 additions & 0 deletions libstdc++-v3/include/bits/fs_fwd.h
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,10 @@ _GLIBCXX_END_NAMESPACE_CXX11
bool is_regular_file(file_status) noexcept;
bool is_symlink(file_status) noexcept;

bool remove(const path&, error_code&) noexcept;
uintmax_t remove_all(const path&);
uintmax_t remove_all(const path&, error_code&);

/// @}
} // namespace filesystem
_GLIBCXX_END_NAMESPACE_VERSION
Expand Down
Loading

0 comments on commit ebf6175

Please sign in to comment.