Skip to content

Comments

lib: normalize . and .. in path before rm/rmSync#61968

Open
RajeshKumar11 wants to merge 1 commit intonodejs:mainfrom
RajeshKumar11:fix/61958-rm-path-normalize
Open

lib: normalize . and .. in path before rm/rmSync#61968
RajeshKumar11 wants to merge 1 commit intonodejs:mainfrom
RajeshKumar11:fix/61958-rm-path-normalize

Conversation

@RajeshKumar11
Copy link
Contributor

Summary

  • fs.rmSync, fs.rm, and fs.promises.rm now call path.normalize() on string paths before passing them to either the C++ binding (rmSync) or the JS rimraf implementation (rm/promises.rm)
  • This makes all three variants behave consistently when the path contains . or .. components

Root cause

Two different code paths handle rm in Node.js:

  • rmSync (C++ — src/node_file.cc:1661): calls std::filesystem::remove_all(file_path).
    With file_path = "a/b/../.", C++ deletes the contents of a (by resolving the path through the kernel), but when it then tries to rmdir("a/b/../.") to remove the top-level entry, the b component was already deleted, so the path can't be resolved → ENOENT → treated as success → a/ is left behind.

  • rm / promises.rm (JS rimraf — lib/internal/fs/rimraf.js): calls rmdir(2) on the original path first.
    On POSIX, rmdir("a/b/../.") returns EINVAL (trailing . is an invalid argument for rmdir). EINVAL is not handled by rimraf's retry logic → the operation fails immediately without removing anything.

Neither behaviour is correct, and they disagree with each other.

Fix

Apply path.normalize() to string paths right after getValidatedPath() in all three entry points:

File Line Change
lib/fs.js rm() if (typeof path === 'string') path = pathModule.normalize(path)
lib/fs.js rmSync() same, before both validation and binding call
lib/internal/fs/promises.js rm() same

path.normalize('a/b/../.')'a', which both rimraf and the C++ binding handle without issue. Buffer paths are left unchanged (they are an edge case and path.normalize only accepts strings).

Test

Three new test cases added to test/parallel/test-fs-rm.js:

  • fs.rmSync with a ../. path removes the target directory completely
  • fs.rm (callback) with the same path
  • fs.promises.rm with the same path

Each case creates <base>/a/b/c/d, applies rm to <base>/a/b/../. (which should resolve to <base>/a), and asserts that <base>/a no longer exists.

Related

  • Ref: isaacs/rimraf — reporter notes a matching fix can also be applied upstream

Fixes: #61958

@nodejs-github-bot nodejs-github-bot added fs Issues and PRs related to the fs subsystem / file system. needs-ci PRs that need a full CI run. labels Feb 24, 2026
When fs.rm, fs.rmSync, or fs.promises.rm receive a path containing
'.' or '..' components (e.g. 'a/b/../.'), the sync and async
implementations behaved differently:

- rmSync (C++ binding): std::filesystem::remove_all deleted the
  directory's children, but when trying to remove the top-level
  entry the path became invalid (the '..' referred to a now-deleted
  directory), causing the parent directory to be left behind.

- promises.rm / rm (JS rimraf): rmdir(2) on a path ending in '.'
  returns EINVAL on POSIX, so the operation failed immediately
  without removing anything.

Fix by calling path.normalize() on string paths immediately after
getValidatedPath(), before the path is passed to either rimraf or
the C++ binding. This resolves '.' and '..' lexically (e.g.
'a/b/../.' becomes 'a'), giving both code paths a clean,
unambiguous path to operate on.

Fixes: nodejs#61958
@RajeshKumar11 RajeshKumar11 force-pushed the fix/61958-rm-path-normalize branch from cd665e4 to 887b2c8 Compare February 24, 2026 17:05
@codecov
Copy link

codecov bot commented Feb 24, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.76%. Comparing base (da5efc4) to head (887b2c8).
⚠️ Report is 14 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #61968      +/-   ##
==========================================
+ Coverage   88.84%   89.76%   +0.92%     
==========================================
  Files         674      674              
  Lines      204957   205609     +652     
  Branches    39309    39419     +110     
==========================================
+ Hits       182087   184563    +2476     
+ Misses      15088    13268    -1820     
+ Partials     7782     7778       -4     
Files with missing lines Coverage Δ
lib/fs.js 98.19% <100.00%> (+4.85%) ⬆️
lib/internal/fs/promises.js 98.73% <100.00%> (+0.67%) ⬆️

... and 177 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fs Issues and PRs related to the fs subsystem / file system. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Discrepancy between fs.rmSync and fs/promises.rm behavior with . and .. in paths

2 participants