From a03c7b755b54e09a3e274974a63c8d0ac32254d7 Mon Sep 17 00:00:00 2001 From: Hans Kratz Date: Mon, 17 Jan 2022 09:45:46 +0100 Subject: [PATCH 2/4] Fix CVE-2022-21658 for UNIX-like ~~ backport to 1.52: - removed macos/redox impl - removed ignored test case - removed no longer needed changes to weak! (which is a v1 macro in 1.52) - used RawFd instead of OwnedFd (latter doesn't exist in 1.52) Signed-off-by: Fabian Grünbichler --- library/std/src/fs/tests.rs | 70 ++++++++ library/std/src/sys/unix/fs.rs | 279 +++++++++++++++++++++++++++++-- 3 files changed, 342 insertions(+), 12 deletions(-) diff --git a/library/std/src/fs/tests.rs b/library/std/src/fs/tests.rs index 9a8f1e44f1f..4d1959258be 100644 --- a/library/std/src/fs/tests.rs +++ b/library/std/src/fs/tests.rs @@ -4,8 +4,10 @@ use crate::io::{ErrorKind, SeekFrom}; use crate::path::Path; use crate::str; +use crate::sync::Arc; use crate::sys_common::io::test::{tmpdir, TempDir}; use crate::thread; +use crate::time::{Duration, Instant}; use rand::{rngs::StdRng, RngCore, SeedableRng}; @@ -588,6 +590,21 @@ fn recursive_rmdir_of_symlink() { } #[test] +fn recursive_rmdir_of_file_fails() { + // test we do not delete a directly specified file. + let tmpdir = tmpdir(); + let canary = tmpdir.join("do_not_delete"); + check!(check!(File::create(&canary)).write(b"foo")); + let result = fs::remove_dir_all(&canary); + #[cfg(unix)] + error!(result, "Not a directory"); + #[cfg(windows)] + error!(result, 267); // ERROR_DIRECTORY - The directory name is invalid. + assert!(result.is_err()); + assert!(canary.exists()); +} + +#[test] // only Windows makes a distinction between file and directory symlinks. #[cfg(windows)] fn recursive_rmdir_of_file_symlink() { #[test] fn unicode_path_is_dir() { assert!(Path::new(".").is_dir()); diff --git a/library/std/src/sys/unix/fs.rs b/library/std/src/sys/unix/fs.rs index a4fff9b2e64..a150d1a9aac 100644 --- a/library/std/src/sys/unix/fs.rs +++ b/library/std/src/sys/unix/fs.rs @@ -48,8 +48,6 @@ use libc::{ dirent64, fstat64, ftruncate64, lseek64, lstat64, off64_t, open64, readdir64_r, stat64, }; -pub use crate::sys_common::fs::remove_dir_all; - pub struct File(FileDesc); // FIXME: This should be available on Linux with all `target_env`. @@ -212,7 +210,7 @@ pub struct DirEntry { target_os = "fuchsia", target_os = "redox" ))] - name: Box<[u8]>, + name: CString, } #[derive(Clone, Debug)] @@ -439,8 +437,6 @@ impl Iterator for ReadDir { target_os = "illumos" ))] fn next(&mut self) -> Option> { - use crate::slice; - unsafe { loop { // Although readdir_r(3) would be a correct function to use here because @@ -458,14 +454,10 @@ impl Iterator for ReadDir { }; } - let name = (*entry_ptr).d_name.as_ptr(); - let namelen = libc::strlen(name) as usize; - let ret = DirEntry { entry: *entry_ptr, - name: slice::from_raw_parts(name as *const u8, namelen as usize) - .to_owned() - .into_boxed_slice(), + // d_name is guaranteed to be null-terminated. + name: CStr::from_ptr((*entry_ptr).d_name.as_ptr()).to_owned(), dir: Arc::clone(&self.inner), }; if ret.name_bytes() != b"." && ret.name_bytes() != b".." { @@ -645,7 +637,26 @@ impl DirEntry { target_os = "redox" ))] fn name_bytes(&self) -> &[u8] { - &*self.name + self.name.as_bytes() + } + + #[cfg(not(any( + target_os = "solaris", + target_os = "illumos", + target_os = "fuchsia", + target_os = "redox" + )))] + fn name_cstr(&self) -> &CStr { + unsafe { CStr::from_ptr(self.entry.d_name.as_ptr()) } + } + #[cfg(any( + target_os = "solaris", + target_os = "illumos", + target_os = "fuchsia", + target_os = "redox" + ))] + fn name_cstr(&self) -> &CStr { + &self.name } } @@ -1330,3 +1341,123 @@ pub fn copy(from: &Path, to: &Path) -> i })?; Ok(bytes_copied as u64) } + +pub use remove_dir_impl::remove_dir_all; + +// Modern implementation using openat(), unlinkat() and fdopendir() +#[cfg(not(any(all(target_os = "macos", target_arch = "x86_64"), target_os = "redox")))] +mod remove_dir_impl { + use super::{cstr, lstat, Dir, InnerReadDir, ReadDir}; + use crate::ffi::CStr; + use crate::io; + use crate::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd}; + use crate::os::unix::prelude::RawFd; + use crate::path::{Path, PathBuf}; + use crate::sync::Arc; + use crate::sys::{cvt, cvt_r}; + use libc::{fdopendir, openat, unlinkat}; + + pub fn openat_nofollow_dironly(parent_fd: Option, p: &CStr) -> io::Result { + let fd = cvt_r(|| unsafe { + openat( + parent_fd.unwrap_or(libc::AT_FDCWD), + p.as_ptr(), + libc::O_CLOEXEC | libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_DIRECTORY, + ) + })?; + Ok(fd) + } + + fn fdreaddir(dir_fd: RawFd) -> io::Result<(ReadDir, RawFd)> { + let ptr = unsafe { fdopendir(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 }), + #[cfg(not(any( + target_os = "solaris", + target_os = "illumos", + target_os = "fuchsia", + target_os = "redox", + )))] + end_of_stream: false, + }, + new_parent_fd, + )) + } + + fn remove_dir_all_recursive(parent_fd: Option, p: &Path) -> io::Result<()> { + let pcstr = cstr(p)?; + + // entry is expected to be a directory, open as such + let fd = openat_nofollow_dironly(parent_fd, &pcstr)?; + + // open the directory passing ownership of the fd + let (dir, fd) = fdreaddir(fd)?; + for child in dir { + let child = child?; + let child_is_dir = if cfg!(any( + target_os = "solaris", + target_os = "illumos", + target_os = "haiku", + target_os = "vxworks" + )) { + // no d_type in dirent + None + } else { + match child.entry.d_type { + libc::DT_UNKNOWN => None, + libc::DT_DIR => Some(true), + _ => Some(false), + } + }; + match child_is_dir { + Some(true) => { + remove_dir_all_recursive(Some(fd), Path::new(&child.file_name()))?; + } + Some(false) => { + cvt(unsafe { unlinkat(fd, child.name_cstr().as_ptr(), 0) })?; + } + 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?; + } + }, + } + } + + // unlink the directory after removing its contents + cvt(unsafe { + unlinkat(parent_fd.unwrap_or(libc::AT_FDCWD), pcstr.as_ptr(), libc::AT_REMOVEDIR) + })?; + Ok(()) + } + + pub fn remove_dir_all(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) + } + } +} -- 2.32.0