Skip to content

Commit 4f67a90

Browse files
committed
[libc++] Fix TOCTOU issue with std::filesystem::remove_all
https://bugs.chromium.org/p/llvm/issues/detail?id=19 rdar://87912416 Differential Revision: https://reviews.llvm.org/D118134
1 parent c7b255e commit 4f67a90

File tree

2 files changed

+195
-1
lines changed

2 files changed

+195
-1
lines changed

libcxx/src/filesystem/operations.cpp

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@
2424
# define NOMINMAX
2525
# include <windows.h>
2626
#else
27-
# include <unistd.h>
27+
# include <dirent.h>
2828
# include <sys/stat.h>
2929
# include <sys/statvfs.h>
30+
# include <unistd.h>
3031
#endif
3132
#include <time.h>
3233
#include <fcntl.h> /* values for fchmodat */
@@ -1338,6 +1339,19 @@ bool __remove(const path& p, error_code* ec) {
13381339
return true;
13391340
}
13401341

1342+
// We currently have two implementations of `__remove_all`. The first one is general and
1343+
// used on platforms where we don't have access to the `openat()` family of POSIX functions.
1344+
// That implementation uses `directory_iterator`, however it is vulnerable to some race
1345+
// conditions, see https://reviews.llvm.org/D118134 for details.
1346+
//
1347+
// The second implementation is used on platforms where `openat()` & friends are available,
1348+
// and it threads file descriptors through recursive calls to avoid such race conditions.
1349+
#if defined(_LIBCPP_WIN32API)
1350+
# define REMOVE_ALL_USE_DIRECTORY_ITERATOR
1351+
#endif
1352+
1353+
#if defined(REMOVE_ALL_USE_DIRECTORY_ITERATOR)
1354+
13411355
namespace {
13421356

13431357
uintmax_t remove_all_impl(path const& p, error_code& ec) {
@@ -1377,6 +1391,97 @@ uintmax_t __remove_all(const path& p, error_code* ec) {
13771391
return count;
13781392
}
13791393

1394+
#else // !REMOVE_ALL_USE_DIRECTORY_ITERATOR
1395+
1396+
namespace {
1397+
1398+
template <class Cleanup>
1399+
struct scope_exit {
1400+
explicit scope_exit(Cleanup const& cleanup)
1401+
: cleanup_(cleanup)
1402+
{ }
1403+
1404+
~scope_exit() { cleanup_(); }
1405+
1406+
private:
1407+
Cleanup cleanup_;
1408+
};
1409+
1410+
uintmax_t remove_all_impl(int parent_directory, const path& p, error_code& ec) {
1411+
// First, try to open the path as a directory.
1412+
const int options = O_CLOEXEC | O_RDONLY | O_DIRECTORY | O_NOFOLLOW;
1413+
int fd = ::openat(parent_directory, p.c_str(), options);
1414+
if (fd != -1) {
1415+
// If that worked, iterate over the contents of the directory and
1416+
// remove everything in it, recursively.
1417+
scope_exit close_fd([=] { ::close(fd); });
1418+
DIR* stream = ::fdopendir(fd);
1419+
if (stream == nullptr) {
1420+
ec = detail::capture_errno();
1421+
return 0;
1422+
}
1423+
scope_exit close_stream([=] { ::closedir(stream); });
1424+
1425+
uintmax_t count = 0;
1426+
while (true) {
1427+
auto [str, type] = detail::posix_readdir(stream, ec);
1428+
static_assert(std::is_same_v<decltype(str), std::string_view>);
1429+
if (str == "." || str == "..") {
1430+
continue;
1431+
} else if (ec || str.empty()) {
1432+
break; // we're done iterating through the directory
1433+
} else {
1434+
count += remove_all_impl(fd, str, ec);
1435+
}
1436+
}
1437+
1438+
// Then, remove the now-empty directory itself.
1439+
if (::unlinkat(parent_directory, p.c_str(), AT_REMOVEDIR) == -1) {
1440+
ec = detail::capture_errno();
1441+
return count;
1442+
}
1443+
1444+
return count + 1; // the contents of the directory + the directory itself
1445+
}
1446+
1447+
ec = detail::capture_errno();
1448+
1449+
// If we failed to open `p` because it didn't exist, it's not an
1450+
// error -- it might have moved or have been deleted already.
1451+
if (ec == errc::no_such_file_or_directory) {
1452+
ec.clear();
1453+
return 0;
1454+
}
1455+
1456+
// If opening `p` failed because it wasn't a directory, remove it as
1457+
// a normal file instead. Note that `openat()` can return either ENOTDIR
1458+
// or ELOOP depending on the exact reason of the failure.
1459+
if (ec == errc::not_a_directory || ec == errc::too_many_symbolic_link_levels) {
1460+
ec.clear();
1461+
if (::unlinkat(parent_directory, p.c_str(), /* flags = */0) == -1) {
1462+
ec = detail::capture_errno();
1463+
return 0;
1464+
}
1465+
return 1;
1466+
}
1467+
1468+
// Otherwise, it's a real error -- we don't remove anything.
1469+
return 0;
1470+
}
1471+
1472+
} // end namespace
1473+
1474+
uintmax_t __remove_all(const path& p, error_code* ec) {
1475+
ErrorHandler<uintmax_t> err("remove_all", ec, &p);
1476+
error_code mec;
1477+
uintmax_t count = remove_all_impl(AT_FDCWD, p, mec);
1478+
if (mec)
1479+
return err.report(mec);
1480+
return count;
1481+
}
1482+
1483+
#endif // REMOVE_ALL_USE_DIRECTORY_ITERATOR
1484+
13801485
void __rename(const path& from, const path& to, error_code* ec) {
13811486
ErrorHandler<void> err("rename", ec, &from, &to);
13821487
if (detail::rename(from.c_str(), to.c_str()) == -1)
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
// UNSUPPORTED: c++03
10+
// UNSUPPORTED: libcpp-has-no-localization
11+
// UNSUPPORTED: libcpp-has-no-threads
12+
13+
// <filesystem>
14+
15+
// Test for a time-of-check to time-of-use issue with std::filesystem::remove_all.
16+
//
17+
// Scenario:
18+
// The attacker wants to get directory contents deleted, to which he does not have access.
19+
// He has a way to get a privileged binary call `std::filesystem::remove_all()` on a
20+
// directory he controls, e.g. in his home directory.
21+
//
22+
// The POC sets up the `attack_dest/attack_file` which the attacker wants to have deleted.
23+
// The attacker repeatedly creates a directory and replaces it with a symlink from
24+
// `victim_del` to `attack_dest` while the victim code calls `std::filesystem::remove_all()`
25+
// on `victim_del`. After a few seconds the attack has succeeded and
26+
// `attack_dest/attack_file` is deleted.
27+
//
28+
// 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
29+
30+
// This test requires a dylib containing the fix shipped in https://reviews.llvm.org/D118134.
31+
// We use UNSUPPORTED instead of XFAIL because the test might not fail reliably.
32+
// UNSUPPORTED: use_system_cxx_lib && target={{.+}}-apple-macosx10.{{9|10|11|12|13|14|15}}
33+
// UNSUPPORTED: use_system_cxx_lib && target={{.+}}-apple-macosx11
34+
// UNSUPPORTED: use_system_cxx_lib && target={{.+}}-apple-macosx12.{{0|1|2}}
35+
36+
// Windows doesn't support the necessary APIs to mitigate this issue.
37+
// UNSUPPORTED: target={{.+}}-windows-{{.+}}
38+
39+
#include <cstdio>
40+
#include <filesystem>
41+
#include <system_error>
42+
#include <thread>
43+
44+
#include "filesystem_include.h"
45+
#include "filesystem_test_helper.h"
46+
47+
int main() {
48+
scoped_test_env env;
49+
fs::path const tmpdir = env.create_dir("mydir");
50+
fs::path const victim_del_path = tmpdir / "victim_del";
51+
fs::path const attack_dest_dir = env.create_dir(tmpdir / "attack_dest");
52+
fs::path const attack_dest_file = env.create_file(attack_dest_dir / "attack_file", 42);
53+
54+
// victim just continuously removes `victim_del`
55+
bool stop = false;
56+
std::thread t{[&]() {
57+
while (!stop) {
58+
std::error_code ec;
59+
fs::remove_all(victim_del_path, ec); // ignore any error
60+
}
61+
}};
62+
63+
// attacker (could of course be in a separate process)
64+
auto start_time = std::chrono::system_clock::now();
65+
auto elapsed_since = [](std::chrono::system_clock::time_point const& time_point) {
66+
return std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now() - time_point);
67+
};
68+
bool attack_succeeded = false;
69+
while (elapsed_since(start_time) < std::chrono::seconds(5)) {
70+
if (!fs::exists(attack_dest_file)) {
71+
std::printf("Victim deleted symlinked file outside of victim_del. Attack succeeded in %lld seconds.\n",
72+
elapsed_since(start_time).count());
73+
attack_succeeded = true;
74+
break;
75+
}
76+
std::error_code ec;
77+
fs::create_directory(victim_del_path, ec);
78+
if (ec) {
79+
continue;
80+
}
81+
82+
fs::remove(victim_del_path);
83+
fs::create_directory_symlink(attack_dest_dir, victim_del_path);
84+
}
85+
stop = true;
86+
t.join();
87+
88+
return attack_succeeded ? 1 : 0;
89+
}

0 commit comments

Comments
 (0)