Summary
On AIX with clang/libc++, std::filesystem::remove_all() incorrectly returns error code 17 (EEXIST - "File exists") instead of 13 (EACCES - "Permission denied") when encountering permission errors during recursive directory removal. This doesn't happen using gcc/libstdc++ on the same machine.
Note: This bug only manifests when running as a non-root user. When run as root, the operation succeeds (no permission error occurs), so the bug is not triggered.
Environment
AIX (Bug Present)
- OS: AIX 7.2
- Compiler: clang version 20.1.7 (powerpc-ibm-aix7.2.0.0)
- Target: powerpc-ibm-aix7.2.0.0
- Standard Library: libc++
AIX with GCC (Correct Behavior)
- OS: AIX 7.2
- Compiler: GCC 12.3.0
- Standard Library: libstdc++
- Result: Bug does NOT occur with GCC on AIX
Also tested behavior on Linux:
Linux (Correct Behavior)
- OS: Fedora 43
- Compiler: clang version 20.1.8 (Fedora 20.1.8-2.fc43)
- Target: x86_64-redhat-linux-gnu
- Standard Library: libc++
Expected Behavior
When std::filesystem::remove_all() fails due to insufficient permissions:
error.value() should be 13 (EACCES)
error.message() should be "Permission denied"
error == std::errc::permission_denied should be true
Actual Behavior on AIX
error.value() is 17 (EEXIST)
error.message() is "File exists"
error == std::errc::permission_denied is false
Note: This bug only manifests when running as a non-root user. When run as root, the operation succeeds (no permission error occurs), so the bug is not triggered.
Impact
This bug causes applications to misinterpret permission errors as "file exists" errors, leading to incorrect error handling.
The Node.js test suite fails on AIX with clang due to this bug:
Test: parallel/test-fs-rm (test case 1918)
Failure:
not ok 1918 parallel/test-fs-rm
AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
+ actual - expected
Comparison {
+ code: '',
- code: 'EACCES',
name: 'Error'
}
actual: Error: , Unknown error: File exists '/home/iojs/build/.tmp.1724/rm-11/fs-KFNaea'
at Object.rmSync (node:fs:1206:18)
...
{
errno: -4094,
code: '',
path: '/home/iojs/build/.tmp.1724/rm-11/fs-KFNaea',
syscall: 'rm'
},
expected: { code: 'EACCES', name: 'Error' }
ref: https://ci.nodejs.org/job/node-test-commit-aix-abmusse/21/nodes=test-ibm-aix72-ppc64_be-2/consoleText
Reproducer
Test Program
Save the following as test-clang-fs-removeall-bug.cpp:
/*
* Bug: std::filesystem::remove_all() returns EEXIST instead of EACCES on AIX
*
* Expected: error.value() == 13 (EACCES - Permission denied)
* Actual on AIX: error.value() == 17 (EEXIST - File exists)
*/
#include <iostream>
#include <filesystem>
#include <system_error>
#include <sys/stat.h>
#include <errno.h>
int main() {
// Create directory structure: root/middle/leaf
std::filesystem::create_directories("root/middle/leaf");
// Make middle read-only to trigger permission error
chmod("root/middle", 0555);
// Try to remove_all (should fail with permission error)
std::error_code error;
std::filesystem::remove_all("root", error);
// Report results
std::cout << "EACCES = " << EACCES << std::endl;
std::cout << "EEXIST = " << EEXIST << std::endl;
std::cout << "error.value() = " << error.value() << std::endl;
std::cout << "error.message() = \"" << error.message() << "\"" << std::endl;
std::cout << "error == std::errc::permission_denied: "
<< (error == std::errc::permission_denied ? "YES" : "NO") << std::endl;
// Cleanup
chmod("root/middle", 0755);
std::filesystem::remove_all("root");
return error.value() == EEXIST ? 1 : 0; // Return 1 if bug is present
}
Compile and Run
On AIX (as non-root user):
/opt/clang+llvm-20.1.7-powerpc64-ibm-aix-7.2/bin/clang++ -o test-clang-fs-removeall-bug.out test-clang-fs-removeall-bug.cpp
./test-clang-fs-removeall-bug.out
On Linux (as non-root user):
clang++ -o test-clang-fs-removeall-bug.out test-clang-fs-removeall-bug.cpp
./test-clang-fs-removeall-bug.out
Note: Must run as non-root user to trigger the permission error.
Output on Linux (clang 20.1.8 - Correct Behavior)
EACCES = 13
EEXIST = 17
error.value() = 13
error.message() = "Permission denied"
error == std::errc::permission_denied: YES
Output on AIX (clang 20.1.7 - Bug Present)
EACCES = 13
EEXIST = 17
error.value() = 17
error.message() = "File exists"
error == std::errc::permission_denied: NO
Verification: This bug has been confirmed across multiple compilers and platforms:
- AIX with clang 20.1.7 + libc++: Bug present (returns EEXIST instead of EACCES)
- Linux with clang 20.1.8 + libc++: Bug NOT present (returns EACCES correctly)
- AIX with GCC 12.3.0 + libstdc++: Bug NOT present (returns EACCES correctly)
This confirms the bug is specific to clang's libc++ implementation on AIX, not a general AIX filesystem issue or a general clang/libc++ bug. The same AIX system works correctly with GCC's libstdc++.
Root Cause Analysis
The bug appears to be in the AIX-specific implementation of std::filesystem::remove_all() in libc++. When the underlying system call (likely rmdir() or unlink()) fails with errno = 13 (EACCES), the error is being incorrectly translated to std::error_code with value 17 (EEXIST).
This issue only occurs when running as a non-root user because:
- Root users bypass permission checks, so no EACCES error occurs
- Non-root users encounter permission errors when trying to remove read-only directories
- The bug in error code translation only manifests when EACCES is actually returned by the system
Workaround
@richardlau created a patch in Node.js to workaround this.
ref: #62788
CC
@nodejs/platform-aix
We need to file this report to AIX compilers team too.
Summary
On AIX with clang/libc++,
std::filesystem::remove_all()incorrectly returns error code 17 (EEXIST - "File exists") instead of 13 (EACCES - "Permission denied") when encountering permission errors during recursive directory removal. This doesn't happen using gcc/libstdc++ on the same machine.Note: This bug only manifests when running as a non-root user. When run as root, the operation succeeds (no permission error occurs), so the bug is not triggered.
Environment
AIX (Bug Present)
AIX with GCC (Correct Behavior)
Also tested behavior on Linux:
Linux (Correct Behavior)
Expected Behavior
When
std::filesystem::remove_all()fails due to insufficient permissions:error.value()should be 13 (EACCES)error.message()should be "Permission denied"error == std::errc::permission_deniedshould be trueActual Behavior on AIX
error.value()is 17 (EEXIST)error.message()is "File exists"error == std::errc::permission_deniedis falseNote: This bug only manifests when running as a non-root user. When run as root, the operation succeeds (no permission error occurs), so the bug is not triggered.
Impact
This bug causes applications to misinterpret permission errors as "file exists" errors, leading to incorrect error handling.
The Node.js test suite fails on AIX with clang due to this bug:
Test:
parallel/test-fs-rm(test case 1918)Failure:
ref: https://ci.nodejs.org/job/node-test-commit-aix-abmusse/21/nodes=test-ibm-aix72-ppc64_be-2/consoleText
Reproducer
Test Program
Save the following as
test-clang-fs-removeall-bug.cpp:Compile and Run
On AIX (as non-root user):
On Linux (as non-root user):
Note: Must run as non-root user to trigger the permission error.
Output on Linux (clang 20.1.8 - Correct Behavior)
Output on AIX (clang 20.1.7 - Bug Present)
Verification: This bug has been confirmed across multiple compilers and platforms:
This confirms the bug is specific to clang's libc++ implementation on AIX, not a general AIX filesystem issue or a general clang/libc++ bug. The same AIX system works correctly with GCC's libstdc++.
Root Cause Analysis
The bug appears to be in the AIX-specific implementation of
std::filesystem::remove_all()in libc++. When the underlying system call (likelyrmdir()orunlink()) fails witherrno = 13(EACCES), the error is being incorrectly translated tostd::error_codewith value 17 (EEXIST).This issue only occurs when running as a non-root user because:
Workaround
@richardlau created a patch in Node.js to workaround this.
ref: #62788
CC
@nodejs/platform-aix
We need to file this report to AIX compilers team too.