Skip to content

Commit 478851c

Browse files
authored
Rollup merge of rust-lang#94446 - rusticstuff:remove_dir_all-illumos-fix, r=cuviper
UNIX `remove_dir_all()`: Try recursing first on the slow path This only affects the _slow_ code path - if there is no `dirent.d_type` or if it is `DT_UNKNOWN`. POSIX specifies that calling `unlink()` or `unlinkat(..., 0)` on a directory is allowed to succeed: > The _path_ argument shall not name a directory unless the process has appropriate privileges and the implementation supports using _unlink()_ on directories. This however can cause dangling inodes requiring an fsck e.g. on Illumos UFS, so we have to avoid that in the common case. We now just try to recurse into it first and unlink() if we can't open it as a directory. The other two commits integrate the Macos x86-64 implementation reducing redundancy. Split into two commits for better reviewing. Fixes rust-lang#94335.
2 parents 69f11ff + 735f60c commit 478851c

File tree

1 file changed

+74
-131
lines changed
  • library/std/src/sys/unix

1 file changed

+74
-131
lines changed

library/std/src/sys/unix/fs.rs

+74-131
Original file line numberDiff line numberDiff line change
@@ -1482,140 +1482,60 @@ mod remove_dir_impl {
14821482
pub use crate::sys_common::fs::remove_dir_all;
14831483
}
14841484

1485-
// Dynamically choose implementation Macos x86-64: modern for 10.10+, fallback for older versions
1486-
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
1485+
// Modern implementation using openat(), unlinkat() and fdopendir()
1486+
#[cfg(not(any(target_os = "redox", target_os = "espidf")))]
14871487
mod remove_dir_impl {
1488-
use super::{cstr, lstat, Dir, InnerReadDir, ReadDir};
1488+
use super::{cstr, lstat, Dir, DirEntry, InnerReadDir, ReadDir};
14891489
use crate::ffi::CStr;
14901490
use crate::io;
14911491
use crate::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd};
14921492
use crate::os::unix::prelude::{OwnedFd, RawFd};
14931493
use crate::path::{Path, PathBuf};
14941494
use crate::sync::Arc;
1495-
use crate::sys::weak::weak;
14961495
use crate::sys::{cvt, cvt_r};
1497-
use libc::{c_char, c_int, DIR};
1498-
1499-
pub fn openat_nofollow_dironly(parent_fd: Option<RawFd>, p: &CStr) -> io::Result<OwnedFd> {
1500-
weak!(fn openat(c_int, *const c_char, c_int) -> c_int);
1501-
let fd = cvt_r(|| unsafe {
1502-
openat.get().unwrap()(
1503-
parent_fd.unwrap_or(libc::AT_FDCWD),
1504-
p.as_ptr(),
1505-
libc::O_CLOEXEC | libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_DIRECTORY,
1506-
)
1507-
})?;
1508-
Ok(unsafe { OwnedFd::from_raw_fd(fd) })
1509-
}
15101496

