diff --git a/lib/internal/fs/rimraf.js b/lib/internal/fs/rimraf.js index 181150f2b7e492..fad208aea61b57 100644 --- a/lib/internal/fs/rimraf.js +++ b/lib/internal/fs/rimraf.js @@ -24,7 +24,9 @@ const { const { sep } = require('path'); const { setTimeout } = require('timers'); const { isWindows } = require('internal/util'); -const notEmptyErrorCodes = new SafeSet(['ENOTEMPTY', 'EEXIST', 'EPERM']); +const notEmptyErrorCodes = isWindows ? + new SafeSet(['ENOTEMPTY', 'EEXIST']) : + new SafeSet(['ENOTEMPTY', 'EEXIST', 'EPERM']); const retryErrorCodes = new SafeSet( ['EBUSY', 'EMFILE', 'ENFILE', 'ENOTEMPTY', 'EPERM']); const epermHandler = isWindows ? fixWinEPERM : _rmdir; diff --git a/test/parallel/test-fs-rm.js b/test/parallel/test-fs-rm.js index 4104c2e948da0e..e92bf8c079715f 100644 --- a/test/parallel/test-fs-rm.js +++ b/test/parallel/test-fs-rm.js @@ -567,5 +567,67 @@ if (isGitPresent) { makeDirectoryWritable(middle); } } + + if (common.isWindows) { + // On Windows, EPERM from rmdir on a directory that cannot be deleted + // due to permissions must not be treated as ENOTEMPTY (which would + // cause rimraf to recurse into and delete the directory's children). + const dirname = nextDirPath(); + const parent = path.join(dirname, 'parent'); + const child = path.join(parent, 'child'); + const childFile = path.join(child, 'childFile.txt'); + fs.mkdirSync(child, common.mustNotMutateObjectDeep({ recursive: true })); + fs.writeFileSync(childFile, 'hello'); + + // (DC) denies deleting children; (DE) denies deleting the directory + // itself. Combined denies on each layer guarantee rmdir returns EPERM. + execSync(`icacls "${dirname}" /deny "everyone:(DC)"`); + execSync(`icacls "${parent}" /deny "everyone:(DE,DC)"`); + execSync(`icacls "${child}" /deny "everyone:(DE)"`); + + const cleanup = () => { + try { + execSync(`icacls "${child}" /remove:d "everyone"`); + } catch { + // Best-effort cleanup; ignore failures (e.g. already cleared). + } + try { + execSync(`icacls "${parent}" /remove:d "everyone"`); + } catch { + // Best-effort cleanup; ignore failures. + } + try { + execSync(`icacls "${dirname}" /remove:d "everyone"`); + } catch { + // Best-effort cleanup; ignore failures. + } + try { + fs.rmSync(dirname, common.mustNotMutateObjectDeep({ + recursive: true, + force: true, + })); + } catch { + // Best-effort cleanup; ignore failures. + } + }; + process.on('exit', cleanup); + + fs.rm(dirname, common.mustNotMutateObjectDeep({ recursive: true }), + common.mustCall((err) => { + try { + assert.ok(err, 'expected EPERM error'); + assert.strictEqual(err.code, 'EPERM'); + assert.strictEqual(err.syscall, 'rmdir'); + assert.ok(err.path.endsWith('\\parent')); + assert.ok( + fs.existsSync(child), + 'EPERM from rmdir must propagate without recursing into children', + ); + } finally { + process.removeListener('exit', cleanup); + cleanup(); + } + })); + } } }