Skip to content

Commit 9ff389d

Browse files
committed
fs: fix repeated copy of directory with symlinks
Repeatedly copying a directory that contains a symlink to an unrelated directory fails on the second copy with ERR_FS_CP_EINVAL because the symlink target is mistaken for a self-referential copy. Compare symlink targets using canonicalized paths, treating identical targets as self-copies only when the target is the destination root or one of its ancestors. Fixes: #65097 Signed-off-by: haramjeong <04harams77@gmail.com>
1 parent 7b6b21a commit 9ff389d

5 files changed

Lines changed: 147 additions & 16 deletions

File tree

‎lib/internal/fs/cp/cp-sync.js‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ function cpSyncFn(src, dest, opts) {
5353
if (!shouldCopy) return;
5454
}
5555

56+
opts = { ...opts, destRoot: dest };
5657
fsBinding.cpSyncCheckPaths(src, dest, opts.dereference, opts.recursive);
5758

5859
return getStats(src, dest, opts);
@@ -71,7 +72,7 @@ function getStats(src, dest, opts) {
7172
srcStat.isBlockDevice()) {
7273
return onFile(srcStat, destStat, src, dest, opts);
7374
} else if (srcStat.isSymbolicLink()) {
74-
return onLink(destStat, src, dest, opts.verbatimSymlinks);
75+
return onLink(destStat, src, dest, opts.verbatimSymlinks, opts.destRoot);
7576
}
7677

7778
// It is not possible to get here because all possible cases are handled above.
@@ -186,7 +187,7 @@ function copyDir(src, dest, opts, mkDir, srcMode) {
186187
}
187188

