diff --git a/library/std/src/fs/tests.rs b/library/std/src/fs/tests.rs index d74f0f00e46f4..6c52f4986b998 100644 --- a/library/std/src/fs/tests.rs +++ b/library/std/src/fs/tests.rs @@ -1707,3 +1707,26 @@ fn test_file_times() { assert_eq!(metadata.created().unwrap(), created); } } + +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn deep_traversal() -> crate::io::Result<()> { + use crate::fs::{create_dir_all, metadata, remove_dir_all, write}; + use crate::iter::repeat; + + let tmpdir = tmpdir(); + + let segment = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; + + let mut dir = tmpdir.join(segment); + repeat(segment).take(100).for_each(|name| dir.push(name)); + assert!(dir.as_os_str().len() > libc::PATH_MAX as usize); + let file = dir.join("b"); + + create_dir_all(&dir).expect("deep create tailed"); + write(&file, "foo").expect("deep write failed"); + metadata(&file).expect("deep stat failed"); + remove_dir_all(&dir).expect("deep remove failed"); + + Ok(()) +} diff --git a/library/std/src/lib.rs b/library/std/src/lib.rs index 9038e8fa9d7aa..0258326610be3 100644 --- a/library/std/src/lib.rs +++ b/library/std/src/lib.rs @@ -296,6 +296,7 @@ #![feature(int_roundings)] #![feature(ip)] #![feature(ip_in_core)] +#![feature(iter_advance_by)] #![feature(maybe_uninit_slice)] #![feature(maybe_uninit_uninit_array)] #![feature(maybe_uninit_write_slice)] diff --git a/library/std/src/sys/common/small_c_string.rs b/library/std/src/sys/common/small_c_string.rs index 963d17a47e4c0..1278e6f5beb14 100644 --- a/library/std/src/sys/common/small_c_string.rs +++ b/library/std/src/sys/common/small_c_string.rs @@ -1,6 +1,5 @@ -use crate::ffi::{CStr, CString}; +use crate::ffi::{CStr, CString, OsStr}; use crate::mem::MaybeUninit; -use crate::path::Path; use crate::slice; use crate::{io, ptr}; @@ -15,11 +14,11 @@ const NUL_ERR: io::Error = io::const_io_error!(io::ErrorKind::InvalidInput, "file name contained an unexpected NUL byte"); #[inline] -pub fn run_path_with_cstr(path: &Path, f: F) -> io::Result +pub fn run_path_with_cstr(path: &(impl AsRef + ?Sized), f: F) -> io::Result where F: FnOnce(&CStr) -> io::Result, { - run_with_cstr(path.as_os_str().as_os_str_bytes(), f) + run_with_cstr(path.as_ref().as_os_str_bytes(), f) } #[inline] diff --git a/library/std/src/sys/unix/fs.rs b/library/std/src/sys/unix/fs.rs index a5604c92a80ba..645948fc9a622 100644 --- a/library/std/src/sys/unix/fs.rs +++ b/library/std/src/sys/unix/fs.rs @@ -43,6 +43,8 @@ use libc::c_char; use libc::dirfd; #[cfg(any(target_os = "linux", target_os = "emscripten"))] use libc::fstatat64; +#[cfg(all(miri, any(target_os = "linux")))] +use libc::open64; #[cfg(any( target_os = "android", target_os = "solaris", @@ -86,7 +88,11 @@ use libc::{ lstat as lstat64, off_t as off64_t, open as open64, stat as stat64, }; #[cfg(any(target_os = "linux", target_os = "emscripten", target_os = "l4re"))] -use libc::{dirent64, fstat64, ftruncate64, lseek64, lstat64, off64_t, open64, stat64}; +use libc::{dirent64, fstat64, ftruncate64, lseek64, lstat64, off64_t, stat64}; + +// FIXME: port this to other unices that support *at syscalls +#[cfg(all(not(miri), any(target_os = "linux", target_os = "android")))] +mod dir_fd; pub use crate::sys_common::fs::try_exists; @@ -141,7 +147,8 @@ cfg_has_statx! {{ // Default `stat64` contains no creation time and may have 32-bit `time_t`. unsafe fn try_statx( fd: c_int, - path: *const c_char, + raw_path: *const c_char, + path: &Path, flags: i32, mask: u32, ) -> Option> { @@ -169,19 +176,17 @@ cfg_has_statx! {{ } let mut buf: libc::statx = mem::zeroed(); - if let Err(err) = cvt(statx(fd, path, flags, mask, &mut buf)) { - if STATX_SAVED_STATE.load(Ordering::Relaxed) == STATX_STATE::Present as u8 { - return Some(Err(err)); - } + let result = match cvt(statx(fd, raw_path, flags, mask, &mut buf)) { + o @ Ok(_) => o, + e @ Err(_) if STATX_SAVED_STATE.load(Ordering::Relaxed) == STATX_STATE::Present as u8 => e, // Availability not checked yet. // // First try the cheap way. - if err.raw_os_error() == Some(libc::ENOSYS) { + Err(err) if err.raw_os_error() == Some(libc::ENOSYS) => { STATX_SAVED_STATE.store(STATX_STATE::Unavailable as u8, Ordering::Relaxed); - return None; - } - + return None + }, // Error other than `ENOSYS` is not a good enough indicator -- it is // known that `EPERM` can be returned as a result of using seccomp to // block the syscall. @@ -192,16 +197,27 @@ cfg_has_statx! {{ // previous iteration of the code checked it for all errors and for now // this is retained. // FIXME what about transient conditions like `ENOMEM`? - let err2 = cvt(statx(0, ptr::null(), 0, libc::STATX_ALL, ptr::null_mut())) - .err() - .and_then(|e| e.raw_os_error()); - if err2 == Some(libc::EFAULT) { - STATX_SAVED_STATE.store(STATX_STATE::Present as u8, Ordering::Relaxed); - return Some(Err(err)); - } else { - STATX_SAVED_STATE.store(STATX_STATE::Unavailable as u8, Ordering::Relaxed); - return None; + e @ Err(_) => { + let err2 = cvt(statx(0, ptr::null(), 0, libc::STATX_ALL, ptr::null_mut())) + .err() + .and_then(|e| e.raw_os_error()); + if err2 == Some(libc::EFAULT) { + STATX_SAVED_STATE.store(STATX_STATE::Present as u8, Ordering::Relaxed); + e + } else { + STATX_SAVED_STATE.store(STATX_STATE::Unavailable as u8, Ordering::Relaxed); + return None + } } + }; + + let result = long_filename_fallback!(path, result, |dirfd, file_name| { + // FIXME: use libc::AT_EMPTY_PATH + cvt(statx(dirfd.as_raw_fd(), file_name.as_ptr(), flags, mask, &mut buf)) + }); + + if let Err(err) = result { + return Some(Err(err)); } // We cannot fill `stat64` exhaustively because of private padding fields. @@ -259,9 +275,25 @@ pub struct ReadDir { } impl ReadDir { + #[cfg(not(any(target_os = "android", target_os = "linux")))] fn new(inner: InnerReadDir) -> Self { Self { inner: Arc::new(inner), end_of_stream: false } } + + #[cfg(any(target_os = "android", target_os = "linux"))] + fn from_dirp(ptr: *mut libc::DIR, root: PathBuf) -> ReadDir { + let inner = InnerReadDir { dirp: Dir(ptr), root }; + ReadDir { + inner: Arc::new(inner), + #[cfg(not(any( + target_os = "solaris", + target_os = "illumos", + target_os = "fuchsia", + target_os = "redox", + )))] + end_of_stream: false, + } + } } struct Dir(*mut libc::DIR); @@ -820,6 +852,7 @@ impl DirEntry { if let Some(ret) = unsafe { try_statx( fd, name, + self.file_name_os_str().as_ref(), libc::AT_SYMLINK_NOFOLLOW | libc::AT_STATX_SYNC_AS_STAT, libc::STATX_ALL, ) } { @@ -1052,19 +1085,43 @@ impl OpenOptions { impl File { pub fn open(path: &Path, opts: &OpenOptions) -> io::Result { - run_path_with_cstr(path, |path| File::open_c(path, opts)) + let result = run_path_with_cstr(path, |path| File::open_c(None, &path, opts)); + + #[cfg(all(not(miri), any(target_os = "linux", target_os = "android")))] + let result = { + use crate::io::ErrorKind; + match result { + Ok(file) => Ok(file), + Err(e) if e.kind() == ErrorKind::InvalidFilename => { + dir_fd::open_deep(None, path, opts) + } + Err(e) => Err(e), + } + }; + + result } - pub fn open_c(path: &CStr, opts: &OpenOptions) -> io::Result { + #[cfg(any(miri, not(any(target_os = "linux", target_os = "android"))))] + pub fn open_c( + dirfd: Option>, + path: &CStr, + opts: &OpenOptions, + ) -> io::Result { let flags = libc::O_CLOEXEC | opts.get_access_mode()? | opts.get_creation_mode()? | (opts.custom_flags as c_int & !libc::O_ACCMODE); - // The third argument of `open64` is documented to have type `mode_t`. On - // some platforms (like macOS, where `open64` is actually `open`), `mode_t` is `u16`. - // However, since this is a variadic function, C integer promotion rules mean that on - // the ABI level, this still gets passed as `c_int` (aka `u32` on Unix platforms). - let fd = cvt_r(|| unsafe { open64(path.as_ptr(), flags, opts.mode as c_int) })?; + let fd = match dirfd { + None => { + // The third argument of `open64` is documented to have type `mode_t`. On + // some platforms (like macOS, where `open64` is actually `open`), `mode_t` is `u16`. + // However, since this is a variadic function, C integer promotion rules mean that on + // the ABI level, this still gets passed as `c_int` (aka `u32` on Unix platforms). + cvt_r(|| unsafe { open64(path.as_ptr(), flags, opts.mode as c_int) })? + } + Some(dirfd) => return super::unsupported::unsupported(), + }; Ok(File(unsafe { FileDesc::from_raw_fd(fd) })) } @@ -1075,6 +1132,7 @@ impl File { if let Some(ret) = unsafe { try_statx( fd, b"\0" as *const _ as *const c_char, + "".as_ref(), libc::AT_EMPTY_PATH | libc::AT_STATX_SYNC_AS_STAT, libc::STATX_ALL, ) } { @@ -1322,7 +1380,13 @@ impl DirBuilder { } pub fn mkdir(&self, p: &Path) -> io::Result<()> { - run_path_with_cstr(p, |p| cvt(unsafe { libc::mkdir(p.as_ptr(), self.mode) }).map(|_| ())) + let result = run_path_with_cstr(p, |p| cvt(unsafe { libc::mkdir(p.as_ptr(), self.mode) })); + + let result = long_filename_fallback!(p, result, |dirfd, file_name| { + cvt(unsafe { libc::mkdirat(dirfd.as_raw_fd(), file_name.as_ptr(), self.mode) }) + }); + + result.map(|_| ()) } pub fn set_mode(&mut self, mode: u32) { @@ -1499,19 +1563,49 @@ impl fmt::Debug for File { } } -pub fn readdir(path: &Path) -> io::Result { - let ptr = run_path_with_cstr(path, |p| unsafe { Ok(libc::opendir(p.as_ptr())) })?; +fn cvt_p(ptr: *mut T) -> io::Result<*mut T> { if ptr.is_null() { - Err(Error::last_os_error()) - } else { - let root = path.to_path_buf(); - let inner = InnerReadDir { dirp: Dir(ptr), root }; - Ok(ReadDir::new(inner)) + return Err(Error::last_os_error()); } + Ok(ptr) } -pub fn unlink(p: &Path) -> io::Result<()> { - run_path_with_cstr(p, |p| cvt(unsafe { libc::unlink(p.as_ptr()) }).map(|_| ())) +pub fn readdir(path: &Path) -> io::Result { + let root = path.to_path_buf(); + let ptr = cvt_p(run_path_with_cstr(path, |p| unsafe { Ok(libc::opendir(p.as_ptr())) })?); + + let ptr = match ptr { + #[cfg(any(target_os = "linux", target_os = "android"))] + Err(e) if e.kind() == crate::io::ErrorKind::InvalidFilename => { + let mut opts = OpenOptions::new(); + opts.read(true); + opts.custom_flags(libc::O_DIRECTORY); + let fd = File::open(path, &opts)?.into_raw_fd(); + cvt_p(unsafe { libc::fdopendir(fd) }) + } + other @ _ => other, + }?; + + let inner = InnerReadDir { dirp: Dir(ptr), root }; + Ok(ReadDir { + inner: Arc::new(inner), + #[cfg(not(any( + target_os = "solaris", + target_os = "illumos", + target_os = "fuchsia", + target_os = "redox", + )))] + end_of_stream: false, + }) +} + +pub fn unlink(path: &Path) -> io::Result<()> { + let result = run_path_with_cstr(path, |p| cvt(unsafe { libc::unlink(p.as_ptr()) })); + let result = long_filename_fallback!(path, result, |dirfd, file_name| { + cvt(unsafe { libc::unlinkat(dirfd.as_raw_fd(), file_name.as_ptr(), 0) }) + }); + + result.map(|_| ()) } pub fn rename(old: &Path, new: &Path) -> io::Result<()> { @@ -1526,8 +1620,15 @@ pub fn set_perm(p: &Path, perm: FilePermissions) -> io::Result<()> { run_path_with_cstr(p, |p| cvt_r(|| unsafe { libc::chmod(p.as_ptr(), perm.mode) }).map(|_| ())) } -pub fn rmdir(p: &Path) -> io::Result<()> { - run_path_with_cstr(p, |p| cvt(unsafe { libc::rmdir(p.as_ptr()) }).map(|_| ())) +pub fn rmdir(path: &Path) -> io::Result<()> { + let result = run_path_with_cstr(path, |p| cvt(unsafe { libc::rmdir(p.as_ptr()) })); + + #[cfg(any(target_os = "linux", target_os = "android"))] + let result = long_filename_fallback!(path, result, |dirfd, file_name| { + cvt(unsafe { libc::unlinkat(dirfd.as_raw_fd(), file_name.as_ptr(), libc::AT_REMOVEDIR) }) + }); + + result.map(|_| ()) } pub fn readlink(p: &Path) -> io::Result { @@ -1602,12 +1703,20 @@ pub fn link(original: &Path, link: &Path) -> io::Result<()> { }) } -pub fn stat(p: &Path) -> io::Result { - run_path_with_cstr(p, |p| { +// On linux this is the default behavior for lstat and stat but it has to be set explicitly for fstatat +#[cfg(any(target_os = "linux", target_os = "android"))] +const DEFAULT_STATAT_FLAGS: c_int = libc::AT_NO_AUTOMOUNT; + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +const DEFAULT_STATAT_FLAGS: c_int = 0; + +pub fn stat(path: &Path) -> io::Result { + run_path_with_cstr(path, |p| { cfg_has_statx! { if let Some(ret) = unsafe { try_statx( libc::AT_FDCWD, p.as_ptr(), + path, libc::AT_STATX_SYNC_AS_STAT, libc::STATX_ALL, ) } { @@ -1616,17 +1725,23 @@ pub fn stat(p: &Path) -> io::Result { } let mut stat: stat64 = unsafe { mem::zeroed() }; - cvt(unsafe { stat64(p.as_ptr(), &mut stat) })?; + let result = cvt(unsafe { stat64(p.as_ptr(), &mut stat) }); + long_filename_fallback!(path, result, |dirfd, file_name| { + cvt(unsafe { + fstatat64(dirfd.as_raw_fd(), file_name.as_ptr(), &mut stat, DEFAULT_STATAT_FLAGS) + }) + })?; Ok(FileAttr::from_stat64(stat)) }) } -pub fn lstat(p: &Path) -> io::Result { - run_path_with_cstr(p, |p| { +pub fn lstat(path: &Path) -> io::Result { + run_path_with_cstr(path, |p| { cfg_has_statx! { if let Some(ret) = unsafe { try_statx( libc::AT_FDCWD, p.as_ptr(), + path, libc::AT_SYMLINK_NOFOLLOW | libc::AT_STATX_SYNC_AS_STAT, libc::STATX_ALL, ) } { @@ -1635,7 +1750,17 @@ pub fn lstat(p: &Path) -> io::Result { } let mut stat: stat64 = unsafe { mem::zeroed() }; - cvt(unsafe { lstat64(p.as_ptr(), &mut stat) })?; + let result = cvt(unsafe { lstat64(p.as_ptr(), &mut stat) }); + long_filename_fallback!(path, result, |dirfd, file_name| { + cvt(unsafe { + fstatat64( + dirfd.as_raw_fd(), + file_name.as_ptr(), + &mut stat, + DEFAULT_STATAT_FLAGS | libc::AT_SYMLINK_NOFOLLOW, + ) + }) + })?; Ok(FileAttr::from_stat64(stat)) }) } @@ -1654,6 +1779,17 @@ pub fn canonicalize(p: &Path) -> io::Result { }))) } +macro long_filename_fallback($path:expr, $result:expr, $fallback:expr) {{ + cfg_if::cfg_if! { + // miri doesn't support the *at syscalls + if #[cfg(all(not(miri), any(target_os = "linux", target_os = "android")))] { + dir_fd::long_filename_fallback($result, $path, $fallback) + } else { + $result + } + } +}} + fn open_from(from: &Path) -> io::Result<(crate::fs::File, crate::fs::Metadata)> { use crate::fs::File; use crate::sys_common::fs::NOT_FILE_ERROR; @@ -1683,7 +1819,6 @@ fn open_to_and_set_permissions( reader_metadata: crate::fs::Metadata, ) -> io::Result<(crate::fs::File, crate::fs::Metadata)> { use crate::fs::OpenOptions; - use crate::os::unix::fs::{OpenOptionsExt, PermissionsExt}; let perm = reader_metadata.permissions(); let writer = OpenOptions::new() @@ -1888,6 +2023,11 @@ mod remove_dir_impl { pub use crate::sys_common::fs::remove_dir_all; } +#[cfg(all(not(miri), any(target_os = "linux", target_os = "android")))] +mod remove_dir_impl { + pub use super::dir_fd::remove_dir_all; +} + // Modern implementation using openat(), unlinkat() and fdopendir() #[cfg(not(any( target_os = "redox", @@ -1895,6 +2035,8 @@ mod remove_dir_impl { target_os = "horizon", target_os = "vita", target_os = "nto", + target_os = "linux", + target_os = "android", miri )))] mod remove_dir_impl { diff --git a/library/std/src/sys/unix/fs/dir_fd.rs b/library/std/src/sys/unix/fs/dir_fd.rs new file mode 100644 index 0000000000000..e319dd126bd00 --- /dev/null +++ b/library/std/src/sys/unix/fs/dir_fd.rs @@ -0,0 +1,464 @@ +use super::super::path::is_sep_byte; +use super::{File, OpenOptions, ReadDir}; +use crate::ffi::{CStr, CString}; +use crate::io; +use crate::os::unix::ffi::OsStrExt; +use crate::os::unix::io::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd}; +use crate::path::{Path, PathBuf}; +use crate::sys::common::small_c_string::run_path_with_cstr; +use crate::sys::fd::FileDesc; +use crate::sys::{cvt, cvt_r}; +use libc::{c_int, openat64, O_PATH}; + +impl ReadDir { + pub fn from_dirfd(dirfd: OwnedFd, root: PathBuf) -> io::Result { + let dir_pointer = super::cvt_p(unsafe { libc::fdopendir(dirfd.as_raw_fd()) })?; + // fdopendir takes ownership on success + crate::mem::forget(dirfd); + Ok(Self::from_dirp(dir_pointer, root)) + } +} + +impl File { + pub fn open_c( + dirfd: Option>, + path: &CStr, + opts: &OpenOptions, + ) -> io::Result { + let flags = libc::O_CLOEXEC + | opts.get_access_mode()? + | opts.get_creation_mode()? + | (opts.custom_flags as c_int & !libc::O_ACCMODE); + + let dirfd = match dirfd { + None => libc::AT_FDCWD, + Some(dirfd) => dirfd.as_raw_fd(), + }; + let fd = cvt_r(|| unsafe { + openat64( + dirfd, + path.as_ptr(), + flags, + // see previous comment why this cast is necessary + opts.mode as c_int, + ) + })?; + + Ok(File(unsafe { FileDesc::from_raw_fd(fd) })) + } +} + +pub fn open_deep( + at_path: Option>, + path: &Path, + opts: &OpenOptions, +) -> io::Result { + const MAX_SLICE: usize = (libc::PATH_MAX - 1) as usize; + + enum AtPath<'a> { + None, + Borrowed(BorrowedFd<'a>), + File(File), + } + + impl<'a> AtPath<'a> { + fn as_fd(&'a self) -> Option> { + match self { + AtPath::Borrowed(borrowed) => Some(*borrowed), + AtPath::File(ref file) => Some(file.as_fd()), + AtPath::None => None, + } + } + } + + let mut raw_path = path.as_os_str().as_bytes(); + let mut at_path = match at_path { + Some(borrowed) => AtPath::Borrowed(borrowed), + None => AtPath::None, + }; + + let mut dir_flags = OpenOptions::new(); + dir_flags.read(true); + dir_flags.custom_flags(O_PATH); + + while raw_path.len() > MAX_SLICE { + let sep_idx = match raw_path.iter().take(MAX_SLICE).rposition(|&byte| is_sep_byte(byte)) { + Some(idx) => idx, + _ => return Err(io::Error::from_raw_os_error(libc::ENAMETOOLONG)), + }; + + let (left, right) = raw_path.split_at(sep_idx + 1); + raw_path = right; + + let to_open = CString::new(left)?; + let dirfd = at_path.as_fd(); + + at_path = AtPath::File(File::open_c(dirfd, &to_open, &dir_flags)?); + } + + let to_open = CString::new(raw_path)?; + let dirfd = at_path.as_fd(); + + File::open_c(dirfd, &to_open, opts) +} + +pub fn long_filename_fallback( + result: io::Result, + path: &Path, + mut fallback: impl FnMut(File, &CStr) -> io::Result, +) -> io::Result { + use crate::io::ErrorKind; + match result { + ok @ Ok(_) => ok, + Err(e) if e.kind() == ErrorKind::InvalidFilename => { + if let Some(parent) = path.parent() { + let mut options = OpenOptions::new(); + options.read(true); + options.custom_flags(libc::O_PATH); + let dirfd = open_deep(None, parent, &options)?; + let file_name = path.file_name().unwrap(); + return run_path_with_cstr(file_name, |file_name| fallback(dirfd, file_name)); + } + + Err(e) + } + Err(e) => Err(e), + } +} + +pub fn remove_dir_all(path: &Path) -> io::Result<()> { + let filetype = crate::fs::symlink_metadata(path)?.file_type(); + if filetype.is_symlink() { + crate::fs::remove_file(path) + } else { + rmdir::remove_dir_all_iter(path) + } +} + +fn unlinkat(dirfd: BorrowedFd<'_>, path: &CStr, rmdir: bool) -> io::Result<()> { + let flags = if rmdir { libc::AT_REMOVEDIR } else { 0 }; + cvt(unsafe { libc::unlinkat(dirfd.as_raw_fd(), path.as_ptr(), flags) })?; + Ok(()) +} + +mod rmdir { + use crate::ffi::CStr; + + use crate::collections::HashSet; + use crate::ffi::OsStr; + use crate::fs::Metadata; + use crate::io; + use crate::io::Result; + use crate::os::unix::ffi::OsStrExt; + use crate::os::unix::fs::MetadataExt; + use crate::os::unix::io::{AsFd, AsRawFd, BorrowedFd, OwnedFd, RawFd}; + use crate::path::{Path, PathBuf}; + use crate::sys::fs::{File, OpenOptions, ReadDir}; + use crate::sys_common::FromInner; + use libc::{O_DIRECTORY, O_NOFOLLOW, O_PATH}; + + use super::{open_deep, unlinkat}; + + #[derive(PartialEq, Eq, Hash, Copy, Clone)] + struct DirId(u64, u64); + + impl From for DirId { + fn from(meta: Metadata) -> Self { + DirId(meta.dev(), meta.ino()) + } + } + + struct DirStack { + /// dirid specified the device id + inode number + /// for consistency checking. + /// The option carries the readdir and the file descriptor + /// from which it has been constructed. + /// Dropping the ReadDir invalidates the fd. + /// The first entry is the root directory whose + /// contents we want to delete. + dirs: Vec<(DirId, Option<(RawFd, ReadDir)>)>, + /// Each name component in the pathbuf represents + /// one entry in the `dirs` vec. + names: PathBuf, + /// Used for loop detection + visited: HashSet, + /// temporary buffer to construct a CStr for syscalls. + child_name_buffer: Vec, + } + + fn dir_open_options() -> OpenOptions { + let mut opts = OpenOptions::new(); + opts.read(true); + opts.custom_flags(O_DIRECTORY | O_NOFOLLOW); + opts + } + + const MAX_FDS: usize = 20; + + impl DirStack { + fn new(root_fd: crate::fs::File) -> Result { + let mut stack = DirStack { + dirs: Vec::new(), + names: PathBuf::from("./"), + visited: HashSet::new(), + child_name_buffer: Vec::new(), + }; + + let meta = root_fd.metadata()?; + let root_id = meta.into(); + + let raw_fd = root_fd.as_raw_fd(); + let dirfd = OwnedFd::from(root_fd); + let reader = ReadDir::from_dirfd(dirfd, PathBuf::new())?; + + stack.visited.insert(root_id); + stack.dirs.push((root_id, Some((raw_fd, reader)))); + + stack.check_invariants(true); + + Ok(stack) + } + + fn ensure_open(&mut self) -> Result<()> { + self.check_invariants(false); + + // Don't refill ancestors until the last one has been popped off. + // Doing it in batches reduces traversal/checking costs. + if self.dirs.last().unwrap().1.is_none() { + let start = self.dirs.len().saturating_sub(MAX_FDS).max(1); + let end = self.dirs.len(); + for idx in start..end { + if self.dirs[idx].1.is_none() { + let nearest_ancestor_idx = self.dirs[..idx] + .iter() + .rposition(|(_, fd)| fd.is_some()) + .expect("some open ancestor"); + let (ancestor_id, ancestor_fd) = &self.dirs[nearest_ancestor_idx]; + let ancestor_id = *ancestor_id; + let mut path_components = self.names.components(); + path_components + .advance_by(nearest_ancestor_idx + 1) + .expect("advanced too far"); + path_components + .advance_back_by(self.dirs.len() - idx - 1) + .expect("advanced_back too far"); + let sub_path = path_components.as_path(); + let depth = idx - nearest_ancestor_idx; + + debug_assert_eq!(sub_path.components().count(), depth); + + let ancestor_fd = + unsafe { BorrowedFd::borrow_raw(ancestor_fd.as_ref().unwrap().0) }; + + // use open_deep since the relative path can be arbitrarily long + let dir = crate::fs::File::from_inner(open_deep( + Some(ancestor_fd), + sub_path, + &dir_open_options(), + )?); + let meta = dir.metadata()?; + let dir_id = DirId(meta.ino(), meta.dev()); + + let (expected_id, entry) = &mut self.dirs[idx]; + + // Security check to prevent TOCTOU attacks when re-traversing the hierarchy. + // `open_deep` follows symlinks, so verify that we arrive at the same spot + // we have been at before. + // But this leaves the possibility of a symlink + inode recycling race... + if dir_id != *expected_id { + return Err(io::const_io_error!( + io::ErrorKind::Uncategorized, + "directory with unexpected dev/ino", + )); + } + // ... so we make extra sure that the directory is a descendant by going back up via `../..` + // to the nearest still-open ancestor. The ancestor being open prevents inode recycling. + // This could be avoided if open_deep used used openat2(..., O_BENEATH) + // but that's only available in linux kernels >= 5.6 + if depth > 1 { + let mut buf = PathBuf::new(); + for _ in 0..depth { + buf.push("..") + } + let mut opts = OpenOptions::new(); + opts.read(true); + // open as path handle since we only want to stat it + opts.custom_flags(O_DIRECTORY | O_PATH); + + let ancestor_o_path = crate::fs::File::from_inner(open_deep( + Some(dir.as_fd()), + &buf, + &opts, + )?); + let meta = ancestor_o_path.metadata()?; + + if DirId(meta.dev(), meta.ino()) != ancestor_id { + return Err(io::const_io_error!( + io::ErrorKind::Uncategorized, + "unexpected ancestor dir dev/ino", + )); + } + } + + let dirfd = OwnedFd::from(dir); + let rawfd = dirfd.as_raw_fd(); + + // supply an empty pathbuf since we don't intend to use DirEntry::path() + *entry = Some((rawfd, ReadDir::from_dirfd(dirfd, PathBuf::new())?)); + } + } + } + + self.check_invariants(true); + + Ok(()) + } + + fn sparsify(&mut self) { + self.check_invariants(true); + + // Keep the root fd and MAX_FDS tail entries open, close the rest. + // + // We could do something more clever here such as keeping a log(n) + // intermediate hops with expoenntial spacing to amortize reopening + // costs in very very deep directory trees. + for entry in self.dirs.iter_mut().skip(1).rev().skip(MAX_FDS) { + if entry.1.is_some() { + entry.1 = None; + } else { + break; + } + } + } + + fn check_invariants(&self, require_current_open: bool) { + debug_assert_eq!(self.names.components().count(), self.dirs.len()); + debug_assert!(self.dirs.len() > 0); + debug_assert!(self.dirs[0].1.is_some()); + if require_current_open { + debug_assert!(self.dirs.last().unwrap().1.is_some()); + } + } + + fn pop(&mut self) -> Result<()> { + self.check_invariants(true); + debug_assert!(self.dirs.len() > 1); + + let (id, fd) = self.dirs.pop().unwrap(); + drop(fd); + + let name = self.names.file_name().expect("path should not be empty"); + let mut buf = crate::mem::take(&mut self.child_name_buffer); + buf.clear(); + buf.extend_from_slice(name.as_bytes()); + buf.push(0); + + self.names.pop(); + self.visited.remove(&id); + + self.ensure_open()?; + + let parent = self.dirs.last().expect("at least the root should still be open"); + + let current_dir_fd = unsafe { BorrowedFd::borrow_raw(parent.1.as_ref().unwrap().0) }; + + let dir_name_c = unsafe { CStr::from_bytes_with_nul_unchecked(buf.as_slice()) }; + unlinkat(current_dir_fd, dir_name_c, true)?; + self.child_name_buffer = buf; + + Ok(()) + } + + fn push(&mut self, dir_name: &OsStr) -> Result<()> { + debug_assert!(dir_name.len() > 0); + self.check_invariants(true); + + let parent_fd = self.dirs.last().unwrap().1.as_ref().unwrap().0; + + let buf = &mut self.child_name_buffer; + buf.clear(); + buf.extend_from_slice(dir_name.as_bytes()); + buf.push(0); + let dir_name_c = unsafe { CStr::from_bytes_with_nul_unchecked(buf.as_slice()) }; + + let dir = crate::fs::File::from_inner(File::open_c( + Some(unsafe { BorrowedFd::borrow_raw(parent_fd) }), + dir_name_c, + &dir_open_options(), + )?); + let meta = dir.metadata()?; + let dir_id = DirId(meta.ino(), meta.dev()); + + if !self.visited.insert(dir_id) { + return Err(io::Error::from_raw_os_error(libc::ELOOP)); + } + + let dirfd = OwnedFd::from(dir); + let rawfd = dirfd.as_raw_fd(); + + let entry = (dir_id, Some((rawfd, ReadDir::from_dirfd(dirfd, PathBuf::new())?))); + + self.dirs.push(entry); + self.names.push(dir_name); + + self.sparsify(); + + Ok(()) + } + + fn walk_tree(&mut self) -> Result<()> { + loop { + self.check_invariants(true); + let (fd, current_dir) = self + .dirs + .last_mut() + .expect("at least the root should be open") + .1 + .as_mut() + .expect("the last entry shouldn't have its FD closed"); + + match current_dir.next() { + Some(child) => { + let child = child?; + let child_name = child.file_name(); + + if child.file_type()?.is_dir() { + self.push(&child_name)?; + continue; + } + + let buf = &mut self.child_name_buffer; + buf.clear(); + buf.extend_from_slice(child_name.as_os_str().as_bytes()); + buf.push(0); + let child_name_c = + unsafe { CStr::from_bytes_with_nul_unchecked(buf.as_slice()) }; + + let fd = unsafe { BorrowedFd::borrow_raw(*fd) }; + unlinkat(fd, child_name_c, false)?; + } + None if self.dirs.len() > 1 => { + self.pop()?; + } + None => break, + } + } + + Ok(()) + } + } + + pub(super) fn remove_dir_all_iter(path: &Path) -> Result<()> { + let root_fd = crate::fs::File::from_inner(File::open(&path, &dir_open_options())?); + + let mut stack = DirStack::new(root_fd)?; + stack.walk_tree()?; + + // There's no dirfd to reuse here since unlinking the starting point requires unlinking relative to + // the parent directory. '..' also cannot be used because that is vulnerable to the directory + // being moved. And we couldn't have started with the parent directory either because the starting + // point may be '/' or '.' which can't be removed themselves but we still should be able to clean + // their descendants. + crate::sys::fs::rmdir(path) + } +} diff --git a/library/std/src/sys/unix/mod.rs b/library/std/src/sys/unix/mod.rs index 77ef086f29b59..e0a19080730d9 100644 --- a/library/std/src/sys/unix/mod.rs +++ b/library/std/src/sys/unix/mod.rs @@ -420,7 +420,7 @@ cfg_if::cfg_if! { } } -#[cfg(any(target_os = "espidf", target_os = "horizon", target_os = "vita"))] +#[cfg(any(miri, not(any(target_os = "linux", target_os = "android"))))] mod unsupported { use crate::io; diff --git a/library/std/src/sys/unix/process/process_common.rs b/library/std/src/sys/unix/process/process_common.rs index 640648e870748..ab3ffd9d84c51 100644 --- a/library/std/src/sys/unix/process/process_common.rs +++ b/library/std/src/sys/unix/process/process_common.rs @@ -475,7 +475,7 @@ impl Stdio { opts.read(readable); opts.write(!readable); let path = unsafe { CStr::from_ptr(DEV_NULL.as_ptr() as *const _) }; - let fd = File::open_c(&path, &opts)?; + let fd = File::open_c(None, &path, &opts)?; Ok((ChildStdio::Owned(fd.into_inner()), None)) } diff --git a/src/tools/miri/tests/fail/shims/fs/isolated_file.stderr b/src/tools/miri/tests/fail/shims/fs/isolated_file.stderr index 2385439c8a5f7..f9b399eb88d1c 100644 --- a/src/tools/miri/tests/fail/shims/fs/isolated_file.stderr +++ b/src/tools/miri/tests/fail/shims/fs/isolated_file.stderr @@ -1,8 +1,8 @@ error: unsupported operation: `open` not available when isolation is enabled --> RUSTLIB/std/src/sys/PLATFORM/fs.rs:LL:CC | -LL | let fd = cvt_r(|| unsafe { open64(path.as_ptr(), flags, opts.mode as c_int) })?; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `open` not available when isolation is enabled +LL | cvt_r(|| unsafe { open64(path.as_ptr(), flags, opts.mode as c_int) })? + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `open` not available when isolation is enabled | = help: pass the flag `-Zmiri-disable-isolation` to disable isolation; = help: or pass `-Zmiri-isolation-error=warn` to configure Miri to return an error code from isolated operations (if supported for that operation) and continue with a warning @@ -12,7 +12,7 @@ LL | let fd = cvt_r(|| unsafe { open64(path.as_ptr(), flags, opts.mode a = note: inside `std::sys::PLATFORM::fs::File::open_c` at RUSTLIB/std/src/sys/PLATFORM/fs.rs:LL:CC = note: inside closure at RUSTLIB/std/src/sys/PLATFORM/fs.rs:LL:CC = note: inside `std::sys::PLATFORM::small_c_string::run_with_cstr::` at RUSTLIB/std/src/sys/PLATFORM/small_c_string.rs:LL:CC - = note: inside `std::sys::PLATFORM::small_c_string::run_path_with_cstr::` at RUSTLIB/std/src/sys/PLATFORM/small_c_string.rs:LL:CC + = note: inside `std::sys::PLATFORM::small_c_string::run_path_with_cstr::` at RUSTLIB/std/src/sys/PLATFORM/small_c_string.rs:LL:CC = note: inside `std::sys::PLATFORM::fs::File::open` at RUSTLIB/std/src/sys/PLATFORM/fs.rs:LL:CC = note: inside `std::fs::OpenOptions::_open` at RUSTLIB/std/src/fs.rs:LL:CC = note: inside `std::fs::OpenOptions::open::<&std::path::Path>` at RUSTLIB/std/src/fs.rs:LL:CC