From ae895a4987fa779008317e3aad083cec06384b24 Mon Sep 17 00:00:00 2001 From: semimikoh Date: Fri, 23 Oct 2026 17:46:54 +0900 Subject: [PATCH 1/2] fs: copy dangling symlinks over existing symlinks Reuse srcIsDir instead of stat() for the subdirectory check. Signed-off-by: semimikoh --- lib/internal/fs/cp/cp.js | 3 +-- ...fs-cp-async-dangling-symlink-over-symlink.mjs | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 test/parallel/test-fs-cp-async-dangling-symlink-over-symlink.mjs diff --git a/lib/internal/fs/cp/cp.js b/lib/internal/fs/cp/cp.js index 1ec5a2d89ca..0c433871169 100644 --- a/lib/internal/fs/cp/cp.js +++ b/lib/internal/fs/cp/cp.js @@ -398,8 +398,7 @@ async function onLink(destStat, src, dest, opts) { // Do not copy if src is a subdir of dest since unlinking // dest in this case would result in removing src contents // and therefore a broken symlink would be created. - const srcStat = await stat(src); - if (srcStat.isDirectory() && isSrcSubdir(resolvedDest, resolvedSrc)) { + if (srcIsDir && isSrcSubdir(resolvedDest, resolvedSrc)) { throw new ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY({ message: `cannot overwrite ${resolvedDest} with ${resolvedSrc}`, path: dest, diff --git a/test/parallel/test-fs-cp-async-dangling-symlink-over-symlink.mjs b/test/parallel/test-fs-cp-async-dangling-symlink-over-symlink.mjs new file mode 100644 index 00000000000..e69686bdd50 --- /dev/null +++ b/test/parallel/test-fs-cp-async-dangling-symlink-over-symlink.mjs @@ -0,0 +1,16 @@ +import { mustNotMutateObjectDeep } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { promises, readlinkSync, symlinkSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import tmpdir from '../common/tmpdir.js'; + +tmpdir.refresh(); + +const src = nextdir(); +const dest = nextdir(); +symlinkSync('missing', src); +symlinkSync(process.execPath, dest); + +await promises.cp(src, dest, mustNotMutateObjectDeep({ force: true })); +assert.strictEqual(readlinkSync(dest), resolve(dirname(src), 'missing')); From ff568e2d90f7f07eba045b3a6f3acaad106c4944 Mon Sep 17 00:00:00 2001 From: semimikoh Date: Mon, 9 Nov 2026 15:19:21 +0900 Subject: [PATCH 2/2] test: cover verbatimSymlinks in fs.cp() Signed-off-by: Semi Koh --- ...cp-async-dangling-symlink-over-symlink.mjs | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/test/parallel/test-fs-cp-async-dangling-symlink-over-symlink.mjs b/test/parallel/test-fs-cp-async-dangling-symlink-over-symlink.mjs index e69686bdd50..87cbe2dfbcf 100644 --- a/test/parallel/test-fs-cp-async-dangling-symlink-over-symlink.mjs +++ b/test/parallel/test-fs-cp-async-dangling-symlink-over-symlink.mjs @@ -7,10 +7,19 @@ import tmpdir from '../common/tmpdir.js'; tmpdir.refresh(); -const src = nextdir(); -const dest = nextdir(); -symlinkSync('missing', src); -symlinkSync(process.execPath, dest); +for (const verbatimSymlinks of [undefined, false, true]) { + const src = nextdir(); + const dest = nextdir(); + symlinkSync('missing', src); + symlinkSync(process.execPath, dest); -await promises.cp(src, dest, mustNotMutateObjectDeep({ force: true })); -assert.strictEqual(readlinkSync(dest), resolve(dirname(src), 'missing')); + const options = { force: true }; + if (verbatimSymlinks !== undefined) { + options.verbatimSymlinks = verbatimSymlinks; + } + + await promises.cp(src, dest, mustNotMutateObjectDeep(options)); + assert.strictEqual(readlinkSync(dest), + verbatimSymlinks ? 'missing' : resolve(dirname(src), 'missing'), + `verbatimSymlinks: ${verbatimSymlinks}`); +}