[libcxx-commits] [PATCH] D118134: [libc++] Fix TOCTOU issue with std::filesystem::remove_all

Arthur O'Dwyer via Phabricator via libcxx-commits libcxx-commits at lists.llvm.org
Tue Jan 25 18:11:56 PST 2022


Quuxplusone added inline comments.


================
Comment at: libcxx/src/filesystem/directory_iterator.cpp:77
-#else
-// defined(_LIBCPP_WIN32API)
 
----------------
I wonder if it would be simpler to just move `remove_all` into //this// .cpp file. A priori (without seeing that this diff is where they came from) it's weird to see static functions in a .h file.


================
Comment at: libcxx/src/filesystem/filesystem_common.h:570
+  ec.clear();
+  if ((dir_entry_ptr = ::readdir(dir_stream)) == nullptr) {
+    if (errno)
----------------
Seems like a perfect place for `if (struct dirent *dir_entry_ptr = ::readdir(dir_stream)) { ...` (and swap the if/else branches).

Btw, for anyone else who's confused why we aren't using `::readdir_r`, apparently `::readdir` is equally thread-safe on modern systems (at least, those modern systems documented by LWN ;)) https://lwn.net/Articles/696474/ We should expect that `dir_entry_ptr` will point to some memory located physically inside the footprint of `*dir_stream` (as opposed to some static buffer or something).

However, I now also see that this code simply moved from `directory_iterator.cpp` (and have suggested that maybe `remove_all` should move over //there// instead), in which case there's no need to drive-by refactor any of this particular function.


================
Comment at: libcxx/src/filesystem/operations.cpp:1351
+
+  ~scope_exit() { cleanup_(); }
+
----------------
If anything below were actually to throw an //exception//, then our `count` would be wrong. But this simplifies the early-return cases even in the absence of exceptions, so I assume that's why you did it.


================
Comment at: libcxx/src/filesystem/operations.cpp:1366
+    DIR* stream = ::fdopendir(fd);
+    scope_exit close_stream([stream] { if (stream != nullptr) ::closedir(stream); });
+    if (stream == nullptr) {
----------------
It would seem simpler to move this down below the `if`, and get rid of the `if (stream != nullptr)` inside the lambda. (Also personally I'd capture `[&]` rather than `[stream]`, because nothing weird is going on here.)
Analogous comments //may// apply to `fd` above.


================
Comment at: libcxx/src/filesystem/operations.cpp:1372
+
+    intmax_t count = 0;
+    while (true) {
----------------
Any reason for `intmax_t` here but `uintmax_t` in the function return type? I suggest consistency, but don't care which.


================
Comment at: libcxx/src/filesystem/operations.cpp:1374
+    while (true) {
+      auto [str, type] = detail::posix_readdir(stream, ec);
+      if (str == "." || str == "..") {
----------------
Consider a documentary `static_assert(std::is_same_v<decltype(str), std::string_view>` here, because comparing `auto` to a string literal smells like it //might// be a pointer comparison, and that would suck.


================
Comment at: libcxx/src/filesystem/operations.cpp:1385
+    // Then, remove the directory itself.
+    if (::unlinkat(parent_directory, p.c_str(), AT_REMOVEDIR) == -1) {
+      ec = detail::capture_errno();
----------------
FWIW, this line is still racey:
https://stackoverflow.com/questions/28517236/can-posix-linux-unlink-file-entries-completely-race-free
https://bugzilla.kernel.org/show_bug.cgi?id=93441
The primitive that we need here is "remove-`parent_directory`'s-child-named-`p`-iff-it-still-refers-to-the-same-inode-as-`fd`", i.e., a sort of compare-exchange primitive, which Linux does not provide and which is impossible to emulate in userspace.

So, this line right here is already the state of the art, and I can't think of anything better to do than to be OK with it.

Also, at first glance I can't see any way to really "exploit" this race. Either (happy path) you remove the now-empty directory you intended to; or the attacker moves that now-empty directory out of the way and puts their own thing in its place which you fail to delete (because it's not a directory, or because it's a non-empty directory); or the attacker moves that now-empty directory out of the way and puts their own empty directory in its place and you delete it (but it was an empty directory, and it wasn't a symlink (because it was a directory), so who cares).
Also, we will //always// have this same race on the front side of the transaction: the programmer gives us a `fs::path` and we start removing whatever's there //now//, not whatever //was// there when the programmer decided to call into us.


================
Comment at: libcxx/src/filesystem/operations.cpp:1416
   }
-  if (!__remove(p, &ec))
-    return npos;
-  return count;
 }
 
----------------
Style: For this if-else ladder, I'd prefer either what-you've-got-minus-all-the-`else`-keywords, or
```
if (ec == errc::no_such_file_or_directory) {
  // Not an error; `p` might have moved or been deleted already.
  ec.clear();
  return 0;
} else if (ec == errc::not_a_directory) {
  // Remove `p` as a normal file instead.
  ec.clear();
  ~~~
```
i.e., put the commentary for each branch //on// the branch, and cuddle the elses as usual.


================
Comment at: libcxx/test/std/input.output/filesystems/fs.op.funcs/fs.op.remove_all/toctou.pass.cpp:37-38
+int main() {
+  fs::path tmpdir = "/tmp/mydir";
+  fs::path victim_del_path = tmpdir / "victim_del";
+
----------------
Running this test deletes whatever I have in `/tmp/mydir/victim_del`? I suggest not committing this as-is. 😛
Looks like the existing tests use things like `env.create_dir` and `env.create_file` to avoid messing with the user's own files.


================
Comment at: libcxx/test/std/input.output/filesystems/fs.op.funcs/fs.op.remove_all/toctou.pass.cpp:78
+    fs::create_directory_symlink(attack_dest_dir, victim_del_path);
+  }
+  stop = true;
----------------
(If this is intended for commit at all) Consider adding another test for the subdirectory case I brought up in my previous review.


Repository:
  rG LLVM Github Monorepo

CHANGES SINCE LAST ACTION
  https://reviews.llvm.org/D118134/new/

https://reviews.llvm.org/D118134



More information about the libcxx-commits mailing list