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: The fix for CVE-2022- (, ) appears to have introduced a regression that can cause file system corruption on some UNIX systems. The new code will attempt to use in any case where it does not know what type of file a directory entry represents. Some systems do not provide that information along with the entry names returned while reading a directory, and even on systems which provide a type hint, that hint may be . In cases where we do not know the type of the entry, the code tries first to unlink it as if it were something other than a directory: The code assumes that on a directory will fail with , or on Linux with the non-standard , but this is not always the case. POSIX does suggests as a failure in some, but not all cases: (See in The Open Group Base Specifications Issue 7, 2018 edition) Implementations are not required to prohibit on directories, and indeed UFS file systems on present day illumos and presumably at least some Solaris systems allow of a directory if the user has sufficient privileges. Other platforms may as well. Unfortunately unlinking a directory on UFS causes a sort of file system corruption that requires an unmount and a trip through to correct. Any code that uses on UFS has since started causing that kind of corruption, which took a bit of work to track down. I think the fix is probably relatively simple, and has the benefit of not touching the code path where we believe we know what sort of entry we are removing: In short, we should try to first, rather than first, as that always fails in a safe way that we can detect. <!-- TRIAGEBOTSTART --<!-- TRIAGEBOTASSIGNSTART --<!-- TRIAGEBOTASSIGNDATASTARTTRIAGEBOTASSIGNDATAEND --<!-- TRIAGEBOTASSIGNEND --<!-- TRIAGEBOT_END --
cc (as you're the one how wrote the new code)
Thanks for the ping claim
Assigning priority as discussed in the of the Prioritization Working Group. label -I-prioritize +P-critical
Was this issue reported upstream? Filesystems corrupting themselves seem like a poor failure mode in any case, and in some ways is a violation of the sandboxing that the kernel ought to provide for the user-space applications.