Skip to content

Commit 391d3bc

Browse files
zichanggcommit-bot@chromium.org
authored andcommitted
File::Copy avoids direct copying on Windows
There is a race condition for copying file on Windows, where CopyFile() returns success but data has not been populated into destination file. E.g process is killed or died in the middle. This cl will change File::Copy as 1. Copy file to a temp file in the same directory of destination file. 2. Rename the file to the target file. Bug: #42119 Change-Id: I39b6d451f6ace970bc554501148259d33de232c7 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/149667 Commit-Queue: Zichang Guo <[email protected]> Reviewed-by: Zach Anderson <[email protected]>
1 parent e4aaa11 commit 391d3bc

File tree

3 files changed

+113
-5
lines changed

3 files changed

+113
-5
lines changed

runtime/bin/file_win.cc

Lines changed: 97 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include <sys/utime.h> // NOLINT
1818

1919
#include "bin/builtin.h"
20+
#include "bin/crypto.h"
2021
#include "bin/directory.h"
2122
#include "bin/namespace.h"
2223
#include "bin/utils.h"
@@ -503,6 +504,85 @@ bool File::RenameLink(Namespace* namespc,
503504
return (move_status != 0);
504505
}
505506

507+
static wchar_t* CopyToDartScopeString(wchar_t* string) {
508+
wchar_t* wide_path = reinterpret_cast<wchar_t*>(
509+
Dart_ScopeAllocate(MAX_PATH * sizeof(wchar_t) + 1));
510+
wcscpy(wide_path, string);
511+
return wide_path;
512+
}
513+
514+
static wchar_t* CopyIntoTempFile(const char* src, const char* dest) {
515+
// This function will copy the file to a temp file in the destination
516+
// directory and return the path of temp file.
517+
// Creating temp file name has the same logic as Directory::CreateTemp(),
518+
// which tries with the rng and falls back to a uuid if it failed.
519+
const char* last_backslash = strrchr(dest, '\\');
520+
if (last_backslash == NULL) {
521+
return NULL;
522+
}
523+
int length_of_parent_dir = last_backslash - dest + 1;
524+
if (length_of_parent_dir + 8 > MAX_PATH) {
525+
return NULL;
526+
}
527+
uint32_t suffix_bytes = 0;
528+
const int kSuffixSize = sizeof(suffix_bytes);
529+
if (Crypto::GetRandomBytes(kSuffixSize,
530+
reinterpret_cast<uint8_t*>(&suffix_bytes))) {
531+
PathBuffer buffer;
532+
char* dir = reinterpret_cast<char*>(
533+
Dart_ScopeAllocate(1 + sizeof(char) * length_of_parent_dir));
534+
memmove(dir, dest, length_of_parent_dir);
535+
dir[length_of_parent_dir] = '\0';
536+
if (!buffer.Add(dir)) {
537+
return NULL;
538+
}
539+
540+
char suffix[8 + 1];
541+
Utils::SNPrint(suffix, sizeof(suffix), "%x", suffix_bytes);
542+
Utf8ToWideScope source_path(src);
543+
if (!buffer.Add(suffix)) {
544+
return NULL;
545+
}
546+
if (CopyFileExW(source_path.wide(), buffer.AsStringW(), NULL, NULL, NULL,
547+
0) != 0) {
548+
return CopyToDartScopeString(buffer.AsStringW());
549+
}
550+
}
551+
// UUID has a total of 36 characters in the form of
552+
// xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx.
553+
if (length_of_parent_dir + 36 > MAX_PATH) {
554+
return NULL;
555+
}
556+
UUID uuid;
557+
RPC_STATUS status = UuidCreateSequential(&uuid);
558+
if ((status != RPC_S_OK) && (status != RPC_S_UUID_LOCAL_ONLY)) {
559+
return NULL;
560+
}
561+
RPC_WSTR uuid_string;
562+
status = UuidToStringW(&uuid, &uuid_string);
563+
if (status != RPC_S_OK) {
564+
return NULL;
565+
}
566+
PathBuffer buffer;
567+
char* dir = reinterpret_cast<char*>(
568+
Dart_ScopeAllocate(1 + sizeof(char) * length_of_parent_dir));
569+
memmove(dir, dest, length_of_parent_dir);
570+
dir[length_of_parent_dir] = '\0';
571+
Utf8ToWideScope dest_path(dir);
572+
if (!buffer.AddW(dest_path.wide()) ||
573+
!buffer.AddW(reinterpret_cast<wchar_t*>(uuid_string))) {
574+
return NULL;
575+
}
576+
577+
RpcStringFreeW(&uuid_string);
578+
Utf8ToWideScope source_path(src);
579+
if (CopyFileExW(source_path.wide(), buffer.AsStringW(), NULL, NULL, NULL,
580+
0) != 0) {
581+
return CopyToDartScopeString(buffer.AsStringW());
582+
}
583+
return NULL;
584+
}
585+
506586
bool File::Copy(Namespace* namespc,
507587
const char* old_path,
508588
const char* new_path) {
@@ -511,11 +591,23 @@ bool File::Copy(Namespace* namespc,
511591
SetLastError(ERROR_FILE_NOT_FOUND);
512592
return false;
513593
}
514-
Utf8ToWideScope system_old_path(old_path);
515-
Utf8ToWideScope system_new_path(new_path);
516-
bool success = CopyFileExW(system_old_path.wide(), system_new_path.wide(),
517-
NULL, NULL, NULL, 0) != 0;
518-
return success;
594+
595+
wchar_t* temp_file = CopyIntoTempFile(old_path, new_path);
596+
if (temp_file == NULL) {
597+
return false;
598+
}
599+
Utf8ToWideScope system_new_dest(new_path);
600+
601+
// Remove the existing file. Otherwise, renaming will fail.
602+
if (Exists(namespc, new_path)) {
603+
DeleteFileW(system_new_dest.wide());
604+
}
605+
606+
if (!MoveFileW(temp_file, system_new_dest.wide())) {
607+
DeleteFileW(temp_file);
608+
return false;
609+
}
610+
return true;
519611
}
520612

521613
int64_t File::LengthFromPath(Namespace* namespc, const char* name) {

tests/standalone/io/file_copy_test.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ void testCopySync() {
3030
Expect.equals(FILE_CONTENT2, file1.readAsStringSync());
3131
Expect.equals(FILE_CONTENT2, file2.readAsStringSync());
3232

33+
// Check there is no temporary files existing.
34+
var list = tmp.listSync();
35+
Expect.equals(2, list.length);
36+
for (var file in list) {
37+
final fileName = file.path.toString();
38+
Expect.isTrue(fileName.contains("file1") || fileName.contains("file2"));
39+
}
40+
3341
// Fail when coping to directory.
3442
var dir = new Directory('${tmp.path}/dir')..createSync();
3543
Expect.throws(() => file1.copySync(dir.path));

tests/standalone_2/io/file_copy_test.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ void testCopySync() {
3030
Expect.equals(FILE_CONTENT2, file1.readAsStringSync());
3131
Expect.equals(FILE_CONTENT2, file2.readAsStringSync());
3232

33+
// Check there is no temporary files existing.
34+
var list = tmp.listSync();
35+
Expect.equals(2, list.length);
36+
for (var file in list) {
37+
final fileName = file.path.toString();
38+
Expect.isTrue(fileName.contains("file1") || fileName.contains("file2"));
39+
}
40+
3341
// Fail when coping to directory.
3442
var dir = new Directory('${tmp.path}/dir')..createSync();
3543
Expect.throws(() => file1.copySync(dir.path));

0 commit comments

Comments
 (0)