Code change:
pub use crate::sys_common::fs::remove_dir_all; } <del> // Dynamically choose implementation Macos x86-64: modern for 10.10+, fallback for older versions #[cfg(all(target_os = "macos", target_arch = "x86_64"))] </del> <ins> // Modern implementation using openat(), unlinkat() and fdopendir() #[cfg(not(any(target_os = "redox", target_os = "espidf")))] </ins> mod remove_dir_impl { <del> use super::{cstr, lstat, Dir, InnerReadDir, ReadDir}; </del> <ins> use super::{cstr, lstat, Dir, DirEntry, InnerReadDir, ReadDir}; </ins> use crate::ffi::CStr; use crate::io; use crate::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd}; use crate::os::unix::prelude::{OwnedFd, RawFd}; use crate::path::{Path, PathBuf}; use crate::sync::Arc; <del> use crate::sys::weak::weak; </del> use crate::sys::{cvt, cvt_r}; <del> use libc::{c_char, c_int, DIR}; pub fn openat_nofollow_dironly(parent_fd: Option<RawFd>, p: &CStr) -> io::Result<OwnedFd> { weak!(fn openat(c_int, *const c_char, c_int) -> c_int); let fd = cvt_r(|| unsafe { openat.get().unwrap()( parent_fd.unwrap_or(libc::AT_FDCWD), p.as_ptr(), libc::O_CLOEXEC | libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_DIRECTORY, ) })?; Ok(unsafe { OwnedFd::from_raw_fd(fd) }) } fn fdreaddir(dir_fd: OwnedFd) -> io::Result<(ReadDir, RawFd)> { weak!(fn fdopendir(c_int) -> *mut DIR, "fdopendir$INODE64"); let ptr = unsafe { fdopendir.get().unwrap()(dir_fd.as_raw_fd()) }; if ptr.is_null() { return Err(io::Error::last_os_error()); } let dirp = Dir(ptr); // file descriptor is automatically closed by libc::closedir() now, so give up ownership let new_parent_fd = dir_fd.into_raw_fd(); // a valid root is not needed because we do not call any functions involving the full path // of the DirEntrys. let dummy_root = PathBuf::new(); Ok(( ReadDir { inner: Arc::new(InnerReadDir { dirp, root: dummy_root }), end_of_stream: false, }, new_parent_fd, )) } fn remove_dir_all_recursive(parent_fd: Option<RawFd>, p: &Path) -> io::Result<()> { weak!(fn unlinkat(c_int, *const c_char, c_int) -> c_int); </del> <ins> #[cfg(not(all(target_os = "macos", target_arch = "x86_64"),))] use libc::{fdopendir, openat, unlinkat}; #[cfg(all(target_os = "macos", target_arch = "x86_64"))] use macos_weak::{fdopendir, openat, unlinkat}; </ins> <del> let pcstr = cstr(p)?; </del> <ins> #[cfg(all(target_os = "macos", target_arch = "x86_64"))] mod macos_weak { use crate::sys::weak::weak; use libc::{c_char, c_int, DIR}; </ins> <del> // entry is expected to be a directory, open as such let fd = openat_nofollow_dironly(parent_fd, &pcstr)?; </del> <ins> fn get_openat_fn() -> Option<unsafe extern "C" fn(c_int, *const c_char, c_int) -> c_int> { weak!(fn openat(c_int, *const c_char, c_int) -> c_int); openat.get() } </ins> <del> // open the directory passing ownership of the fd let (dir, fd) = fdreaddir(fd)?; for child in dir { let child = child?; match child.entry.d_type { libc::DT_DIR => { remove_dir_all_recursive(Some(fd), Path::new(&child.file_name()))?; } libc::DT_UNKNOWN => { match cvt(unsafe { unlinkat.get().unwrap()(fd, child.name_cstr().as_ptr(), 0) }) { // type unknown - try to unlink Err(err) if err.raw_os_error() == Some(libc::EPERM) => { // if the file is a directory unlink fails with EPERM remove_dir_all_recursive(Some(fd), Path::new(&child.file_name()))?; } result => { result?; } } } _ => { // not a directory -> unlink cvt(unsafe { unlinkat.get().unwrap()(fd, child.name_cstr().as_ptr(), 0) })?; } } </del> <ins> pub fn has_openat() -> bool { get_openat_fn().is_some() </ins> } <del> // unlink the directory after removing its contents cvt(unsafe { unlinkat.get().unwrap()( parent_fd.unwrap_or(libc::AT_FDCWD), pcstr.as_ptr(), libc::AT_REMOVEDIR, ) })?; Ok(()) } </del> <ins> pub unsafe fn openat(dirfd: c_int, pathname: *const c_char, flags: c_int) -> c_int { get_openat_fn().map(|openat| openat(dirfd, pathname, flags)).unwrap_or_else(|| { crate::sys::unix::os::set_errno(libc::ENOSYS); -1 }) } </ins> <del> fn remove_dir_all_modern(p: &Path) -> io::Result<()> { // We cannot just call remove_dir_all_recursive() here because that would not delete a passed // symlink. No need to worry about races, because remove_dir_all_recursive() does not recurse // into symlinks. let attr = lstat(p)?; if attr.file_type().is_symlink() { crate::fs::remove_file(p) } else { remove_dir_all_recursive(None, p) </del> <ins> pub unsafe fn fdopendir(fd: c_int) -> *mut DIR { weak!(fn fdopendir(c_int) -> *mut DIR, "fdopendir$INODE64"); fdopendir.get().map(|fdopendir| fdopendir(fd)).unwrap_or_else(|| { crate::sys::unix::os::set_errno(libc::ENOSYS); crate::ptr::null_mut() }) </ins> } <del> } pub fn remove_dir_all(p: &Path) -> io::Result<()> { weak!(fn openat(c_int, *const c_char, c_int) -> c_int); if openat.get().is_some() { // openat() is available with macOS 10.10+, just like unlinkat() and fdopendir() remove_dir_all_modern(p) } else { // fall back to classic implementation crate::sys_common::fs::remove_dir_all(p) </del> <ins> pub unsafe fn unlinkat(dirfd: c_int, pathname: *const c_char, flags: c_int) -> c_int { weak!(fn unlinkat(c_int, *const c_char, c_int) -> c_int); unlinkat.get().map(|unlinkat| unlinkat(dirfd, pathname, flags)).unwrap_or_else(|| { crate::sys::unix::os::set_errno(libc::ENOSYS); -1 }) </ins> } } <del> } // Modern implementation using openat(), unlinkat() and fdopendir() #[cfg(not(any( all(target_os = "macos", target_arch = "x86_64"), target_os = "redox", target_os = "espidf" )))] mod remove_dir_impl { use super::{cstr, lstat, Dir, DirEntry, InnerReadDir, ReadDir}; use crate::ffi::CStr; use crate::io; use crate::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd}; use crate::os::unix::prelude::{OwnedFd, RawFd}; use crate::path::{Path, PathBuf}; use crate::sync::Arc; use crate::sys::{cvt, cvt_r}; use libc::{fdopendir, openat, unlinkat}; </del> pub fn openat_nofollow_dironly(parent_fd: Option<RawFd>, p: &CStr) -> io::Result<OwnedFd> { let fd = cvt_r(|| unsafe {

