Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
fs: use libuv for recursive cpSync to avoid VirtioFS EACCES
fs.cpSync() recursive copy fails EACCES on Docker VirtioFS bind
mounts. CopyDirRecursive()'s regular-file branch calls
std::filesystem::copy_file(), which libstdc++ implements by creating
dest with mode 0200 and then fchmod(). VirtioFS rejects that initial
0200 open with EACCES. The single-file cpSync path and the
fresh-destination branch of CopyDirRecursive() already avoid this by
using uv_fs_copyfile(), which opens dest with its final mode
directly.

Use uv_fs_copyfile() for the merge-into-existing-directory case too,
preserving force, errorOnExist and skip-existing semantics via
UV_FS_COPYFILE_EXCL and an existence check. Also drops the
file_copy_opts capture, now unused once the regular-file branch no
longer calls std::filesystem::copy_file().

Fixes: #65497
Signed-off-by: Jeremy Schoemaker <jeremy@shoemoney.com>
  • Loading branch information
shoemoney committed Sep 9, 2026
commit 69470d16770a0491b23190aa678fdf8c2f94c63d
54 changes: 33 additions & 21 deletions src/node_file.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4827,18 +4827,9 @@ CpError CopyDirRecursive(const std::filesystem::path& src_path,
}
}

auto file_copy_opts = std::filesystem::copy_options::recursive;
if (options.force) {
file_copy_opts |= std::filesystem::copy_options::overwrite_existing;
} else if (options.error_on_exist) {
file_copy_opts |= std::filesystem::copy_options::none;
} else {
file_copy_opts |= std::filesystem::copy_options::skip_existing;
}

std::function<CpError(std::filesystem::path, std::filesystem::path)>
copy_dir_contents;
copy_dir_contents = [&options, &copy_dir_contents, file_copy_opts](
copy_dir_contents = [&options, &copy_dir_contents](
std::filesystem::path src,
std::filesystem::path dest) -> CpError {
std::error_code error;
Expand Down Expand Up @@ -4989,20 +4980,41 @@ CpError CopyDirRecursive(const std::filesystem::path& src_path,
CpError fresh = CopyFileFresh(
dir_entry.path(), dest_file_path, options.copyfile_flags);
if (fresh.kind != CpError::kNone) return fresh;
} else if (!options.force && !options.error_on_exist &&
std::filesystem::exists(dest_file_path, error)) {
copied = false;
} else {
copied = std::filesystem::copy_file(
dir_entry.path(), dest_file_path, file_copy_opts, error);
// uv_fs_copyfile() instead of std::filesystem::copy_file():
// libstdc++ implements copy_file() by creating dest with mode
// 0200 and then fchmod(), which VirtioFS bind mounts reject
// with EACCES. uv_fs_copyfile() opens dest with the final mode
// directly, matching the single-file cpSync path.
error.clear();
auto src_str = ConvertPathToUTF8(dir_entry.path());
auto dest_file_str = ConvertPathToUTF8(dest_file_path);
uv_fs_t req;
auto cleanup = OnScopeLeave([&req]() { uv_fs_req_cleanup(&req); });
int flags = options.error_on_exist ? UV_FS_COPYFILE_EXCL : 0;
int rc = uv_fs_copyfile(nullptr,
&req,
src_str.c_str(),
dest_file_str.c_str(),
flags,
nullptr);
if (is_uv_error(rc)) {
if (rc == UV_EEXIST) {
return {CpError::kEexist,
0,
"cp",
SPrintF("[ERR_FS_CP_EEXIST]: Target already exists: "
"cp returned EEXIST (%s already exists)",
dest_file_path),
{}};
}
return CpError::Uv(rc, "cp", dest_file_str);
}
}
if (error) {
if (error == std::errc::file_exists) {
return {CpError::kEexist,
0,
"cp",
SPrintF("[ERR_FS_CP_EEXIST]: Target already exists: "
"cp returned EEXIST (%s already exists)",
dest_file_path),
{}};
}
return CpError::Std(error, dest_str);
}

Expand Down
Loading