Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion lib/internal/fs/rimraf.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
62 changes: 62 additions & 0 deletions test/parallel/test-fs-rm.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}));
}
}
}