-
Notifications
You must be signed in to change notification settings - Fork 13.4k
fix: fs::remove_dir_all: treat internal ENOENT as success #127623
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -2029,6 +2029,7 @@ mod remove_dir_impl { | |||||||||||||||||||||||||||||||||
use crate::path::{Path, PathBuf}; | ||||||||||||||||||||||||||||||||||
use crate::sys::common::small_c_string::run_path_with_cstr; | ||||||||||||||||||||||||||||||||||
use crate::sys::{cvt, cvt_r}; | ||||||||||||||||||||||||||||||||||
use crate::sys_common::ignore_notfound; | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
pub fn openat_nofollow_dironly(parent_fd: Option<RawFd>, p: &CStr) -> io::Result<OwnedFd> { | ||||||||||||||||||||||||||||||||||
let fd = cvt_r(|| unsafe { | ||||||||||||||||||||||||||||||||||
|
@@ -2082,6 +2083,16 @@ mod remove_dir_impl { | |||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
fn is_enoent(result: &io::Result<()>) -> bool { | ||||||||||||||||||||||||||||||||||
if let Err(err) = result | ||||||||||||||||||||||||||||||||||
&& matches!(err.raw_os_error(), Some(libc::ENOENT)) | ||||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||||
true | ||||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||||
false | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
Comment on lines
+2086
to
+2094
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
With this function, you can simply wrap the relevant function calls in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this seems clever, but it doesn't work for a few reasons:
using try blocks doesn't have either of these issues. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
As far as I can tell, this actually works with the function. It'd treat removal failed with
I actually feel the opposite way, it's better if we annotate all the places where we treat E.g. the recursive call does not need to ignore There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
actually, maybe we should ignore the error in the recursive call, but not ignore any ENOENT errors outside of the for loop, as top-level ENOENT errors should be returned (currently this happens in |
||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
fn remove_dir_all_recursive(parent_fd: Option<RawFd>, path: &CStr) -> io::Result<()> { | ||||||||||||||||||||||||||||||||||
// try opening as directory | ||||||||||||||||||||||||||||||||||
let fd = match openat_nofollow_dironly(parent_fd, &path) { | ||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||
|
@@ -2105,27 +2116,35 @@ mod remove_dir_impl { | |||||||||||||||||||||||||||||||||
for child in dir { | ||||||||||||||||||||||||||||||||||
let child = child?; | ||||||||||||||||||||||||||||||||||
let child_name = child.name_cstr(); | ||||||||||||||||||||||||||||||||||
match is_dir(&child) { | ||||||||||||||||||||||||||||||||||
Some(true) => { | ||||||||||||||||||||||||||||||||||
remove_dir_all_recursive(Some(fd), child_name)?; | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
Some(false) => { | ||||||||||||||||||||||||||||||||||
cvt(unsafe { unlinkat(fd, child_name.as_ptr(), 0) })?; | ||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ignore_enoent(cvt(unsafe { unlinkat(fd, child_name.as_ptr(), 0) }))?; |
||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
None => { | ||||||||||||||||||||||||||||||||||
// POSIX specifies that calling unlink()/unlinkat(..., 0) on a directory can succeed | ||||||||||||||||||||||||||||||||||
// if the process has the appropriate privileges. This however can causing orphaned | ||||||||||||||||||||||||||||||||||
// directories requiring an fsck e.g. on Solaris and Illumos. So we try recursing | ||||||||||||||||||||||||||||||||||
// into it first instead of trying to unlink() it. | ||||||||||||||||||||||||||||||||||
remove_dir_all_recursive(Some(fd), child_name)?; | ||||||||||||||||||||||||||||||||||
// we need an inner try block, because if one of these | ||||||||||||||||||||||||||||||||||
// directories has already been deleted, then we need to | ||||||||||||||||||||||||||||||||||
// continue the loop, not return ok. | ||||||||||||||||||||||||||||||||||
let result: io::Result<()> = try { | ||||||||||||||||||||||||||||||||||
match is_dir(&child) { | ||||||||||||||||||||||||||||||||||
Some(true) => { | ||||||||||||||||||||||||||||||||||
remove_dir_all_recursive(Some(fd), child_name)?; | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
Some(false) => { | ||||||||||||||||||||||||||||||||||
cvt(unsafe { unlinkat(fd, child_name.as_ptr(), 0) })?; | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
None => { | ||||||||||||||||||||||||||||||||||
// POSIX specifies that calling unlink()/unlinkat(..., 0) on a directory can succeed | ||||||||||||||||||||||||||||||||||
// if the process has the appropriate privileges. This however can causing orphaned | ||||||||||||||||||||||||||||||||||
// directories requiring an fsck e.g. on Solaris and Illumos. So we try recursing | ||||||||||||||||||||||||||||||||||
// into it first instead of trying to unlink() it. | ||||||||||||||||||||||||||||||||||
remove_dir_all_recursive(Some(fd), child_name)?; | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||
if result.is_err() && !is_enoent(&result) { | ||||||||||||||||||||||||||||||||||
return result; | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
// unlink the directory after removing its contents | ||||||||||||||||||||||||||||||||||
cvt(unsafe { | ||||||||||||||||||||||||||||||||||
ignore_notfound(cvt(unsafe { | ||||||||||||||||||||||||||||||||||
unlinkat(parent_fd.unwrap_or(libc::AT_FDCWD), path.as_ptr(), libc::AT_REMOVEDIR) | ||||||||||||||||||||||||||||||||||
})?; | ||||||||||||||||||||||||||||||||||
}))?; | ||||||||||||||||||||||||||||||||||
Ok(()) | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
|
jieyouxu marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
//@ ignore-windows | ||
lolbinarycat marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// This test attempts to make sure that running `remove_dir_all` | ||
// doesn't result in a NotFound error one of the files it | ||
// is deleting is deleted concurrently. | ||
// | ||
// The windows implementation for `remove_dir_all` is significantly | ||
// more complicated, and has not yet been brought up to par with | ||
// the implementation on other platforms, so this test is marked as | ||
// `ignore-windows` until someone more expirenced with windows can | ||
// sort that out. | ||
|
||
use std::fs::remove_dir_all; | ||
use std::path::Path; | ||
use std::thread; | ||
use std::time::Duration; | ||
|
||
use run_make_support::rfs::{create_dir, write}; | ||
use run_make_support::run_in_tmpdir; | ||
|
||
fn main() { | ||
let mut race_happened = false; | ||
run_in_tmpdir(|| { | ||
for i in 0..150 { | ||
create_dir("outer"); | ||
create_dir("outer/inner"); | ||
write("outer/inner.txt", b"sometext"); | ||
|
||
thread::scope(|scope| { | ||
let t1 = scope.spawn(|| { | ||
thread::sleep(Duration::from_nanos(i)); | ||
remove_dir_all("outer").unwrap(); | ||
}); | ||
|
||
let race_happened_ref = &race_happened; | ||
let t2 = scope.spawn(|| { | ||
let r1 = remove_dir_all("outer/inner"); | ||
let r2 = remove_dir_all("outer/inner.txt"); | ||
if r1.is_ok() && r2.is_err() { | ||
race_happened = true; | ||
} | ||
}); | ||
}); | ||
|
||
assert!(!Path::new("outer").exists()); | ||
|
||
// trying to remove a nonexistant top-level directory should | ||
// still result in an error. | ||
let Err(err) = remove_dir_all("outer") else { | ||
panic!("removing nonexistant dir did not result in an error"); | ||
}; | ||
assert_eq!(err.kind(), std::io::ErrorKind::NotFound); | ||
} | ||
}); | ||
if !race_happened { | ||
eprintln!( | ||
"WARNING: multithreaded deletion never raced, \ | ||
try increasing the number of attempts or \ | ||
adjusting the sleep timing" | ||
); | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.