188189
// TODO(@anonrig): Move this function to C++.
189-
function onLink(destStat, src, dest, verbatimSymlinks) {
190+
function onLink(destStat, src, dest, verbatimSymlinks, destRoot) {
190191
let resolvedSrc = readlinkSync(src);
191192
if (!verbatimSymlinks && !isAbsolute(resolvedSrc)) {
192193
resolvedSrc = resolve(dirname(src), resolvedSrc);
@@ -212,7 +213,13 @@ function onLink(destStat, src, dest, verbatimSymlinks) {
212213
resolvedDest = resolve(dirname(dest), resolvedDest);
213214
}
214215

215-
if (srcIsDir && isSrcSubdir(resolvedSrc, resolvedDest)) {
216+
// A symlink that resolves to the same target as the destination symlink
217+
// is not a self-copy, unless the target is the destination directory
218+
// itself or one of its ancestors.
219+
const sameTarget = resolvedSrc === resolvedDest &&
220+
!isSrcSubdir(resolvedSrc, resolve(destRoot));
221+
222+
if (srcIsDir && isSrcSubdir(resolvedSrc, resolvedDest) && !sameTarget) {
216223
throw new ERR_FS_CP_EINVAL({
217224
message: `cannot copy ${resolvedSrc} to a subdirectory of self ` +
218225
`${resolvedDest}`,
@@ -225,7 +232,8 @@ function onLink(destStat, src, dest, verbatimSymlinks) {
225232
// Prevent copy if src is a subdir of dest since unlinking
226233
// dest in this case would result in removing src contents
227234
// and therefore a broken symlink would be created.
228-
if (statSync(dest).isDirectory() && isSrcSubdir(resolvedDest, resolvedSrc)) {
235+
if (statSync(dest).isDirectory() && isSrcSubdir(resolvedDest, resolvedSrc) &&
236+
!sameTarget) {
229237
throw new ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY({
230238
message: `cannot overwrite ${resolvedDest} with ${resolvedSrc}`,
231239
path: dest,

‎lib/internal/fs/cp/cp.js‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ async function cpFn(src, dest, opts) {
6363
'node is not recommended';
6464
process.emitWarning(warning, 'TimestampPrecisionWarning');
6565
}
66+
opts = { ...opts, destRoot: dest };
6667
const stats = await checkPaths(src, dest, opts);
6768
const { srcStat, destStat, skipped } = stats;
6869
if (skipped) return;
@@ -357,7 +358,13 @@ async function onLink(destStat, src, dest, opts) {
357358
resolvedDest = resolve(dirname(dest), resolvedDest);
358359
}
359360

360-
if (srcIsDir && isSrcSubdir(resolvedSrc, resolvedDest)) {
361+
// A symlink that resolves to the same target as the destination symlink
362+
// is not a self-copy, unless the target is the destination directory
363+
// itself or one of its ancestors.
364+
const sameTarget = resolvedSrc === resolvedDest &&
365+
!isSrcSubdir(resolvedSrc, resolve(opts.destRoot));
366+
367+
if (srcIsDir && isSrcSubdir(resolvedSrc, resolvedDest) && !sameTarget) {
361368
throw new ERR_FS_CP_EINVAL({
362369
message: `cannot copy ${resolvedSrc} to a subdirectory of self ` +
363370
`${resolvedDest}`,
@@ -371,7 +378,8 @@ async function onLink(destStat, src, dest, opts) {
371378
// dest in this case would result in removing src contents
372379
// and therefore a broken symlink would be created.
373380
const srcStat = await stat(src);
374-
if (srcStat.isDirectory() && isSrcSubdir(resolvedDest, resolvedSrc)) {
381+
if (srcStat.isDirectory() && isSrcSubdir(resolvedDest, resolvedSrc) &&
382+
!sameTarget) {
375383
throw new ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY({
376384
message: `cannot overwrite ${resolvedDest} with ${resolvedSrc}`,
377385
path: dest,

‎src/node_file.cc‎

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4101,6 +4101,7 @@ static void CpSyncCopyDir(const FunctionCallbackInfo<Value>& args) {
41014101
force,
41024102
error_on_exist,
41034103
dereference,
4104+
dest_path,
41044105
&isolate](std::filesystem::path src,
41054106
std::filesystem::path dest) {
41064107
std::error_code error;
@@ -4124,6 +4125,15 @@ static void CpSyncCopyDir(const FunctionCallbackInfo<Value>& args) {
41244125
return false;
41254126
}
41264127

4128+
auto symlink_target_absolute = std::filesystem::weakly_canonical(
4129+
std::filesystem::absolute(src / symlink_target));
4130+
#ifdef _WIN32
4131+
auto wstr = symlink_target_absolute.wstring();
4132+
if (wstr.starts_with(L"\\\\?\\")) {
4133+
symlink_target_absolute = std::filesystem::path(wstr.substr(4));
4134+
}
4135+
#endif
4136+
41274137
if (std::filesystem::exists(dest_file_path)) {
41284138
if (std::filesystem::is_symlink((dest_file_path.c_str()))) {
41294139
auto current_dest_symlink_target =
@@ -4133,9 +4143,29 @@ static void CpSyncCopyDir(const FunctionCallbackInfo<Value>& args) {
41334143
return false;
41344144
}
41354145

4146+
// A symlink that resolves to the same target as the destination
4147+
// symlink is not a self-copy, unless the target is the
4148+
// destination directory itself or one of its ancestors.
4149+
auto current_dest_symlink_target_absolute =
4150+
std::filesystem::weakly_canonical(
4151+
std::filesystem::absolute(dest_file_path.parent_path() /
4152+
current_dest_symlink_target));
4153+
#ifdef _WIN32
4154+
auto wstr2 = current_dest_symlink_target_absolute.wstring();
4155+
if (wstr2.starts_with(L"\\\\?\\")) {
4156+
current_dest_symlink_target_absolute =
4157+
std::filesystem::path(wstr2.substr(4));
4158+
}
4159+
#endif
4160+
bool same_target =
4161+
symlink_target_absolute ==
4162+
current_dest_symlink_target_absolute &&
4163+
!isInsideDir(symlink_target_absolute, dest_path);
4164+
41364165
if (!dereference &&
41374166
std::filesystem::is_directory(symlink_target) &&
4138-
isInsideDir(symlink_target, current_dest_symlink_target)) {
4167+
isInsideDir(symlink_target, current_dest_symlink_target) &&
4168+
!same_target) {
41394169
static constexpr const char* message =
41404170
"Cannot copy %s to a subdirectory of self %s";
41414171
THROW_ERR_FS_CP_EINVAL(
@@ -4147,7 +4177,8 @@ static void CpSyncCopyDir(const FunctionCallbackInfo<Value>& args) {
41474177
// dest in this case would result in removing src contents
41484178
// and therefore a broken symlink would be created.
41494179
if (std::filesystem::is_directory(dest_file_path) &&
4150-
isInsideDir(current_dest_symlink_target, symlink_target)) {
4180+
isInsideDir(current_dest_symlink_target, symlink_target) &&
4181+
!same_target) {
41514182
static constexpr const char* message =
41524183
"cannot overwrite %s with %s";
41534184
THROW_ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY(
@@ -4174,14 +4205,6 @@ static void CpSyncCopyDir(const FunctionCallbackInfo<Value>& args) {
41744205
}
41754206
}
41764207
}
4177-
auto symlink_target_absolute = std::filesystem::weakly_canonical(
4178-
std::filesystem::absolute(src / symlink_target));
4179-
#ifdef _WIN32
4180-
auto wstr = symlink_target_absolute.wstring();
4181-
if (wstr.starts_with(L"\\\\?\\")) {
4182-
symlink_target_absolute = std::filesystem::path(wstr.substr(4));
4183-
}
4184-
#endif
41854208
if (dir_entry.is_directory()) {
41864209
std::filesystem::create_directory_symlink(
41874210
symlink_target_absolute, dest_file_path, error);
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// This tests that repeatedly copying a directory containing a symlink
2+
// to an unrelated directory succeeds.
3+
// See https://github.com/nodejs/node/issues/65097.
4+
import { mustCall, mustNotMutateObjectDeep } from '../common/index.mjs';
5+
import { nextdir } from '../common/fs.js';
6+
import assert from 'node:assert';
7+
import { cp, mkdirSync, realpathSync, symlinkSync } from 'node:fs';
8+
import { join } from 'node:path';
9+
10+
import tmpdir from '../common/tmpdir.js';
11+
tmpdir.refresh();
12+
13+
const root = nextdir();
14+
const src = join(root, 'src');
15+
const dest = join(root, 'dest');
16+
const target = join(root, 'target');
17+
mkdirSync(src, mustNotMutateObjectDeep({ recursive: true }));
18+
mkdirSync(target);
19+
symlinkSync(target, join(src, 'link'));
20+
cp(src, dest, mustNotMutateObjectDeep({ recursive: true }), mustCall((err) => {
21+
assert.ifError(err);
22+
cp(src, dest, mustNotMutateObjectDeep({ recursive: true }), mustCall((err) => {
23+
assert.ifError(err);
24+
assert.strictEqual(realpathSync(join(dest, 'link')), realpathSync(target));
25+
}));
26+
}));
27+
28+
// A symlink with a relative target pointing to an unrelated directory.
29+
{
30+
const root = nextdir();
31+
const src = join(root, 'src');
32+
const dest = join(root, 'dest');
33+
const target = join(root, 'target');
34+
mkdirSync(src, mustNotMutateObjectDeep({ recursive: true }));
35+
mkdirSync(target);
36+
symlinkSync('../target', join(src, 'link'));
37+
cp(src, dest, mustNotMutateObjectDeep({ recursive: true }), mustCall((err) => {
38+
assert.ifError(err);
39+
cp(src, dest, mustNotMutateObjectDeep({ recursive: true }), mustCall((err) => {
40+
assert.ifError(err);
41+
assert.strictEqual(realpathSync(join(dest, 'link')), realpathSync(target));
42+
}));
43+
}));
44+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// This tests that repeatedly copying a directory containing a symlink
2+
// to an unrelated directory succeeds.
3+
// See https://github.com/nodejs/node/issues/65097.
4+
import { mustNotMutateObjectDeep } from '../common/index.mjs';
5+
import { nextdir } from '../common/fs.js';
6+
import assert from 'node:assert';
7+
import { cpSync, mkdirSync, realpathSync, symlinkSync } from 'node:fs';
8+
import { join } from 'node:path';
9+
10+
import tmpdir from '../common/tmpdir.js';
11+
tmpdir.refresh();
12+
13+
const root = nextdir();
14+
const src = join(root, 'src');
15+
const dest = join(root, 'dest');
16+
const target = join(root, 'target');
17+
mkdirSync(src, mustNotMutateObjectDeep({ recursive: true }));
18+
mkdirSync(target);
19+
symlinkSync(target, join(src, 'link'));
20+
cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true }));
21+
cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true }));
22+
assert.strictEqual(realpathSync(join(dest, 'link')), realpathSync(target));
23+
24+
// Also exercise the JavaScript (filter) path. The destination symlink
25+
// already exists at this point, so this covers the repeated-copy case on
26+
// the filtered code path as well.
27+
cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true, filter: () => true }));
28+
cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true, filter: () => true }));
29+
assert.strictEqual(realpathSync(join(dest, 'link')), realpathSync(target));
30+
31+
// A symlink with a relative target pointing to an unrelated directory.
32+
{
33+
const root = nextdir();
34+
const src = join(root, 'src');
35+
const dest = join(root, 'dest');
36+
const target = join(root, 'target');
37+
mkdirSync(src, mustNotMutateObjectDeep({ recursive: true }));
38+
mkdirSync(target);
39+
symlinkSync('../target', join(src, 'link'));
40+
cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true }));
41+
cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true }));
42+
assert.strictEqual(realpathSync(join(dest, 'link')), realpathSync(target));
43+
44+
// Same as above, exercising the JavaScript (filter) path.
45+
cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true, filter: () => true }));
46+
cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true, filter: () => true }));
47+
assert.strictEqual(realpathSync(join(dest, 'link')), realpathSync(target));
48+
}

0 commit comments

Comments
 (0)