Repository navigation
Description
Version
v26.13.1, v24.22.3 and v22.23.5, the newest of each line, and v26.13.0, v24.22.2, v24.14.1 and v22.23.4. The call named under Additional information is unchanged on main.
Platform
Microsoft Windows [Version 10.0.26300.9818] x64
Subsystem
fs
What steps will reproduce the bug?
Save this as cpsync-volume-root.js and run node cpsync-volume-root.js as an ordinary (not elevated) user. It copies a small folder with fs.cpSync(src, dest, { recursive: true }) three ways, each in a child process so that the exit code can be shown, and removes everything it made.
'use strict';
// fs.cpSync() ends the process on Windows when the destination already exists
// directly under the root of a volume (C:\, D:\, ...).
//
// Run: node cpsync-volume-root.js
//
// Each case runs cpSync in a child process, so the exit code can be shown, and
// everything the script makes is removed afterwards.
const { spawnSync } = require('node:child_process');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const root = path.parse(os.tmpdir()).root; // the volume that holds the temp folder, e.g. C:\
const src = fs.mkdtempSync(path.join(os.tmpdir(), 'cpsync-src-'));
fs.writeFileSync(path.join(src, 'file.txt'), 'x');
const deeper = fs.mkdtempSync(path.join(os.tmpdir(), 'cpsync-deeper-'));
const cases = [
{ what: 'destination exists, directly under the volume root', dest: path.join(root, `cpsync-dest-a-${process.pid}`), exists: true },
{ what: 'destination does not exist, directly under the volume root', dest: path.join(root, `cpsync-dest-b-${process.pid}`), exists: false },
{ what: 'destination exists, one folder deeper', dest: path.join(deeper, 'dest'), exists: true },
];
const child = "require('node:fs').cpSync(process.argv[1], process.argv[2], { recursive: true }); console.log('copied');";
console.log(`Node ${process.version} ${process.arch}, Windows ${os.release()}`);
try {
for (const c of cases) {
if (c.exists) fs.mkdirSync(c.dest);
const run = spawnSync(process.execPath, ['-e', child, src, c.dest], { encoding: 'utf8' });
console.log(`\n${c.what}`);
console.log(` cpSync(${JSON.stringify(src)}, ${JSON.stringify(c.dest)}, { recursive: true })`);
console.log(` exit code ${run.status} (0x${(run.status >>> 0).toString(16).toUpperCase()})`);
console.log(` stdout: ${JSON.stringify(run.stdout.trim())}`);
console.log(` stderr: ${JSON.stringify(run.stderr.trim())}`);
fs.rmSync(c.dest, { recursive: true, force: true });
}
} finally {
for (const c of cases) fs.rmSync(c.dest, { recursive: true, force: true });
fs.rmSync(src, { recursive: true, force: true });
fs.rmSync(deeper, { recursive: true, force: true });
}The smallest form is fs.mkdirSync('C:\\x'); fs.cpSync(src, 'C:\\x', { recursive: true });.
How often does it reproduce? Is there a required condition?
Always: 12 of 12 runs, two each on v24.14.1, v24.22.2, v24.22.3, v26.13.0 and v26.13.1 and one each on v22.23.4 and v22.23.5, each from a fresh copy of the script.
The required condition is a destination that already exists, directly under a volume's root (C:\x, D:\x). It happens on NTFS (C:) and on ReFS (a Dev Drive), whatever is copied: on v24.14.1, a single file copied onto an existing file directly under a volume's root ends the process the same way, and so does a directory copy with filter: () => true, which does not take the native cpSyncCopyDir path. It does not happen when the destination does not exist yet, when the existing destination is one folder deeper, or under the root of a drive letter made with subst over an ordinary folder.
What is the expected behavior? Why is that the expected behavior?
The copy succeeds and prints copied, as it does when the existing destination is one folder deeper. The documentation of fs.cpSync() describes copying over an existing destination (force, default true: "overwrite existing file or directory") and makes no exception for a volume's root. If the copy could not be made, an error the caller can catch would be expected.
What do you see instead?
The child exits with code 3221226505 (0xC0000409) and writes nothing to stdout or stderr. No exception reaches JavaScript, so try/catch cannot handle it. Output on v26.13.1 (the user name replaced with USER):
Node v26.13.1 x64, Windows 10.0.26300
destination exists, directly under the volume root
cpSync("C:\\Users\\USER\\AppData\\Local\\Temp\\cpsync-src-BusLBy", "C:\\cpsync-dest-a-46300", { recursive: true })
exit code 3221226505 (0xC0000409)
stdout: ""
stderr: ""
destination does not exist, directly under the volume root
cpSync("C:\\Users\\USER\\AppData\\Local\\Temp\\cpsync-src-BusLBy", "C:\\cpsync-dest-b-46300", { recursive: true })
exit code 0 (0x0)
stdout: "copied"
stderr: ""
destination exists, one folder deeper
cpSync("C:\\Users\\USER\\AppData\\Local\\Temp\\cpsync-src-BusLBy", "C:\\Users\\USER\\AppData\\Local\\Temp\\cpsync-deeper-pbVQGk\\dest", { recursive: true })
exit code 0 (0x0)
stdout: "copied"
stderr: ""
Every other version named above prints the same exit codes. v20.20.2 does not end the process: it throws a catchable Error: EPERM: operation not permitted, mkdir 'C:' from checkParentDir, for a destination directly under the root whether or not it exists.
Additional information
The cause appears to be the last statement of CpSyncCheckPaths in src/node_file.cc:
if (!dest_exists || !std::filesystem::exists(dest_path.parent_path())) {It calls the throwing overload of std::filesystem::exists, and only when the destination exists, which is the required condition. dest_path is the namespaced path, \\?\C:\x. MSVC's std::filesystem takes \\? as that path's root name, so its parent_path() is \\?\C:, the volume itself rather than its root directory, and exists fails there with error 87 ("The parameter is incorrect."), so the throwing overload throws. A small program built with MSVC 19.51 shows this:
path \\?\C:\x: root_name '\\?', parent_path '\\?\C:', exists(parent, ec) 0, ec 87 'The parameter is incorrect.'
throwing exists(parent) threw: exists: The parameter is incorrect.: "\\?\C:"
path \\?\W:\x: root_name '\\?', parent_path '\\?\W:', exists(parent, ec) 1, ec 0 'The operation completed successfully.'
path \\?\C:\Users\x: root_name '\\?', parent_path '\\?\C:\Users', exists(parent, ec) 1, ec 0 'The operation completed successfully.'
(W: there is a subst drive letter, a link to an ordinary folder, which is why that case works.) I have not confirmed in a debugger that this is the exception that ends the process.
Related, but not the same: #63970 (the native cpSyncCopyDir path terminates on filesystem errors; here a filter does not avoid the crash) and #67141 (a throwing std::filesystem::equivalent in the same function, for a dangling symlink).
Activity