1511-
fn fdreaddir(dir_fd: OwnedFd) -> io::Result<(ReadDir, RawFd)> {
1512-
weak!(fn fdopendir(c_int) -> *mut DIR, "fdopendir$INODE64");
1513-
let ptr = unsafe { fdopendir.get().unwrap()(dir_fd.as_raw_fd()) };
1514-
if ptr.is_null() {
1515-
return Err(io::Error::last_os_error());
1516-
}
1517-
let dirp = Dir(ptr);
1518-
// file descriptor is automatically closed by libc::closedir() now, so give up ownership
1519-
let new_parent_fd = dir_fd.into_raw_fd();
1520-
// a valid root is not needed because we do not call any functions involving the full path
1521-
// of the DirEntrys.
1522-
let dummy_root = PathBuf::new();
1523-
Ok((
1524-
ReadDir {
1525-
inner: Arc::new(InnerReadDir { dirp, root: dummy_root }),
1526-
end_of_stream: false,
1527-
},
1528-
new_parent_fd,
1529-
))
1530-
}
1531-
1532-
fn remove_dir_all_recursive(parent_fd: Option<RawFd>, p: &Path) -> io::Result<()> {
1533-
weak!(fn unlinkat(c_int, *const c_char, c_int) -> c_int);
1497+
#[cfg(not(all(target_os = "macos", target_arch = "x86_64"),))]
1498+
use libc::{fdopendir, openat, unlinkat};
1499+
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
1500+
use macos_weak::{fdopendir, openat, unlinkat};
15341501

1535-
let pcstr = cstr(p)?;
1502+
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
1503+
mod macos_weak {
1504+
use crate::sys::weak::weak;
1505+
use libc::{c_char, c_int, DIR};
15361506

1537-
// entry is expected to be a directory, open as such
1538-
let fd = openat_nofollow_dironly(parent_fd, &pcstr)?;
1507+
fn get_openat_fn() -> Option<unsafe extern "C" fn(c_int, *const c_char, c_int) -> c_int> {
1508+
weak!(fn openat(c_int, *const c_char, c_int) -> c_int);
1509+
openat.get()
1510+
}
15391511

1540-
// open the directory passing ownership of the fd
1541-
let (dir, fd) = fdreaddir(fd)?;
1542-
for child in dir {
1543-
let child = child?;
1544-
match child.entry.d_type {
1545-
libc::DT_DIR => {
1546-
remove_dir_all_recursive(Some(fd), Path::new(&child.file_name()))?;
1547-
}
1548-
libc::DT_UNKNOWN => {
1549-
match cvt(unsafe { unlinkat.get().unwrap()(fd, child.name_cstr().as_ptr(), 0) })
1550-
{
1551-
// type unknown - try to unlink
1552-
Err(err) if err.raw_os_error() == Some(libc::EPERM) => {
1553-
// if the file is a directory unlink fails with EPERM
1554-
remove_dir_all_recursive(Some(fd), Path::new(&child.file_name()))?;
1555-
}
1556-
result => {
1557-
result?;
1558-
}
1559-
}
1560-
}
1561-
_ => {
1562-
// not a directory -> unlink
1563-
cvt(unsafe { unlinkat.get().unwrap()(fd, child.name_cstr().as_ptr(), 0) })?;
1564-
}
1565-
}
1512+
pub fn has_openat() -> bool {
1513+
get_openat_fn().is_some()
15661514
}
15671515

1568-
// unlink the directory after removing its contents
1569-
cvt(unsafe {
1570-
unlinkat.get().unwrap()(
1571-
parent_fd.unwrap_or(libc::AT_FDCWD),
1572-
pcstr.as_ptr(),
1573-
libc::AT_REMOVEDIR,
1574-
)
1575-
})?;
1576-
Ok(())
1577-
}
1516+
pub unsafe fn openat(dirfd: c_int, pathname: *const c_char, flags: c_int) -> c_int {
1517+
get_openat_fn().map(|openat| openat(dirfd, pathname, flags)).unwrap_or_else(|| {
1518+
crate::sys::unix::os::set_errno(libc::ENOSYS);
1519+
-1
1520+
})
1521+
}
15781522

1579-
fn remove_dir_all_modern(p: &Path) -> io::Result<()> {
1580-
// We cannot just call remove_dir_all_recursive() here because that would not delete a passed
1581-
// symlink. No need to worry about races, because remove_dir_all_recursive() does not recurse
1582-
// into symlinks.
1583-
let attr = lstat(p)?;
1584-
if attr.file_type().is_symlink() {
1585-
crate::fs::remove_file(p)
1586-
} else {
1587-
remove_dir_all_recursive(None, p)
1523+
pub unsafe fn fdopendir(fd: c_int) -> *mut DIR {
1524+
weak!(fn fdopendir(c_int) -> *mut DIR, "fdopendir$INODE64");
1525+
fdopendir.get().map(|fdopendir| fdopendir(fd)).unwrap_or_else(|| {
1526+
crate::sys::unix::os::set_errno(libc::ENOSYS);
1527+
crate::ptr::null_mut()
1528+
})
15881529
}
1589-
}
15901530

1591-
pub fn remove_dir_all(p: &Path) -> io::Result<()> {
1592-
weak!(fn openat(c_int, *const c_char, c_int) -> c_int);
1593-
if openat.get().is_some() {
1594-
// openat() is available with macOS 10.10+, just like unlinkat() and fdopendir()
1595-
remove_dir_all_modern(p)
1596-
} else {
1597-
// fall back to classic implementation
1598-
crate::sys_common::fs::remove_dir_all(p)
1531+
pub unsafe fn unlinkat(dirfd: c_int, pathname: *const c_char, flags: c_int) -> c_int {
1532+
weak!(fn unlinkat(c_int, *const c_char, c_int) -> c_int);
1533+
unlinkat.get().map(|unlinkat| unlinkat(dirfd, pathname, flags)).unwrap_or_else(|| {
1534+
crate::sys::unix::os::set_errno(libc::ENOSYS);
1535+
-1
1536+
})
15991537
}
16001538
}
1601-
}
1602-
1603-
// Modern implementation using openat(), unlinkat() and fdopendir()
1604-
#[cfg(not(any(
1605-
all(target_os = "macos", target_arch = "x86_64"),
1606-
target_os = "redox",
1607-
target_os = "espidf"
1608-
)))]
1609-
mod remove_dir_impl {
1610-
use super::{cstr, lstat, Dir, DirEntry, InnerReadDir, ReadDir};
1611-
use crate::ffi::CStr;
1612-
use crate::io;
1613-
use crate::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd};
1614-
use crate::os::unix::prelude::{OwnedFd, RawFd};
1615-
use crate::path::{Path, PathBuf};
1616-
use crate::sync::Arc;
1617-
use crate::sys::{cvt, cvt_r};
1618-
use libc::{fdopendir, openat, unlinkat};
16191539

