Skip to content

AIX: clang libc++ std::filesystem::remove_all() Returns Wrong Error Code #62790

@abmusse

Description

@abmusse

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:

  1. Root users bypass permission checks, so no EACCES error occurs
  2. Non-root users encounter permission errors when trying to remove read-only directories
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    aixIssues and PRs related to the AIX platform.fsIssues and PRs related to the fs subsystem / file system.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions