Change:
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.
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.
We can try to avoid this case by default, as is switching to try unknown as a directory first, but you should note that this is still going to be racy. If there's an unprivileged adversary like the CVE supposes, then they can try to race changes between "try (and fail) as a directory" to "unlink as a file", to make it unlink a directory after all and induce corruption. IOW, it sounds like it's not safe for a privileged process to unlink paths at all if they may be changed by lesser privileges .
It should not really cause corruption according to the , just orphaned directories: Of course this can lead to resource exhaustion since the disk space and the inodes cannot be reclaimed until the next .
And that user is only root or otherwise root-like? i.e. it's not just talking about regular access privileges?
For Illumos the process must have root privileges or needs to be in the process privilege set (). Solaris has at some point abolished this privilege and apparently does not allow unlinking directories anymore ().

Discussion:
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 {

fn remove_dir_all_recursive(parent_fd: Option<RawFd>, p: &Path) -> io::Result<()> { let pcstr = cstr(p)?; <del> // entry is expected to be a directory, open as such let fd = openat_nofollow_dironly(parent_fd, &pcstr)?; </del> <ins> // try opening as directory let fd = match openat_nofollow_dironly(parent_fd, &pcstr) { Err(err) if err.raw_os_error() == Some(libc::ENOTDIR) => { // not a directory - don't traverse further return match parent_fd { // unlink... Some(parent_fd) => { cvt(unsafe { unlinkat(parent_fd, pcstr.as_ptr(), 0) }).map(drop) } // ...unless this was supposed to be the deletion root directory None => Err(err), }; } result => result?, }; </ins> // open the directory passing ownership of the fd let (dir, fd) = fdreaddir(fd)?;

Some(false) => { cvt(unsafe { unlinkat(fd, child.name_cstr().as_ptr(), 0) })?; } <del> None => match cvt(unsafe { unlinkat(fd, child.name_cstr().as_ptr(), 0) }) { // type unknown - try to unlink Err(err) if err.raw_os_error() == Some(libc::EISDIR) || err.raw_os_error() == Some(libc::EPERM) => { // if the file is a directory unlink fails with EISDIR on Linux and EPERM everyhwere else remove_dir_all_recursive(Some(fd), Path::new(&child.file_name()))?; } result => { result?; } }, </del> <ins> 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), Path::new(&child.file_name()))?; } </ins> } }

Ok(()) } <del> pub fn remove_dir_all(p: &Path) -> io::Result<()> { </del> <ins> fn remove_dir_all_modern(p: &Path) -> io::Result<()> { </ins> // 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.

remove_dir_all_recursive(None, p) } } <ins> #[cfg(not(all(target_os = "macos", target_arch = "x86_64")))] pub fn remove_dir_all(p: &Path) -> io::Result<()> { remove_dir_all_modern(p) } #[cfg(all(target_os = "macos", target_arch = "x86_64"))] pub fn remove_dir_all(p: &Path) -> io::Result<()> { if macos_weak::has_openat() { // 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) } } </ins> }