Skip to content

Commit 8455a62

Browse files
committed
fs: fix fs.rm support for loop symlinks
Fixes: #45404
1 parent 4ac830e commit 8455a62

File tree

2 files changed

+124
-7
lines changed

2 files changed

+124
-7
lines changed

lib/internal/fs/utils.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -769,7 +769,7 @@ const validateRmOptions = hideStackFrames((path, options, expectDir, cb) => {
769769
options = validateRmdirOptions(options, defaultRmOptions);
770770
validateBoolean(options.force, 'options.force');
771771

772-
lazyLoadFs().stat(path, (err, stats) => {
772+
lazyLoadFs().lstat(path, (err, stats) => {
773773
if (err) {
774774
if (options.force && err.code === 'ENOENT') {
775775
return cb(null, options);
@@ -800,7 +800,7 @@ const validateRmOptionsSync = hideStackFrames((path, options, expectDir) => {
800800

801801
if (!options.force || expectDir || !options.recursive) {
802802
const isDirectory = lazyLoadFs()
803-
.statSync(path, { throwIfNoEntry: !options.force })?.isDirectory();
803+
.lstatSync(path, { throwIfNoEntry: !options.force })?.isDirectory();
804804

805805
if (expectDir && !isDirectory) {
806806
return false;

test/parallel/test-fs-rm.js

+122-5
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ function makeNonEmptyDirectory(depth, files, folders, dirname, createSymLinks) {
4949
path.join(dirname, `link-${depth}-bad`),
5050
'file'
5151
);
52+
53+
// Symlinks that form a loop
54+
[['a', 'b'], ['b', 'a']].forEach(([x, y]) => {
55+
fs.symlinkSync(
56+
`link-${depth}-loop-${x}`,
57+
path.join(dirname, `link-${depth}-loop-${y}`),
58+
'file'
59+
);
60+
});
5261
}
5362

5463
// File with a name that looks like a glob
@@ -88,7 +97,7 @@ function removeAsync(dir) {
8897

8998
// Attempted removal should fail now because the directory is gone.
9099
fs.rm(dir, common.mustCall((err) => {
91-
assert.strictEqual(err.syscall, 'stat');
100+
assert.strictEqual(err.syscall, 'lstat');
92101
}));
93102
}));
94103
}));
@@ -137,6 +146,48 @@ function removeAsync(dir) {
137146
fs.rmSync(filePath, common.mustNotMutateObjectDeep({ force: true }));
138147
}
139148
}));
149+
150+
// Should delete a valid symlink
151+
const linkTarget = path.join(tmpdir.path, 'link-target-async.txt');
152+
fs.writeFileSync(linkTarget, '');
153+
const validLink = path.join(tmpdir.path, 'valid-link-async');
154+
fs.symlinkSync(linkTarget, validLink);
155+
fs.rm(validLink, common.mustNotMutateObjectDeep({ recursive: true }), common.mustCall((err) => {
156+
try {
157+
assert.strictEqual(err, null);
158+
assert.strictEqual(fs.existsSync(validLink), false);
159+
} finally {
160+
fs.rmSync(linkTarget, common.mustNotMutateObjectDeep({ force: true }));
161+
fs.rmSync(validLink, common.mustNotMutateObjectDeep({ force: true }));
162+
}
163+
}));
164+
165+
// Should delete an invalid symlink
166+
const invalidLink = path.join(tmpdir.path, 'invalid-link-async');
167+
fs.symlinkSync('definitely-does-not-exist-async', invalidLink);
168+
fs.rm(invalidLink, common.mustNotMutateObjectDeep({ recursive: true }), common.mustCall((err) => {
169+
try {
170+
assert.strictEqual(err, null);
171+
assert.strictEqual(fs.existsSync(invalidLink), false);
172+
} finally {
173+
fs.rmSync(invalidLink, common.mustNotMutateObjectDeep({ force: true }));
174+
}
175+
}));
176+
177+
// Should delete a symlink that is part of a loop
178+
const loopLinkA = path.join(tmpdir.path, 'loop-link-async-a');
179+
const loopLinkB = path.join(tmpdir.path, 'loop-link-async-b');
180+
fs.symlinkSync(loopLinkA, loopLinkB);
181+
fs.symlinkSync(loopLinkB, loopLinkA);
182+
fs.rm(loopLinkA, common.mustNotMutateObjectDeep({ recursive: true }), common.mustCall((err) => {
183+
try {
184+
assert.strictEqual(err, null);
185+
assert.strictEqual(fs.existsSync(loopLinkA), false);
186+
} finally {
187+
fs.rmSync(loopLinkA, common.mustNotMutateObjectDeep({ force: true }));
188+
fs.rmSync(loopLinkB, common.mustNotMutateObjectDeep({ force: true }));
189+
}
190+
}));
140191
}
141192

142193
// Removing a .git directory should not throw an EPERM.
@@ -168,7 +219,7 @@ if (isGitPresent) {
168219
}, {
169220
code: 'ENOENT',
170221
name: 'Error',
171-
message: /^ENOENT: no such file or directory, stat/
222+
message: /^ENOENT: no such file or directory, lstat/
172223
});
173224

174225
// Should delete a file
@@ -181,6 +232,39 @@ if (isGitPresent) {
181232
fs.rmSync(filePath, common.mustNotMutateObjectDeep({ force: true }));
182233
}
183234

235+
// Should delete a valid symlink
236+
const linkTarget = path.join(tmpdir.path, 'link-target.txt');
237+
fs.writeFileSync(linkTarget, '');
238+
const validLink = path.join(tmpdir.path, 'valid-link');
239+
fs.symlinkSync(linkTarget, validLink);
240+
try {
241+
fs.rmSync(validLink);
242+
} finally {
243+
fs.rmSync(linkTarget, common.mustNotMutateObjectDeep({ force: true }));
244+
fs.rmSync(validLink, common.mustNotMutateObjectDeep({ force: true }));
245+
}
246+
247+
// Should delete an invalid symlink
248+
const invalidLink = path.join(tmpdir.path, 'invalid-link');
249+
fs.symlinkSync('definitely-does-not-exist', invalidLink);
250+
try {
251+
fs.rmSync(invalidLink);
252+
} finally {
253+
fs.rmSync(invalidLink, common.mustNotMutateObjectDeep({ force: true }));
254+
}
255+
256+
// Should delete a symlink that is part of a loop
257+
const loopLinkA = path.join(tmpdir.path, 'loop-link-a');
258+
const loopLinkB = path.join(tmpdir.path, 'loop-link-b');
259+
fs.symlinkSync(loopLinkA, loopLinkB);
260+
fs.symlinkSync(loopLinkB, loopLinkA);
261+
try {
262+
fs.rmSync(loopLinkA);
263+
} finally {
264+
fs.rmSync(loopLinkA, common.mustNotMutateObjectDeep({ force: true }));
265+
fs.rmSync(loopLinkB, common.mustNotMutateObjectDeep({ force: true }));
266+
}
267+
184268
// Should accept URL
185269
const fileURL = pathToFileURL(path.join(tmpdir.path, 'rm-file.txt'));
186270
fs.writeFileSync(fileURL, '');
@@ -195,7 +279,7 @@ if (isGitPresent) {
195279
fs.rmSync(dir, { recursive: true });
196280

197281
// Attempted removal should fail now because the directory is gone.
198-
assert.throws(() => fs.rmSync(dir), { syscall: 'stat' });
282+
assert.throws(() => fs.rmSync(dir), { syscall: 'lstat' });
199283
}
200284

201285
// Removing a .git directory should not throw an EPERM.
@@ -222,7 +306,7 @@ if (isGitPresent) {
222306
await fs.promises.rm(dir, common.mustNotMutateObjectDeep({ recursive: true }));
223307

224308
// Attempted removal should fail now because the directory is gone.
225-
await assert.rejects(fs.promises.rm(dir), { syscall: 'stat' });
309+
await assert.rejects(fs.promises.rm(dir), { syscall: 'lstat' });
226310

227311
// Should fail if target does not exist
228312
await assert.rejects(fs.promises.rm(
@@ -231,7 +315,7 @@ if (isGitPresent) {
231315
), {
232316
code: 'ENOENT',
233317
name: 'Error',
234-
message: /^ENOENT: no such file or directory, stat/
318+
message: /^ENOENT: no such file or directory, lstat/
235319
});
236320

237321
// Should not fail if target does not exist and force option is true
@@ -247,6 +331,39 @@ if (isGitPresent) {
247331
fs.rmSync(filePath, common.mustNotMutateObjectDeep({ force: true }));
248332
}
249333

334+
// Should delete a valid symlink
335+
const linkTarget = path.join(tmpdir.path, 'link-target-prom.txt');
336+
fs.writeFileSync(linkTarget, '');
337+
const validLink = path.join(tmpdir.path, 'valid-link-prom');
338+
fs.symlinkSync(linkTarget, validLink);
339+
try {
340+
await fs.promises.rm(validLink);
341+
} finally {
342+
fs.rmSync(linkTarget, common.mustNotMutateObjectDeep({ force: true }));
343+
fs.rmSync(validLink, common.mustNotMutateObjectDeep({ force: true }));
344+
}
345+
346+
// Should delete an invalid symlink
347+
const invalidLink = path.join(tmpdir.path, 'invalid-link-prom');
348+
fs.symlinkSync('definitely-does-not-exist-prom', invalidLink);
349+
try {
350+
await fs.promises.rm(invalidLink);
351+
} finally {
352+
fs.rmSync(invalidLink, common.mustNotMutateObjectDeep({ force: true }));
353+
}
354+
355+
// Should delete a symlink that is part of a loop
356+
const loopLinkA = path.join(tmpdir.path, 'loop-link-prom-a');
357+
const loopLinkB = path.join(tmpdir.path, 'loop-link-prom-b');
358+
fs.symlinkSync(loopLinkA, loopLinkB);
359+
fs.symlinkSync(loopLinkB, loopLinkA);
360+
try {
361+
await fs.promises.rm(loopLinkA);
362+
} finally {
363+
fs.rmSync(loopLinkA, common.mustNotMutateObjectDeep({ force: true }));
364+
fs.rmSync(loopLinkB, common.mustNotMutateObjectDeep({ force: true }));
365+
}
366+
250367
// Should accept URL
251368
const fileURL = pathToFileURL(path.join(tmpdir.path, 'rm-promises-file.txt'));
252369
fs.writeFileSync(fileURL, '');

0 commit comments

Comments
 (0)