Skip to content

[libc++] Fix filesystem::remove_all bug with read-only nested directory - #197104

Merged
ldionne merged 8 commits into
llvm:mainfrom
jakeegan:remove_all_bug
Jul 13, 2026
Merged

ldionne merged 8 commits into
llvm:mainfrom
jakeegan:remove_all_bug

Conversation

@jakeegan

Copy link
Copy Markdown
Member

Consider a directory parent/child, where parent has read-only permissions. Calling remove_all on parent is expected to fail with a permission error, however it fails with a different error depending on the platform (such as "error file exists" on AIX).

During the recursive removal, unlinkat is first called on the child and fails with a permission error. Rather than returning, the function continues and tries unlinkat on the parent and fails with a different error depending on the platform. This error is returned instead of the permission error. This PR makes the first encountered error be returned instead.

According to the standard [fs.op.remove.all]:

Effects: Recursively deletes the contents of p if it exists, then deletes file p itself, as if by POSIX remove.

The word "then" implies an ordering where deleting the contents of p should be successful before deleting file p itself.

Fixes #197100.

@jakeegan
jakeegan requested a review from a team as a code owner May 12, 2026 05:47
@llvmorg-github-actions llvmorg-github-actions Bot added the libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi. label May 12, 2026
@llvmorg-github-actions

Copy link
Copy Markdown

@llvm/pr-subscribers-libcxx

Author: Jake Egan (jakeegan)

Changes

Consider a directory parent/child, where parent has read-only permissions. Calling remove_all on parent is expected to fail with a permission error, however it fails with a different error depending on the platform (such as "error file exists" on AIX).

During the recursive removal, unlinkat is first called on the child and fails with a permission error. Rather than returning, the function continues and tries unlinkat on the parent and fails with a different error depending on the platform. This error is returned instead of the permission error. This PR makes the first encountered error be returned instead.

According to the standard [fs.op.remove.all]:
> Effects: Recursively deletes the contents of p if it exists, then deletes file p itself, as if by POSIX remove.

The word "then" implies an ordering where deleting the contents of p should be successful before deleting file p itself.

Fixes #197100.


Full diff: https://github.com/llvm/llvm-project/pull/197104.diff

1 Files Affected:

  • (modified) libcxx/src/filesystem/operations.cpp (+3)
diff --git a/libcxx/src/filesystem/operations.cpp b/libcxx/src/filesystem/operations.cpp
index 745db87ce3736..6afcec07b374d 100644
--- a/libcxx/src/filesystem/operations.cpp
+++ b/libcxx/src/filesystem/operations.cpp
@@ -904,6 +904,9 @@ uintmax_t remove_all_impl(int parent_directory, const path& p, error_code& ec) {
         break; // we're done iterating through the directory
       } else {
         count += remove_all_impl(fd, str, ec);
+        // If there's an error removing the child, return immediately to preserve the error code.
+        if (ec)
+          return count;
       }
     }
 

@hubert-reinterpretcast hubert-reinterpretcast left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a test.

@jakeegan

Copy link
Copy Markdown
Member Author

Do you have an idea what that test would look like? I didn't add a test because I'm not sure there's a way to verify this change without checking the specific error code that is returned, which can vary by platform. This change is ensuring the first encountered error is returned, but I don't think the standard mandates specifically what that error should be. I could add a test that checks any error is returned in this scenario, but it wouldn't verify this functional change.

@hubert-reinterpretcast

Copy link
Copy Markdown
Member

Do you have an idea what that test would look like?

Perhaps we can create a libcxx (not std) test marked as unsupported for Windows and make use of POSIX APIs and semantics?

We can expect std::errc::permission_denied to be associated with removing the child if the parent directory is readable and writable but not searchable (i.e., 0600).

@github-actions

github-actions Bot commented May 15, 2026 •

Copy link
Copy Markdown

✅ With the latest revision this PR passed the C/C++ code formatter.

@jakeegan

Copy link
Copy Markdown
Member Author

Remaining CI failures don't appear to be related

@jakeegan
jakeegan requested a review from philnik777 May 28, 2026 20:05

@hubert-reinterpretcast hubert-reinterpretcast left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@jakeegan
jakeegan requested a review from perry-ca June 4, 2026 14:15
@jakeegan
jakeegan requested a review from a team June 15, 2026 20:05
@jakeegan

jakeegan commented Jun 15, 2026 •

Copy link
Copy Markdown
Member Author

Ping (libc++ reviewers)

@jakeegan

Copy link
Copy Markdown
Member Author

Ping

