nodejs / nodejs/node

fs: rmSync() reports UV_UNKNOWN with an empty code for unmapped errno values

Open
#65,884 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
JavaScript
Stars
122k
Forks
37.3k
Avg merge
4d 2h
Merged PRs (30d)
283

Description

Version

v26.7.0

Platform

Linux x64

Subsystem

fs / src/node_file.cc

What steps will reproduce the bug?
const fs = require('node:fs');
fs.mkdirSync('a/b', { recursive: true });
try {
  fs.rmSync('a/.', { recursive: true, force: true });
} catch (err) {
  console.log('code   :', JSON.stringify(err.code));
  console.log('errno  :', err.errno);
  console.log('syscall:', err.syscall);
  console.log('message:', err.message);
}

Output:

code   : ""
errno  : -4094
syscall: "rm"
message: , Unknown error: Invalid argument 'a/.'
What is the expected behavior? Why is that the expected behavior?

err.code should be 'EINVAL' and err.errno should be -22. The underlying
failure is a plain EINVAL from rmdir(2), which POSIX requires for a path whose
final component is ..

Every other fs error carries a usable code, and err.code === 'EINVAL' is the
documented way to branch on fs failures, so an empty string breaks ordinary error
handling. The message is also malformed: it begins with , because the empty code
is interpolated into it.

What do you see instead?

errno is -4094 (UV_UNKNOWN) and code is the empty string.

Additional information

This is not specific to EINVAL. RmSync() in src/node_file.cc translates the
std::error_code from std::filesystem::remove_all() with a hardcoded chain that
covers four values:

  • operation_not_permitted -> EPERM
  • directory_not_empty -> ENOTEMPTY
  • not_a_directory -> ENOTDIR
  • permission_denied -> EACCES (EPERM on Windows)

Anything else falls through to the tail of the function:

std::string message = "Unknown error: " + error.message();
return env->ThrowErrnoException(
    UV_UNKNOWN, "rm", message.c_str(), path_c_str);

So the mapping is an allowlist rather than a translation, and any errno outside
those four surfaces as UV_UNKNOWN with an empty code. EINVAL is simply the
one that is easiest to trigger from user code.

The async form is unaffected, because fs.rm() and fsPromises.rm() use the JS
rimraf and report EINVAL correctly.

Noting for context that the dot segment handling in #61958 is being addressed
separately at the JS layer. That change stops a/. from reaching this code path,
but it does not fix the mapping, which stays reachable for other errnos.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Read src/node_file.cc at RmSync() and trace how the std::error_code from std::filesystem::remove_all() becomes a Node.js error. Add regression coverage for an unmapped errno such as EINVAL, verifying the returned code, errno, and message are usable rather than UV_UNKNOWN with an empty code.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, javascript, nodejs
Domain
operating-systems
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.