Remove race conditions from File.Move and Directory.Move #33370
Remove race conditions from File.Move and Directory.Move #33370joshudson wants to merge 8 commits into
Conversation
There was a problem hiding this comment.
comment about performance of trying link() on FAT is clearly not correct because the syscall fast-fails so if you have to perform any checks before deciding to rename() it's slower.
… (Directory.Move only applies on Linux)
|
Azure builds die with 🍺 /usr/local/Cellar/pkg-config/0.29.2: 11 files, 626.9KB ##[error]Bash exited with code '1'. Not mine! |
|
(NETCORE_ENGINEERING_TELEMETRY=InitializeToolset) Failed to install dotnet SDK from public location (exit code '1'). Not mine again! |
|
Ok somebody else needs to look at this. Remaining failed test cases: System.IO.Tests.DirectoryInfo_MoveTo.MoveDirectory_NoOpWhenMovingDirectoryWithUpperCaseToOtherDirectoryWithLowerCase This one is testing for actually succeeding and clobbering bar/FOO. I'm convinced my patch behaves correctly and the test is just wrong, because the documentation says it won't clobber the directory. The remaining two failing tests are: System.IO.Tests.File_Move_Tests.Unix_File_Move_To_Same_Director System.IO.Tests.File_Move_Tests.Unix_File_Move_With_Set_NotifyFilte which check if File.Move() actually does a rename operation. But you can't do |
adamsitnik
left a comment
There was a problem hiding this comment.
Hi @joshudson
First of all, I want to thank you for your work and apologize for the delay in the reviewing process.
Could you please respond to my comments and also solve the conflicts?
Thanks,
Adam
| } | ||
| else | ||
| { | ||
| #if HAVE_RENAMEAT2 && defined(RENAME_NOREPLACE) |
There was a problem hiding this comment.
just for my education: is it possible to have HAVE_RENAMEAT2 but no RENAME_NOREPLACE defined?
There was a problem hiding this comment.
It's feature probing. Clearly both must exist or it won't work. Officially, we don't need to know. I think it's possible, but I'm not certain.
| else if (errno == EEXIST) | ||
| { | ||
| // But renameat2 returns EEXIST on renaming a file onto itself. Check that. | ||
| result = RenameOnEexist(result, NULL, oldPath, newPath); |
There was a problem hiding this comment.
on renaming a file onto itself
Is this a breaking change? I would expect that previously we would have thrown an exception in the managed layer because FileExists would return true?
Do we have a unit test that covers this path?
There was a problem hiding this comment.
No unit test is possible because we can't demand root to run unit tests. We can't get here if the name components are equal.
| // Linux renameat2() provides the functionality we want inline. | ||
| // This is in a feature check block as this API may well be ported to other OSes later. | ||
| do { | ||
| result = renameat2(AT_FDCWD, oldPath, AT_FDCWD, newPath, RENAME_NOREPLACE); |
There was a problem hiding this comment.
Thank you for using AT_FDCWD here and for taking care of the edge case! 👍
Could you please add a comment about why it's important to use it here? (I had to check the docs to find out)
Is there any chance that you could add a unit test that would guard us from removing this flag in the future?
There was a problem hiding this comment.
I'm calling renameat2() for RENAME_NOREPLACE. AT_FDCWD is what you pass if you don't have anything else to pass in that slot.
There was a problem hiding this comment.
On third attempt, pronoun [it] is ambiguous. Do you mean AT_FDCWD or RENAME_NOREPLACE? A unit test for missing RENAME_NOREPLACE would just check that MoveFile(,,false) doesn't ovewrite its target and I think that already exists. If AT_FDCWD isn't passed, it will fail pretty hard as handle 0 won't be a directory handle.
| result = -1; // Keeps compiler happy. | ||
| if (!(flags & 2)) | ||
| { | ||
| result = link(oldPath, newPath); |
There was a problem hiding this comment.
| result = link(oldPath, newPath); | |
| result = link(oldPath, newPath); // link does not overwrite newPath if it exists |
| result = link(oldPath, newPath); | ||
| if (result >= 0) | ||
| { | ||
| while ((result = unlink(oldPath)) < 0 && errno == EINTR); |
There was a problem hiding this comment.
what are the side effects of using this approach (link and unlink) compared to what we had before (check + rename with TOCTOU as a side effect)? can it lead to any new problems? (please keep in mind that I am not a Unix expert)
There was a problem hiding this comment.
Side effect: File system watcher looking for rename() won't ever see it.
Incidentally I had to go through our own codebase ten years ago and systematically swap File.Move() for P/Invoke of CreateHardLink because somebody deployed our application on Windows pointing at a NAS box and hit the race condition where Windows resolving a UNC to a NAS box will inherently have this race. There is one other side effect, that is if the application crashes, you can see both names at once. But it's not really a side effect on Unix, because that can happen anyway on power loss.
|
Hey just to let you know I'm not ignoring this. I've got some time off upcoming and I intend to look at this during that time. |
| // file, if it exists. If deletion fails for a reason other than the file not existing, fail. | ||
| if (Interop.Sys.Unlink(destBackupFullPath) != 0) | ||
| // We're backing up the destination file to the backup file. If it exists, it will be removed if possible. | ||
| if (Interop.Sys.Rename(destFullPath, destBackupFullPath, 1) < 0) |
There was a problem hiding this comment.
Is the call Interop.Sys.Rename(destFullPath, destBackupFullPath, 1) identical to calling the method Interop.Sys.Rename(string oldPath, string newPath) that existed prior to this PR?
There was a problem hiding this comment.
It's supposed to be.
| // file, if it exists. If deletion fails for a reason other than the file not existing, fail. | ||
| if (Interop.Sys.Unlink(destBackupFullPath) != 0) | ||
| // We're backing up the destination file to the backup file. If it exists, it will be removed if possible. | ||
| if (Interop.Sys.Rename(destFullPath, destBackupFullPath, 1) < 0) |
There was a problem hiding this comment.
Is < 0 correct or should it be != 0 instead?
There was a problem hiding this comment.
That's a style debate, not a functional debate. I kept the original return values of 0 for success and -1 for failure.
| errno.Error == Interop.Error.EMLINK) // too many hard links to the source file | ||
| { | ||
| // Destination file could not be renamed. Copy it. | ||
| CopyFile(destFullPath, destBackupFullPath, true); |
There was a problem hiding this comment.
Why are we no longer attempting to create a link before a copy for the destBackupFullPath?
There was a problem hiding this comment.
Because if you hit any of those errors, link() won't work either.
|
I'm blocked on #46301 ; the tree doesn't build referencing SOS_readme.md |
|
I fixed minor problems from review; however I ran into problems dealing with File.Replace. Filed doc issue dotnet/docs#22288 ; the code might also be wrong in master. |
|
Some test failures: |
|
@danmosemsft : Yes, that's a decision that has to be made. I left these failing until we decide we're good with the filesystem watcher not seeing any rename events on |
|
@adamsitnik thoughts about next steps for this PR? |
|
We don't seem to be making progress on this PR. Regretfully, I'm going to close it. It can always be reopened . |
(Directory.Move only applies on Linux)
Fixes #31228