Related discussion:
text: It was reported upstream, and we're working on a fix -- but that fix will obviously only be in new versions of the operating system. To expand further on the corruption: the file system becomes inconsistent and requires repair, and the damage such as it is can only be caused by privileged users; i.e., by root or a user with root equivalent privileges. I am not yet aware of a way that it would cause actual data loss, just an interruption to service, but anything is possible. Allowing the linking and unlinking of directories is a regrettable, but seemingly intentional feature we are saddled with through our UNIX heritage. I'm working to disable and eventually remove the feature in the OS -- although we strongly value backwards compatibility, in this case I can't really see a legitimate use case or find a consumer that would be broken by doing so, but we won't know for sure until we try. While this specific case is unfortunate, and we can and will work on our end to try to mitigate the impact, the fact remains that POSIX explicitly documents and allows for implementations where on a directory does not return . For the code to be completely correct on all POSIX systems, it cannot make that assumption. In addition, working around the issue in new versions of the OS doesn't fix existing deployments out there already. Enterprise customers are often reluctant to promptly update their operating system, even though they may update software built and deployed on it -- software that may be written in Rust. I myself hit this issue by upgrading only the Rust compiler to a new stable version, and trying to build and use Rust-based software we have to build UFS ramdisks on illumos. We aim to fix the OS for the future, but the critical regression in Rust needs to be fixed as well for the present. We appreciate everybody who has looked at this so far -- please let me know how I can help expedite the process!
Thanks again for the report. The linked PR should fix the issue. Apparently only and guarantee that trying to a directory fails. , and allow it, at least in theory. After looking around a bit I found that it is even explicitly documented for and . So it is not a bug but a feature there... After changing that in you might want to adapt the man page as well.