16201540
pub fn openat_nofollow_dironly(parent_fd: Option<RawFd>, p: &CStr) -> io::Result<OwnedFd> {
16211541
let fd = cvt_r(|| unsafe {
@@ -1683,8 +1603,21 @@ mod remove_dir_impl {
16831603
fn remove_dir_all_recursive(parent_fd: Option<RawFd>, p: &Path) -> io::Result<()> {
16841604
let pcstr = cstr(p)?;
16851605

1686-
// entry is expected to be a directory, open as such
1687-
let fd = openat_nofollow_dironly(parent_fd, &pcstr)?;
1606+
// try opening as directory
1607+
let fd = match openat_nofollow_dironly(parent_fd, &pcstr) {
1608+
Err(err) if err.raw_os_error() == Some(libc::ENOTDIR) => {
1609+
// not a directory - don't traverse further
1610+
return match parent_fd {
1611+
// unlink...
1612+
Some(parent_fd) => {
1613+
cvt(unsafe { unlinkat(parent_fd, pcstr.as_ptr(), 0) }).map(drop)
1614+
}
1615+
// ...unless this was supposed to be the deletion root directory
1616+
None => Err(err),
1617+
};
1618+
}
1619+
result => result?,
1620+
};
16881621

16891622
// open the directory passing ownership of the fd
16901623
let (dir, fd) = fdreaddir(fd)?;
@@ -1697,19 +1630,13 @@ mod remove_dir_impl {
16971630
Some(false) => {
16981631
cvt(unsafe { unlinkat(fd, child.name_cstr().as_ptr(), 0) })?;
16991632
}
1700-
None => match cvt(unsafe { unlinkat(fd, child.name_cstr().as_ptr(), 0) }) {
1701-
// type unknown - try to unlink
1702-
Err(err)
1703-
if err.raw_os_error() == Some(libc::EISDIR)
1704-
|| err.raw_os_error() == Some(libc::EPERM) =>
1705-
{
1706-
// if the file is a directory unlink fails with EISDIR on Linux and EPERM everyhwere else
1707-
remove_dir_all_recursive(Some(fd), Path::new(&child.file_name()))?;
1708-
}
1709-
result => {
1710-
result?;
1711-
}
1712-
},
1633+
None => {
1634+
// POSIX specifies that calling unlink()/unlinkat(..., 0) on a directory can succeed
1635+
// if the process has the appropriate privileges. This however can causing orphaned
1636+
// directories requiring an fsck e.g. on Solaris and Illumos. So we try recursing
1637+
// into it first instead of trying to unlink() it.
1638+
remove_dir_all_recursive(Some(fd), Path::new(&child.file_name()))?;
1639+
}
17131640
}
17141641
}
17151642

@@ -1720,7 +1647,7 @@ mod remove_dir_impl {
17201647
Ok(())
17211648
}
17221649

1723-
pub fn remove_dir_all(p: &Path) -> io::Result<()> {
1650+
fn remove_dir_all_modern(p: &Path) -> io::Result<()> {
17241651
// We cannot just call remove_dir_all_recursive() here because that would not delete a passed
17251652
// symlink. No need to worry about races, because remove_dir_all_recursive() does not recurse
17261653
// into symlinks.
@@ -1731,4 +1658,20 @@ mod remove_dir_impl {
17311658
remove_dir_all_recursive(None, p)
17321659
}
17331660
}
1661+
1662+
#[cfg(not(all(target_os = "macos", target_arch = "x86_64")))]
1663+
pub fn remove_dir_all(p: &Path) -> io::Result<()> {
1664+
remove_dir_all_modern(p)
1665+
}
1666+
1667+
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
1668+
pub fn remove_dir_all(p: &Path) -> io::Result<()> {
1669+
if macos_weak::has_openat() {
1670+
// openat() is available with macOS 10.10+, just like unlinkat() and fdopendir()
1671+
remove_dir_all_modern(p)
1672+
} else {
1673+
// fall back to classic implementation
1674+
crate::sys_common::fs::remove_dir_all(p)
1675+
}
1676+
}
17341677
}

0 commit comments

Comments
 (0)