Skip to content

Commit b91d82d

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 d6bbf57 commit b91d82d

5 files changed

Lines changed: 135 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);
@@ -211,7 +212,13 @@ function onLink(destStat, src, dest, verbatimSymlinks) {
211212
}
212213
const srcIsDir = fsBinding.internalModuleStat(src) === 1;
213214

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

358359
const srcIsDir = fsBinding.internalModuleStat(src) === 1;
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
@@ -3755,6 +3755,7 @@ static void CpSyncCopyDir(const FunctionCallbackInfo<Value>& args) {
37553755
force,
37563756
error_on_exist,
37573757
dereference,
3758+
dest_path,
37583759
&isolate](std::filesystem::path src,
37593760
std::filesystem::path dest) {
37603761
std::error_code error;
@@ -3778,6 +3779,15 @@ static void CpSyncCopyDir(const FunctionCallbackInfo<Value>& args) {
37783779
return false;
37793780
}
37803781

3782+
auto symlink_target_absolute = std::filesystem::weakly_canonical(
3783+
std::filesystem::absolute(src / symlink_target));
3784+
#ifdef _WIN32
3785+
auto wstr = symlink_target_absolute.wstring();
3786+
if (wstr.starts_with(L"\\\\?\\")) {
3787+
symlink_target_absolute = std::filesystem::path(wstr.substr(4));
3788+
}
3789+
#endif
3790+
37813791
if (std::filesystem::exists(dest_file_path)) {
37823792
if (std::filesystem::is_symlink((dest_file_path.c_str()))) {
37833793
auto current_dest_symlink_target =
@@ -3787,9 +3797,29 @@ static void CpSyncCopyDir(const FunctionCallbackInfo<Value>& args) {
37873797
return false;
37883798
}
37893799

3800+
// A symlink that resolves to the same target as the destination
3801+
// symlink is not a self-copy, unless the target is the
3802+
// destination directory itself or one of its ancestors.
3803+
auto current_dest_symlink_target_absolute =
3804+
std::filesystem::weakly_canonical(
3805+
std::filesystem::absolute(dest_file_path.parent_path() /
3806+
current_dest_symlink_target));
3807+
#ifdef _WIN32
3808+
auto wstr2 = current_dest_symlink_target_absolute.wstring();
3809+
if (wstr2.starts_with(L"\\\\?\\")) {
3810+
current_dest_symlink_target_absolute =
3811+
std::filesystem::path(wstr2.substr(4));
3812+
}
3813+
#endif
3814+
bool same_target =
3815+
symlink_target_absolute ==
3816+
current_dest_symlink_target_absolute &&
3817+
!isInsideDir(symlink_target_absolute, dest_path);
3818+
37903819
if (!dereference &&
37913820
std::filesystem::is_directory(symlink_target) &&
3792-
isInsideDir(symlink_target, current_dest_symlink_target)) {
3821+
isInsideDir(symlink_target, current_dest_symlink_target) &&
3822+
!same_target) {
37933823
static constexpr const char* message =
37943824
"Cannot copy %s to a subdirectory of self %s";
37953825
THROW_ERR_FS_CP_EINVAL(
@@ -3801,7 +3831,8 @@ static void CpSyncCopyDir(const FunctionCallbackInfo<Value>& args) {
38013831
// dest in this case would result in removing src contents
38023832
// and therefore a broken symlink would be created.
38033833
if (std::filesystem::is_directory(dest_file_path) &&
3804-
isInsideDir(current_dest_symlink_target, symlink_target)) {
3834+
isInsideDir(current_dest_symlink_target, symlink_target) &&
3835+
!same_target) {
38053836
static constexpr const char* message =
38063837
"cannot overwrite %s with %s";
38073838
THROW_ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY(
@@ -3828,14 +3859,6 @@ static void CpSyncCopyDir(const FunctionCallbackInfo<Value>& args) {
38283859
}
38293860
}
38303861
}
3831-
auto symlink_target_absolute = std::filesystem::weakly_canonical(
3832-
std::filesystem::absolute(src / symlink_target));
3833-
#ifdef _WIN32
3834-
auto wstr = symlink_target_absolute.wstring();
3835-
if (wstr.starts_with(L"\\\\?\\")) {
3836-
symlink_target_absolute = std::filesystem::path(wstr.substr(4));
3837-
}
3838-
#endif
38393862
if (dir_entry.is_directory()) {
38403863
std::filesystem::create_directory_symlink(
38413864
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: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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+
// A symlink with a relative target pointing to an unrelated directory.
25+
{
26+
const root = nextdir();
27+
const src = join(root, 'src');
28+
const dest = join(root, 'dest');
29+
const target = join(root, 'target');
30+
mkdirSync(src, mustNotMutateObjectDeep({ recursive: true }));
31+
mkdirSync(target);
32+
symlinkSync('../target', join(src, 'link'));
33+
cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true }));
34+
cpSync(src, dest, mustNotMutateObjectDeep({ recursive: true }));
35+
assert.strictEqual(realpathSync(join(dest, 'link')), realpathSync(target));
36+
}

0 commit comments

Comments
 (0)