Skip to content

Win: Fix std::fs::rename failing on Windows Server by attempting the non-atomic rename first #138133

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

Fulgen301
Copy link
Contributor

We previously attempted the atomic rename first, and fell back to trying a non-atomic rename if it wasn't supported. However, this does not work reliably on Windows Server - NTFS partitions created in Windows Server 2016 apparently need to be upgraded, and it outright fails with ERROR_NOT_SUPPORTED on ReFS on Windows Server 2022.

This commit switches the two calls so that FileRenameInfo is tried first, and an atomic rename via FileRenameInfoEx is only attempted if the non-atomic rename fails.

Fix #137499.
Fix #137971.

This PR works similarly to #137528, but is a cleaner fix in my opinion and avoids additional syscalls to reopen the file if MoveFileEx fails.

@rustbot
Copy link
Collaborator

rustbot commented Mar 7, 2025

r? @ChrisDenton

rustbot has assigned @ChrisDenton.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

@rustbot rustbot added O-windows Operating system: Windows S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Mar 7, 2025
@bors
Copy link
Collaborator

bors commented Mar 8, 2025

☔ The latest upstream changes (presumably #138208) made this pull request unmergeable. Please resolve the merge conflicts.

@ChrisDenton
Copy link
Member

Ok, I've been testing this on many Windows versions and on different filesystem. I think the better approach is to test for the following error codes:

  • ERROR_INVALID_PARAMETER
  • ERROR_NOT_SUPPORTED
  • ERROR_INVALID_FUNCTION

ERROR_INVALID_FUNCTION seems unique to 1607. Windows versions before that consistently return ERROR_INVALID_PARAMETER. After that it can be ERROR_NOT_SUPPORTED or ERROR_INVALID_PARAMETER depending on the OS version and filesystem.

@the8472
Copy link
Member

the8472 commented Mar 19, 2025

This commit switches the two calls so that FileRenameInfo is tried first, and an atomic rename via FileRenameInfoEx is only attempted if the non-atomic rename fails.

What are the non-atomic semantics on windows? Unix software generally expects renames to be atomic.

@jieyouxu
Copy link
Member

@rustbot author

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Apr 10, 2025
@rustbot
Copy link
Collaborator

rustbot commented Apr 10, 2025

Reminder, once the PR becomes ready for a review, use @rustbot ready.

@alex-semenyuk
Copy link
Member

@Fulgen301
Thanks for your contribution.
Any updates on this PR? Also please make rebase

@Fulgen301
Copy link
Contributor Author

I'll fix it up before the week is over.

In addition to ERROR_INVALID_PARAMETER for Windows versions older than
Windows 10 1607, some Windows versions may also fail with
ERROR_INVALID_FUNCTION or ERROR_NOT_SUPPORTED to indicate that atomic
rename is not supported. Fallback to FileRenameInfo in those cases.
Copy link
Member

@ChrisDenton ChrisDenton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll do a proper review when I have some time to do some testing on various Windows versions, filesystems, etc. But a few quick observations:

// This is a dynamically sized struct so we need to get the position of the last field to calculate the actual size.
let Ok(new_len_without_nul_in_bytes): Result<u32, _> = ((new.count_bytes() - 1) * 2).try_into()
else {
return Err(WinError::INVALID_PARAMETER).io_result();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably ERROR_FILENAME_EXCED_RANGE would be better or else a custom error message. Even if it's not strictly what happened before.

Err(err) if err.raw_os_error() == Some(c::ERROR_INVALID_PARAMETER as _) => None,
Err(err) => Some(Err(err)),
}
.unwrap_or_else(|| create_file(0, 0))?;
Copy link
Member

@ChrisDenton ChrisDenton Jun 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the error codes that are handled I worry this doesn't account for reparse points that cannot be opened, not because they're aren't supported but because they cannot be interpreted by CreateFile. E.g. app execution aliases aren't actually links in the filesystem sense and can only be interpreted by CreateProcess. We should still be able to rename them though.

};

unsafe {
dealloc(file_rename_info.cast::<u8>(), layout);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now fairly distant from the alloc. While nothing returns or panics before this atm, I wouldn't be surprised if some refactoring in the future missed this subtlety. So I would be happier with a drop guard or some other custom struct that is guaranteed to free memory wherever the scope ends.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
O-windows Operating system: Windows S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-libs Relevant to the library team, which will review and decide on the PR/issue.
Projects
None yet
7 participants