Repository: nodejs/node
Pull request: Fix/fs rmsync broken symlink 61020

 ## Summary

  Fix `fs.rmSync()` so it correctly removes broken symbolic links (and symlinks to
  directories) instead of becoming a no-op or throwing `ERR_FS_EISDIR`.

  Fixes: https://github.com/nodejs/node/issues/61020

  ## Details

  `rmSync` was using `std::filesystem::status()`, which follows symlinks. For
  broken symlinks this reports `not_found`, causing `rmSync` to return early
  without unlinking the symlink itself. Switching to `std::filesystem::symlink_status()`
  makes `rmSync` operate on the symlink entry, matching `fs.rm()` behavior.

  ## Tests

  - `./node test/parallel/test-fs-rm.js`

Changed files:
filename: src/node_file.cc
patch: @@ -1636,7 +1636,7 @@ static void RmSync(const FunctionCallbackInfo<Value>& args) {
       env, permission::PermissionScope::kFileSystemWrite, path.ToStringView());
   auto file_path = std::filesystem::path(path.ToStringView());
   std::error_code error;
-  auto file_status = std::filesystem::status(file_path, error);
+  auto file_status = std::filesystem::symlink_status(file_path, error);
 
   if (file_status.type() == std::filesystem::file_type::not_found) {
     return;

filename: test/parallel/test-fs-rm.js
patch: @@ -165,10 +165,13 @@ function removeAsync(dir) {
   // Should delete an invalid symlink
   const invalidLink = tmpdir.resolve('invalid-link-async');
   fs.symlinkSync('definitely-does-not-exist-async', invalidLink);
+  assert.ok(fs.lstatSync(invalidLink).isSymbolicLink());
+  // `existsSync()` follows symlinks, so this confirms the target does not exist.
+  assert.strictEqual(fs.existsSync(invalidLink), false);
   fs.rm(invalidLink, common.mustNotMutateObjectDeep({ recursive: true }), common.mustCall((err) => {
     try {
       assert.strictEqual(err, null);
-      assert.strictEqual(fs.existsSync(invalidLink), false);
+      assert.throws(() => fs.lstatSync(invalidLink), { code: 'ENOENT' });
     } finally {
       fs.rmSync(invalidLink, common.mustNotMutateObjectDeep({ force: true }));
     }
@@ -247,11 +250,14 @@ if (isGitPresent) {
   }
 
   // Should delete an invalid symlink
+  // Refs: https://github.com/nodejs/node/issues/61020
   const invalidLink = tmpdir.resolve('invalid-link');
   fs.symlinkSync('definitely-does-not-exist', invalidLink);
+  assert.ok(fs.lstatSync(invalidLink).isSymbolicLink());
+  assert.strictEqual(fs.existsSync(invalidLink), false);
   try {
     fs.rmSync(invalidLink);
-    assert.strictEqual(fs.existsSync(invalidLink), false);
+    assert.throws(() => fs.lstatSync(invalidLink), { code: 'ENOENT' });
   } finally {
     fs.rmSync(invalidLink, common.mustNotMutateObjectDeep({ force: true }));
   }
@@ -355,9 +361,11 @@ if (isGitPresent) {
   // Should delete an invalid symlink
   const invalidLink = tmpdir.resolve('invalid-link-prom');
   fs.symlinkSync('definitely-does-not-exist-prom', invalidLink);
+  assert.ok(fs.lstatSync(invalidLink).isSymbolicLink());
+  assert.strictEqual(fs.existsSync(invalidLink), false);
   try {
     await fs.promises.rm(invalidLink);
-    assert.strictEqual(fs.existsSync(invalidLink), false);
+    assert.throws(() => fs.lstatSync(invalidLink), { code: 'ENOENT' });
   } finally {
     fs.rmSync(invalidLink, common.mustNotMutateObjectDeep({ force: true }));
   }