@ldionne
ldionne merged commit c31455c into llvm:main Jul 13, 2026
80 checks passed
@jakeegan
jakeegan deleted the remove_all_bug branch July 13, 2026 13:59
pedroMVicente pushed a commit to pedroMVicente/llvm-project that referenced this pull request Jul 15, 2026
…ry (llvm#197104)

Consider a directory `parent/child`, where `parent` has read-only
permissions. Calling `remove_all` on `parent` is expected to fail with a
permission error, however it fails with a different error depending on
the platform (such as "error file exists" on AIX).

During the recursive removal, `unlinkat` is first called on the child
and fails with a permission error. Rather than returning, the function
continues and tries `unlinkat` on the parent and fails with a different
error depending on the platform. This error is returned instead of the
permission error. This PR makes the first encountered error be returned
instead.

According to the standard [fs.op.remove.all]:
> Effects: Recursively deletes the contents of p if it exists, then
> deletes file p itself, as if by POSIX remove.

The word "then" implies an ordering where deleting the contents of p
should be successful before deleting file p itself.

Fixes llvm#197100.
nodejs-github-bot pushed a commit to nodejs/node that referenced this pull request Sep 24, 2026
fs.rmSync() is implemented with std::filesystem::remove_all(). When a
read-only directory has a child, libc++ before LLVM 23 drops the
child's EACCES and reports the ENOTEMPTY it then gets for the parent
instead. The test pinned that behavior to macOS, but it depends on the
C++ standard library rather than on the OS: a Linux build against
libc++ fails the test when run as non-root, and macOS will start
reporting EACCES once it ships the fixed libc++. Accept either code
for that case on all POSIX platforms.

Refs: llvm/llvm-project#197104
Refs: #57103
Signed-off-by: Shelley Vohr <[email protected]>
PR-URL: #66065
Reviewed-By: Xuguang Mei <[email protected]>
aduh95 pushed a commit to nodejs/node that referenced this pull request Sep 27, 2026
fs.rmSync() is implemented with std::filesystem::remove_all(). When a
read-only directory has a child, libc++ before LLVM 23 drops the
child's EACCES and reports the ENOTEMPTY it then gets for the parent
instead. The test pinned that behavior to macOS, but it depends on the
C++ standard library rather than on the OS: a Linux build against
libc++ fails the test when run as non-root, and macOS will start
reporting EACCES once it ships the fixed libc++. Accept either code
for that case on all POSIX platforms.

Refs: llvm/llvm-project#197104
Refs: #57103
Signed-off-by: Shelley Vohr <[email protected]>
PR-URL: #66065
Reviewed-By: Xuguang Mei <[email protected]>
aduh95 pushed a commit to nodejs/node that referenced this pull request Oct 23, 2026
fs.rmSync() is implemented with std::filesystem::remove_all(). When a
read-only directory has a child, libc++ before LLVM 23 drops the
child's EACCES and reports the ENOTEMPTY it then gets for the parent
instead. The test pinned that behavior to macOS, but it depends on the
C++ standard library rather than on the OS: a Linux build against
libc++ fails the test when run as non-root, and macOS will start
reporting EACCES once it ships the fixed libc++. Accept either code
for that case on all POSIX platforms.

Refs: llvm/llvm-project#197104
Refs: #57103
Signed-off-by: Shelley Vohr <[email protected]>
PR-URL: #66065
Reviewed-By: Xuguang Mei <[email protected]>
aduh95 pushed a commit to nodejs/node that referenced this pull request Oct 28, 2026
fs.rmSync() is implemented with std::filesystem::remove_all(). When a
read-only directory has a child, libc++ before LLVM 23 drops the
child's EACCES and reports the ENOTEMPTY it then gets for the parent
instead. The test pinned that behavior to macOS, but it depends on the
C++ standard library rather than on the OS: a Linux build against
libc++ fails the test when run as non-root, and macOS will start
reporting EACCES once it ships the fixed libc++. Accept either code
for that case on all POSIX platforms.

Refs: llvm/llvm-project#197104
Refs: #57103
Signed-off-by: Shelley Vohr <[email protected]>
PR-URL: #66065
Reviewed-By: Xuguang Mei <[email protected]>
aduh95 pushed a commit to nodejs/node that referenced this pull request Nov 11, 2026
fs.rmSync() is implemented with std::filesystem::remove_all(). When a
read-only directory has a child, libc++ before LLVM 23 drops the
child's EACCES and reports the ENOTEMPTY it then gets for the parent
instead. The test pinned that behavior to macOS, but it depends on the
C++ standard library rather than on the OS: a Linux build against
libc++ fails the test when run as non-root, and macOS will start
reporting EACCES once it ships the fixed libc++. Accept either code
for that case on all POSIX platforms.

Refs: llvm/llvm-project#197104
Refs: #57103
Signed-off-by: Shelley Vohr <[email protected]>
PR-URL: #66065
Reviewed-By: Xuguang Mei <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[libc++] filesystem::remove_all returns wrong error code on a read-only nested directory

4 participants