diff --git a/libcxx/src/filesystem/directory_iterator.cpp b/libcxx/src/filesystem/directory_iterator.cpp --- a/libcxx/src/filesystem/directory_iterator.cpp +++ b/libcxx/src/filesystem/directory_iterator.cpp @@ -25,56 +25,7 @@ namespace detail { namespace { -#if !defined(_LIBCPP_WIN32API) - -#if defined(DT_BLK) -template -static file_type get_file_type(DirEntT* ent, int) { - switch (ent->d_type) { - case DT_BLK: - return file_type::block; - case DT_CHR: - return file_type::character; - case DT_DIR: - return file_type::directory; - case DT_FIFO: - return file_type::fifo; - case DT_LNK: - return file_type::symlink; - case DT_REG: - return file_type::regular; - case DT_SOCK: - return file_type::socket; - // Unlike in lstat, hitting "unknown" here simply means that the underlying - // filesystem doesn't support d_type. Report is as 'none' so we correctly - // set the cache to empty. - case DT_UNKNOWN: - break; - } - return file_type::none; -} -#endif // defined(DT_BLK) - -template -static file_type get_file_type(DirEntT* ent, long) { - return file_type::none; -} - -static pair posix_readdir(DIR* dir_stream, - error_code& ec) { - struct dirent* dir_entry_ptr = nullptr; - errno = 0; // zero errno in order to detect errors - ec.clear(); - if ((dir_entry_ptr = ::readdir(dir_stream)) == nullptr) { - if (errno) - ec = capture_errno(); - return {}; - } else { - return {dir_entry_ptr->d_name, get_file_type(dir_entry_ptr, 0)}; - } -} -#else -// defined(_LIBCPP_WIN32API) +#if defined(_LIBCPP_WIN32API) static file_type get_file_type(const WIN32_FIND_DATAW& data) { if (data.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT && diff --git a/libcxx/src/filesystem/filesystem_common.h b/libcxx/src/filesystem/filesystem_common.h --- a/libcxx/src/filesystem/filesystem_common.h +++ b/libcxx/src/filesystem/filesystem_common.h @@ -21,6 +21,7 @@ #include "system_error" #if !defined(_LIBCPP_WIN32API) +# include // for DIR* & friends # include # include # include @@ -527,6 +528,54 @@ return posix_utimensat(p, TS, ec); #endif } + +#if defined(DT_BLK) +template +static file_type get_file_type(DirEntT* ent, int) { + switch (ent->d_type) { + case DT_BLK: + return file_type::block; + case DT_CHR: + return file_type::character; + case DT_DIR: + return file_type::directory; + case DT_FIFO: + return file_type::fifo; + case DT_LNK: + return file_type::symlink; + case DT_REG: + return file_type::regular; + case DT_SOCK: + return file_type::socket; + // Unlike in lstat, hitting "unknown" here simply means that the underlying + // filesystem doesn't support d_type. Report is as 'none' so we correctly + // set the cache to empty. + case DT_UNKNOWN: + break; + } + return file_type::none; +} +#endif // defined(DT_BLK) + +template +static file_type get_file_type(DirEntT*, long) { + return file_type::none; +} + +static pair posix_readdir(DIR* dir_stream, + error_code& ec) { + struct dirent* dir_entry_ptr = nullptr; + errno = 0; // zero errno in order to detect errors + ec.clear(); + if ((dir_entry_ptr = ::readdir(dir_stream)) == nullptr) { + if (errno) + ec = capture_errno(); + return {}; + } else { + return {dir_entry_ptr->d_name, get_file_type(dir_entry_ptr, 0)}; + } +} + #endif /* !_LIBCPP_WIN32API */ } // namespace diff --git a/libcxx/src/filesystem/operations.cpp b/libcxx/src/filesystem/operations.cpp --- a/libcxx/src/filesystem/operations.cpp +++ b/libcxx/src/filesystem/operations.cpp @@ -46,6 +46,8 @@ # include // for gettimeofday and timeval #endif +#include + #if defined(__ELF__) && defined(_LIBCPP_LINK_RT_LIB) # pragma comment(lib, "rt") #endif @@ -1340,40 +1342,87 @@ namespace { -uintmax_t remove_all_impl(path const& p, error_code& ec) { - const auto npos = static_cast(-1); - const file_status st = __symlink_status(p, &ec); - if (ec) - return npos; - uintmax_t count = 1; - if (is_directory(st)) { - for (directory_iterator it(p, ec); !ec && it != directory_iterator(); - it.increment(ec)) { - auto other_count = remove_all_impl(it->path(), ec); - if (ec) - return npos; - count += other_count; +template +struct scope_exit { + explicit scope_exit(Cleanup const& cleanup) + : cleanup_(cleanup) + { } + + ~scope_exit() { cleanup_(); } + +private: + Cleanup cleanup_; +}; + +uintmax_t remove_all_impl(int parent_directory, const path& p, error_code& ec) { + // First, try to open the path as a directory. + const int options = O_CLOEXEC | O_RDONLY | O_DIRECTORY | O_NOFOLLOW; + int fd = ::openat(parent_directory, p.c_str(), options); + scope_exit close_fd([fd] { if (fd != -1) ::close(fd); }); + if (fd != -1) { + // If that worked, first iterate over the contents of the directory and + // remove everything in it, recursively. + DIR* stream = ::fdopendir(fd); + scope_exit close_stream([stream] { if (stream != nullptr) ::closedir(stream); }); + if (stream == nullptr) { + ec = detail::capture_errno(); + return 0; } - if (ec) - return npos; + + intmax_t count = 0; + while (true) { + auto [str, type] = detail::posix_readdir(stream, ec); + if (str == "." || str == "..") { + continue; + } else if (ec || str.empty()) { + break; // we're done iterating through the directory + } else { + count += remove_all_impl(fd, str, ec); + } + } + + // Then, remove the directory itself. + if (::unlinkat(parent_directory, p.c_str(), AT_REMOVEDIR) == -1) { + ec = detail::capture_errno(); + return count; + } + + return count + 1; // the contents of the directory + the directory itself + } + + // Otherwise, we failed to open `p` because it didn't exist, it's not an + // error -- it might have moved or have been deleted already. + ec = detail::capture_errno(); + if (ec == errc::no_such_file_or_directory) { + ec.clear(); + return 0; + } + + // If opening `p` failed because it wasn't a directory, remove it as a normal + // file instead. + else if (ec == errc::not_a_directory) { + ec.clear(); + if (::unlinkat(parent_directory, p.c_str(), /* flags = */0) == -1) { + ec = detail::capture_errno(); + return 0; + } + return 1; + } + + // Otherwise, it's a real error -- we don't remove anything. + else { + return 0; } - if (!__remove(p, &ec)) - return npos; - return count; } } // end namespace uintmax_t __remove_all(const path& p, error_code* ec) { ErrorHandler err("remove_all", ec, &p); - error_code mec; - auto count = remove_all_impl(p, mec); - if (mec) { - if (mec == errc::no_such_file_or_directory) - return 0; + uintmax_t count = remove_all_impl(AT_FDCWD, p, mec); + if (mec) return err.report(mec); - } return count; } diff --git a/libcxx/test/std/input.output/filesystems/fs.op.funcs/fs.op.remove_all/toctou.pass.cpp b/libcxx/test/std/input.output/filesystems/fs.op.funcs/fs.op.remove_all/toctou.pass.cpp new file mode 100644 --- /dev/null +++ b/libcxx/test/std/input.output/filesystems/fs.op.funcs/fs.op.remove_all/toctou.pass.cpp @@ -0,0 +1,83 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +// UNSUPPORTED: c++03 +// UNSUPPORTED: no-exceptions + +// + +// Test for a time-of-check to time-of-use issue with std::filesystem::remove_all. +// +// Scenario: +// The attacker wants to get directory contents deleted, to which he does not have access. +// He has a way to get a privileged binary call `std::filesystem::remove_all()` on a +// directory he controls, e.g. in his home directory. +// +// The POC sets up the `attack_dest/attack_file` which the attacker wants to have deleted. +// The attacker repeatedly creates a directory and replaces it with a symlink from +// `victim_del` to `attack_dest` while the victim code calls `std::filesystem::remove_all()` +// on `victim_del`. After a few seconds the attack has succeeded and +// `attack_dest/attack_file` is deleted. +// +// This is taken from https://github.com/rust-lang/wg-security-response/blob/master/patches/CVE-2022-21658/0002-Fix-CVE-2022-21658-for-UNIX-like.patch + +#include +#include +#include +#include + +#include "filesystem_include.h" + +int main() { + fs::path tmpdir = "/tmp/mydir"; + fs::path victim_del_path = tmpdir / "victim_del"; + + // setup dest + fs::path attack_dest_dir = tmpdir / "attack_dest"; + fs::create_directories(attack_dest_dir); + fs::path attack_dest_file = attack_dest_dir / "attack_file"; + { std::ofstream of(attack_dest_file); } + + // victim just continuously removes `victim_del` + bool stop = false; + std::thread t{[&]() { + while (!stop) { + try { + fs::remove_all(victim_del_path); + } catch (fs::filesystem_error const&) { + // ignore + } + } + }}; + + // attacker (could of course be in a separate process) + auto start_time = std::chrono::system_clock::now(); + auto elapsed_since = [](std::chrono::system_clock::time_point const& time_point) { + return std::chrono::duration_cast(std::chrono::system_clock::now() - time_point); + }; + bool attack_succeeded = false; + while (elapsed_since(start_time) < std::chrono::seconds(5)) { + if (!fs::exists(attack_dest_file)) { + std::cout << "Victim deleted symlinked file outside of victim_del. Attack succeeded in " + << elapsed_since(start_time).count() << " seconds." << std::endl; + attack_succeeded = true; + break; + } + try { + fs::create_directory(victim_del_path); + } catch (fs::filesystem_error const&) { + continue; + } + fs::remove(victim_del_path); + fs::create_directory_symlink(attack_dest_dir, victim_del_path); + } + stop = true; + t.join(); + + return attack_succeeded ? 1 : 0; +}