Skip to content
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

Fix fs::remove_dir_all #31944

Closed
wants to merge 5 commits into from
Closed

Fix fs::remove_dir_all #31944

wants to merge 5 commits into from

Conversation

pitdicker
Copy link
Contributor

This fixes the case where remove_dir_all is unable to remove large directories on Windows due to race conditions.
It is now also able to remove read-only files, and when the paths become longer that MAX_PATH.

Fixes #29497

This is based on #31892, I hope I didn't mess up git to much...

@rust-highfive
Copy link
Collaborator

r? @aturon

(rust_highfive has picked a reviewer for you, use r? to override)

@retep998
Copy link
Member

Your PR has three merges. You may want to consider rebasing and force pushing.


#[repr(C)]
pub struct FILE_DISPOSITION_INFO {
pub DeleteFile: BOOL, // for true use -1, not TRUE
Copy link
Member

Choose a reason for hiding this comment

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

Why does this need to be -1? I don't see anything on MSDN for why this can't be TRUE.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This struct is the same as FILE_DISPOSITION_INFORMATION which uses a BOOLEAN. But TRUE just doesn't work, it gives an error. The documentation for this fuction is not great, this one is better: https://msdn.microsoft.com/nl-nl/library/windows/hardware/ff567096(v=vs.85).aspx

Copy link
Member

Choose a reason for hiding this comment

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

Set this member to TRUE to delete the file when it is closed.

Nowhere do I see any mention of -1. What error do you get when you use TRUE instead of -1?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Something like: the file name, directory name or volume lable syntax is incorrect (I am on mobile, can't test now). But it took me hours to get working because of this bug. Only afterwards I found out this function is just a very thin wrapper around an NT internal function that takes a BOOLEAN instead of a BOOL, explaining the problem.

Copy link
Member

Choose a reason for hiding this comment

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

Can you move this comment to where it's called down in remove(&self) and put some of these comments there as well? Sounds like it'll be useful info for the next reader!

@pitdicker
Copy link
Contributor Author

@retep998 thanks for helping out with FILE_RENAME_INFO on irc!

And time to improve my git skills :(

On Windows files can have a simple read-only permission flag, and the
more complex access rights
and access control lists. The read-only flag is one bit of the file
attributes.

To make sure only the read-only flag is toggled and not the other
attributes, read the files
attributes and write them back with only this bit changed. By reading
and setting the attributes
from the handle the file does not have to be opened twice.

Also directories can not be read-only, here the flag has a different
meaning. So we should no
longer report directories as read-only, and also not allow making them
read-only.
This fixes the case where `remove_dir_all` is unable to remove large
directories on Windows due to race conditions.
It is now also able to remove read-only files, and when the paths become
longer that MAX_PATH.
@pitdicker
Copy link
Contributor Author

Rebased

opts.access_mode(c::FILE_READ_ATTRIBUTES | c::FILE_WRITE_ATTRIBUTES);
opts.custom_flags(c::FILE_FLAG_BACKUP_SEMANTICS);
let file = try!(File::open(path, &opts));
file.set_perm(perm)
Copy link
Member

Choose a reason for hiding this comment

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

Could you add some tests for this as well? It's fine for some of them to be windows-specific (e.g. in this module), but specifically:

  • Directories can't be set to readonly
  • Other attributes are preserved

@alexcrichton
Copy link
Member

Thanks @pitdicker! This is looking great to me

@alexcrichton alexcrichton assigned alexcrichton and unassigned aturon Feb 29, 2016
@pitdicker
Copy link
Contributor Author

Thanks for the review!.
I will do the other modifications during the week

@pitdicker
Copy link
Contributor Author

Updated.

I messed up a lot with the permissions stuff of directories. Here is the article that explains how the read-only attribute indicates a directory is special on Windows. As it turns out you can create and delete files in a read-only directory, but nor delete or remove the dir itself. This is exactly the opposite of Unix, where you can't create or delete files in a read-only directory, but can delete the dir itself.

So I reversed the permission stuff. And we now have an other edge case to handle on Unix: readonly dirs. Before every call to remove_dir_all_recursive we now check if the dir is read-only, and adjust the permissions. I choose to do this outside of remove_dir_all_recursive, because we already do a stat in remove_dir_all that can now be reused.

I think this addresses all the comments except the suggestion to use offset_of! (I can't find it) and to tweak to_u16s (for an other pr).

// readonly
mode.set_readonly(false);
try!(set_perm(&child.path(), mode));
}
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps this function could take &Metadata as an argument and perform this logic at the start? That way it could prevent duplication between here/above.

@alexcrichton
Copy link
Member

Hm now I'm getting a little more worried with this as time passes, it seems like we're definitely going out of our way to implement a arguably primitive operation, which may end up being surprising to some. This code was originally modelled after Boost which is significantly simpler than what's going on here (even on Unix). That's not necessarily either good or bad, but it's indicative to me that we should tread lightly here.

Perhaps we can try to find some other implementations of this function in other libraries? It it standard practice to attempt to so aggressively remove files/directories?

@pitdicker
Copy link
Contributor Author

I think the part in the Windows version of moving files is not really over the top. You could say it is just a necessary evil to avoid races. But I share your feeling about the rest.

Personally I am finding more and more functions that could benefit from options. Overwrite or fail if something already exist. Follow symlinks or not. Delete read-only files, fail, or maybe ask for confirmation.... For copy whether we should copy times, attributes etc. Be recursive or not.

What to do about read-only files on Windows, and read-only directories on Unix is the difficult part. I kind of like it to just work, as you are also able to move the dir somewhere else and it looks deleted from that place. But I don't feel very strongly about it. (of course it would be sad, as it took me quite some time to get working...)

For a filesystem library to take inspiration from, I look a little at AFIO. I can't read it very easily, but it has the focus on correctness, avoiding races, and cross-platform consistency. But it is not all good, otherwise it would have ended up in Boost.

@alexcrichton
Copy link
Member

Yeah it was somewhat planned that std::fs would grow various builders over time for options like these. You're right that fs::copy could do so much more (attributes, recursive, etc), but they'd probably want to all be encapsulated in some builder with tweakable options.

In terms of removing readonly directories on Unix, it looks like rm -rf doesn't automatically remove a readonly directory. Maybe we should stay consistent with that? Maybe we should only support removing readonly files on Windows specially?

@alexcrichton alexcrichton added T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. and removed T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. labels Mar 14, 2016
@alexcrichton
Copy link
Member

We discussed this during the libs triage meeting yesterday, and we unfortunately didn't reach too many conclusions about this. Some thoughts brought up were:

  • It would likely be worth looking at what other standard libraries do in this respect. For example this was copied from Boost, and it looks like Python also has a similar implementation (and similar bug reports).
  • We could perhaps add a remove_dir_all_force variant along the lines of rm -f to do the "extra work" to really delete something. If this is the function that basically everyone uses, however, then that seems unfortunate?
  • We may want to think about what would precisely be documented here. For example what are we comfortable documenting that this function does across platforms? Can we strictly document all "weird cases" it handles?
  • There's also the whole story of if you want to whitelist syscalls changing this over time can be hard, but this dies into whether we can document what it does today.

cc @rust-lang/libs

@retep998
Copy link
Member

The MSVC implementation of std::experimental::filesystem::remove_all is interesting. Instead of iterating over all the entries in the directory, it just gets the first entry, removes it, and repeats until the directory is empty or it encounters an error.

@alexcrichton
Copy link
Member

Interesting! Is the source for that public so we can browse over?

@retep998
Copy link
Member

Well you can view the source if you install MSVC and the Windows SDK.

remove_all and _Remove_all in C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\include\filesystem
Some other helper functions in C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\crt\src\stl\filesys.h

@bors
Copy link
Contributor

bors commented Mar 23, 2016

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

@alexcrichton
Copy link
Member

Closing due to inactivity, but feel free to resubmit with a rebase!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

fs::remove_dir_all rarely succeeds for large directories on windows
6 participants