Skip to content

Commit 93e708b

Browse files
committed
Use limited Readdir cache and use openat(dirf, "..", O_NOFOLLOW) to go up.
1 parent d2cfba1 commit 93e708b

File tree

1 file changed

+121
-37
lines changed
  • library/std/src/sys/unix

1 file changed

+121
-37
lines changed

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

+121-37
Original file line numberDiff line numberDiff line change
@@ -1478,13 +1478,15 @@ mod remove_dir_impl {
14781478
#[cfg(not(any(target_os = "redox", target_os = "espidf")))]
14791479
mod remove_dir_impl {
14801480
use super::{cstr, lstat, Dir, DirEntry, InnerReadDir, ReadDir};
1481-
use crate::ffi::CStr;
1481+
use crate::ffi::{CStr, CString};
14821482
use crate::io;
1483+
use crate::mem;
14831484
use crate::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd};
14841485
use crate::os::unix::prelude::{OwnedFd, RawFd};
14851486
use crate::path::{Path, PathBuf};
14861487
use crate::sync::Arc;
14871488
use crate::sys::{cvt, cvt_r};
1489+
use alloc::collections::VecDeque;
14881490

14891491
#[cfg(not(all(target_os = "macos", target_arch = "x86_64"),))]
14901492
use libc::{fdopendir, openat, unlinkat};
@@ -1518,6 +1520,23 @@ mod remove_dir_impl {
15181520
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
15191521
use macos_weak::{fdopendir, openat, unlinkat};
15201522

1523+
#[cfg(not(any(
1524+
target_os = "linux",
1525+
target_os = "emscripten",
1526+
target_os = "l4re",
1527+
target_os = "android"
1528+
)))]
1529+
use libc::fstat as fstat64;
1530+
#[cfg(any(
1531+
target_os = "linux",
1532+
target_os = "emscripten",
1533+
target_os = "l4re",
1534+
target_os = "android"
1535+
))]
1536+
use libc::fstat64;
1537+
1538+
const MAX_OPEN_FDS: usize = 32;
1539+
15211540
pub fn openat_nofollow_dironly(parent_fd: Option<RawFd>, p: &CStr) -> io::Result<OwnedFd> {
15221541
let fd = cvt_r(|| unsafe {
15231542
openat(
@@ -1605,55 +1624,120 @@ mod remove_dir_impl {
16051624
}
16061625
}
16071626

1608-
fn remove_dir_all_loop(p: &Path) -> io::Result<()> {
1609-
use crate::ffi::CString;
1627+
struct CachedReadDir {
1628+
readdir: ReadDir,
1629+
raw_fd: RawFd,
1630+
}
16101631

1611-
struct State {
1612-
dir: ReadDir,
1613-
fd: RawFd,
1614-
parent_fd: Option<RawFd>,
1615-
pcstr: CString,
1616-
}
1632+
struct DirComponent {
1633+
name: CString,
1634+
ino: u64,
1635+
}
16171636

1618-
impl State {
1619-
fn new(parent_fd: Option<RawFd>, pcstr: CString) -> io::Result<Self> {
1620-
// entry is expected to be a directory, open as such
1621-
let fd = openat_nofollow_dironly(parent_fd, &pcstr)?;
1637+
fn readdir_open_path(path: &CStr) -> io::Result<(DirComponent, CachedReadDir)> {
1638+
let dir_fd = openat_nofollow_dironly(None, path)?;
16221639

1623-
// open the directory passing ownership of the fd
1624-
let (dir, fd) = fdreaddir(fd)?;
1640+
// use fstat() to get the inode of the directory
1641+
let mut stat = unsafe { mem::zeroed() };
1642+
cvt(unsafe { fstat64(dir_fd.as_raw_fd(), &mut stat) })?;
1643+
let (readdir, raw_fd) = fdreaddir(dir_fd)?;
1644+
Ok((
1645+
DirComponent { name: CString::new("")?, ino: stat.st_ino },
1646+
CachedReadDir { readdir, raw_fd },
1647+
))
1648+
}
16251649

1626-
Ok(Self { dir, fd, parent_fd, pcstr })
1627-
}
1628-
}
1650+
fn readdir_open_child(
1651+
readdir: &CachedReadDir,
1652+
child: &DirEntry,
1653+
) -> io::Result<(DirComponent, CachedReadDir)> {
1654+
let dir_fd = openat_nofollow_dironly(Some(readdir.raw_fd), child.name_cstr())?;
1655+
let (readdir, raw_fd) = fdreaddir(dir_fd)?;
1656+
Ok((
1657+
DirComponent { name: child.name_cstr().into(), ino: child.entry.d_ino },
1658+
CachedReadDir { readdir, raw_fd },
1659+
))
1660+
}
16291661

1630-
let mut parents = Vec::<State>::new();
1631-
let mut current = State::new(None, cstr(p)?)?;
1662+
fn readdir_reopen_parent(
1663+
dir: &CachedReadDir,
1664+
expected_parent_dir: &DirComponent,
1665+
) -> io::Result<CachedReadDir> {
1666+
let parent_dir_fd = openat_nofollow_dironly(Some(dir.raw_fd), unsafe {
1667+
CStr::from_bytes_with_nul_unchecked(b"..\0")
1668+
})?;
1669+
let mut stat = unsafe { mem::zeroed() };
1670+
cvt(unsafe { fstat64(parent_dir_fd.as_raw_fd(), &mut stat) })?;
1671+
// Make sure that the reopened parent directory has the same inode as when we visited it descending
1672+
// the directory tree. More detailed risk analysis TBD.
1673+
if expected_parent_dir.ino != stat.st_ino {
1674+
return Err(io::Error::new(
1675+
io::ErrorKind::Uncategorized,
1676+
"parent directory inode does not match",
1677+
));
1678+
}
1679+
let (readdir, raw_fd) = fdreaddir(parent_dir_fd)?;
1680+
Ok(CachedReadDir { readdir, raw_fd })
1681+
}
1682+
1683+
fn remove_dir_all_loop(root: &Path) -> io::Result<()> {
1684+
// all ancestor names and inodes from the deletion root directory to the parent of the currently processed
1685+
// directory
1686+
let mut parent_dir_components = Vec::new();
1687+
// cache of up to MAX_OPEN_FDS ancestor ReadDirs and associated file descriptors
1688+
let mut readdir_cache = VecDeque::with_capacity(MAX_OPEN_FDS);
1689+
// the directory name, inode pair and ReadDir currently being processed
1690+
let (mut current_dir_component, mut current_readdir) = readdir_open_path(&cstr(root)?)?;
16321691
loop {
1633-
while let Some(child) = current.dir.next() {
1692+
while let Some(child) = current_readdir.readdir.next() {
16341693
let child = child?;
1635-
if !unlink_direntry(&child, current.fd)? {
1694+
if !unlink_direntry(&child, current_readdir.raw_fd)? {
16361695
// Descend into this child directory
1637-
let parent = current;
1638-
current = State::new(Some(parent.fd), child.name_cstr().into())?;
1639-
parents.push(parent);
1696+
1697+
let (child_dir_compoment, child_readdir) =
1698+
readdir_open_child(&current_readdir, &child)?;
1699+
parent_dir_components.push(current_dir_component);
1700+
1701+
// avoid growing the cache over capacity
1702+
if readdir_cache.len() == readdir_cache.capacity() {
1703+
readdir_cache.pop_front();
1704+
}
1705+
readdir_cache.push_back(current_readdir);
1706+
1707+
current_readdir = child_readdir;
1708+
current_dir_component = child_dir_compoment;
16401709
}
16411710
}
16421711

1643-
// unlink the directory after removing its contents
1644-
cvt(unsafe {
1645-
unlinkat(
1646-
current.parent_fd.unwrap_or(libc::AT_FDCWD),
1647-
current.pcstr.as_ptr(),
1648-
libc::AT_REMOVEDIR,
1649-
)
1650-
})?;
1651-
1652-
match parents.pop() {
1653-
Some(parent) => current = parent,
1654-
None => return Ok(()),
1712+
match parent_dir_components.pop() {
1713+
Some(parent) => {
1714+
// Going back up...
1715+
let parent_readdir = match readdir_cache.pop_back() {
1716+
Some(readdir) => readdir,
1717+
None => {
1718+
// not cached -> reopen
1719+
readdir_reopen_parent(&current_readdir, &parent)?
1720+
}
1721+
};
1722+
// unlink the directory after having removed its contents
1723+
cvt(unsafe {
1724+
unlinkat(
1725+
parent_readdir.raw_fd,
1726+
current_dir_component.name.as_ptr(),
1727+
libc::AT_REMOVEDIR,
1728+
)
1729+
})?;
1730+
1731+
current_dir_component = parent;
1732+
current_readdir = parent_readdir;
1733+
}
1734+
None => break,
16551735
}
16561736
}
1737+
1738+
// unlink root dir
1739+
cvt(unsafe { unlinkat(libc::AT_FDCWD, cstr(root)?.as_ptr(), libc::AT_REMOVEDIR) })?;
1740+
Ok(())
16571741
}
16581742

16591743
fn remove_dir_all_modern(p: &Path) -> io::Result<()> {

0 commit comments

Comments
 (0)