Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
openSUSE:Step:FrontRunner
rust.22504
0001-Fix-CVE-2022-21658-for-UNIX-like.patch
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File 0001-Fix-CVE-2022-21658-for-UNIX-like.patch of Package rust.22504
From ba50aa8f1234aee294f26d0c1d54927fff609f5c Mon Sep 17 00:00:00 2001 From: Hans Kratz <hans@appfour.com> Date: Mon, 17 Jan 2022 09:45:46 +0100 Subject: [PATCH] Fix CVE-2022-21658 for UNIX-like --- library/std/src/fs.rs | 9 +- library/std/src/fs/tests.rs | 70 ++++++++ library/std/src/sys/unix/fs.rs | 277 +++++++++++++++++++++++++++++-- library/std/src/sys/unix/weak.rs | 6 +- 4 files changed, 346 insertions(+), 16 deletions(-) diff --git a/library/std/src/fs.rs b/library/std/src/fs.rs index e6120b8ee31..2bd12120844 100644 --- a/library/std/src/fs.rs +++ b/library/std/src/fs.rs @@ -1974,11 +1974,14 @@ pub fn remove_dir<P: AsRef<Path>>(path: P) -> io::Result<()> { /// /// # Platform-specific behavior /// -/// This function currently corresponds to `opendir`, `lstat`, `rm` and `rmdir` functions on Unix -/// and the `FindFirstFile`, `GetFileAttributesEx`, `DeleteFile`, and `RemoveDirectory` functions -/// on Windows. +/// This function currently uses the `openat`, `fdopendir`, `unlinkat` and `lstat` functions on +/// Unix (execept for MacOS before version 10.10 and REDOX) and the `FindFirstFile`, +/// `GetFileAttributesEx`, `DeleteFile`, and `RemoveDirectory` functions on Windows. /// Note that, this [may change in the future][changes]. /// +/// On MacOS before version 10.10 and on REDOX this function is not protected against time-of-check +/// to time-of-Use (TOCTOU) race conditions and should not be used in security-sensitive code. +/// /// [changes]: io#platform-specific-behavior /// /// # Errors diff --git a/library/std/src/fs/tests.rs b/library/std/src/fs/tests.rs index ce8d3a56f7a..554b11222c0 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}; @@ -587,6 +589,21 @@ fn recursive_rmdir_of_symlink() { assert!(canary.exists()); } +#[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)] @@ -606,6 +623,59 @@ fn recursive_rmdir_of_file_symlink() { } } +#[test] +#[ignore] // takes too much time +fn recursive_rmdir_toctou() { + // Test for time-of-check to time-of-use issues. + // + // Scenario: + // The attacker wants to get directory contents deleted, to which he does not have access. + // He has a way to get a privileged Rust binary call `std::fs::remove_dir_all()` on a + // directory he controls, e.g. in his home directory. + // + // The POC sets up the `attack_dest/attack_file` which the attacker wants to have deleted. + // The attacker repeatedly creates a directory and replaces it with a symlink from + // `victim_del` to `attack_dest` while the victim code calls `std::fs::remove_dir_all()` + // on `victim_del`. After a few seconds the attack has succeeded and + // `attack_dest/attack_file` is deleted. + let tmpdir = tmpdir(); + let victim_del_path = tmpdir.join("victim_del"); + let victim_del_path_clone = victim_del_path.clone(); + + // setup dest + let attack_dest_dir = tmpdir.join("attack_dest"); + let attack_dest_dir = attack_dest_dir.as_path(); + fs::create_dir(attack_dest_dir).unwrap(); + let attack_dest_file = tmpdir.join("attack_dest/attack_file"); + File::create(&attack_dest_file).unwrap(); + + let drop_canary_arc = Arc::new(()); + let drop_canary_weak = Arc::downgrade(&drop_canary_arc); + + eprintln!("x: {:?}", &victim_del_path); + + // victim just continuously removes `victim_del` + thread::spawn(move || { + while drop_canary_weak.upgrade().is_some() { + let _ = fs::remove_dir_all(&victim_del_path_clone); + } + }); + + // attacker (could of course be in a separate process) + let start_time = Instant::now(); + while Instant::now().duration_since(start_time) < Duration::from_secs(1000) { + if !attack_dest_file.exists() { + panic!( + "Victim deleted symlinked file outside of victim_del. Attack succeeded in {:?}.", + Instant::now().duration_since(start_time) + ); + } + let _ = fs::create_dir(&victim_del_path); + let _ = fs::remove_dir(&victim_del_path); + let _ = symlink_dir(attack_dest_dir, &victim_del_path); + } +} + #[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 45bae25a0c3..6e7b02db447 100644 --- a/library/std/src/sys/unix/fs.rs +++ b/library/std/src/sys/unix/fs.rs @@ -48,8 +48,6 @@ 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<io::Result<DirEntry>> { - use crate::slice; - unsafe { loop { // Although readdir_r(3) would be a correct function to use here because @@ -458,14 +454,10 @@ fn next(&mut self) -> Option<io::Result<DirEntry>> { }; } - 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 @@ fn name_bytes(&self) -> &[u8] { 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 } } @@ -1335,3 +1346,245 @@ pub fn chroot(dir: &Path) -> io::Result<()> { cvt(unsafe { libc::chroot(dir.as_ptr()) })?; Ok(()) } + +pub use remove_dir_impl::remove_dir_all; + +// Fallback for REDOX +#[cfg(target_os = "redox")] +mod remove_dir_impl { + pub use crate::sys_common::fs::remove_dir_all; +} + +// Dynamically choose implementation Macos x86-64: modern for 10.10+, fallback for older versions +#[cfg(all(target_os = "macos", target_arch = "x86_64"))] +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::weak::weak; + use crate::sys::{cvt, cvt_r}; + use libc::{c_char, c_int, DIR}; + + pub fn openat_nofollow_dironly(parent_fd: Option<RawFd>, p: &CStr) -> io::Result<RawFd> { + weak!(fn openat(c_int, *const c_char, c_int) -> c_int); + 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, + ) + }) + } + + fn fdreaddir(dir_fd: RawFd) -> io::Result<(ReadDir, RawFd)> { + weak!(fn fdopendir(c_int) -> *mut DIR, "fdopendir$INODE64"); + let ptr = unsafe { fdopendir.get().unwrap()(dir_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; + // 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); + + 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?; + 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) })?; + } + } + } + + // 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(()) + } + + 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) + } + } + + 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) + } + } +} + +// 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}; + + fn openat_nofollow_dironly(parent_fd: Option<RawFd>, p: &CStr) -> io::Result<RawFd> { + 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, + ) + }) + } + + fn fdreaddir(dir_fd: RawFd) -> io::Result<(ReadDir, RawFd)> { + let ptr = unsafe { fdopendir(dir_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<RawFd>, 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) + } + } +} diff --git a/library/std/src/sys/unix/weak.rs b/library/std/src/sys/unix/weak.rs index 432fe4c33bc..bb952b181fc 100644 --- a/library/std/src/sys/unix/weak.rs +++ b/library/std/src/sys/unix/weak.rs @@ -28,8 +28,12 @@ macro_rules! weak { (fn $name:ident($($t:ty),*) -> $ret:ty) => ( + weak!(fn $name($($t),*) -> $ret, stringify!($name)); + ); + (fn $name:ident($($t:ty),*) -> $ret:ty, $sym:expr) => ( + #[allow(non_upper_case_globals)] static $name: crate::sys::weak::Weak<unsafe extern "C" fn($($t),*) -> $ret> = - crate::sys::weak::Weak::new(concat!(stringify!($name), '\0')); + crate::sys::weak::Weak::new(concat!($sym, '\0')); ) } -- 2.34.1
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor