Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 54e22eb

Browse files
hkratzpietroalbini
authored andcommittedJan 19, 2022
Fix CVE-2022-21658 for UNIX-like
1 parent 5ab67bf commit 54e22eb

File tree

3 files changed

+351
-13
lines changed

3 files changed

+351
-13
lines changed
 

‎library/std/src/fs/tests.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ use crate::fs::{self, File, OpenOptions};
44
use crate::io::{ErrorKind, SeekFrom};
55
use crate::path::Path;
66
use crate::str;
7+
use crate::sync::Arc;
78
use crate::sys_common::io::test::{tmpdir, TempDir};
89
use crate::thread;
10+
use crate::time::{Duration, Instant};
911

1012
use rand::{rngs::StdRng, RngCore, SeedableRng};
1113

@@ -601,6 +603,21 @@ fn recursive_rmdir_of_symlink() {
601603
assert!(canary.exists());
602604
}
603605

606+
#[test]
607+
fn recursive_rmdir_of_file_fails() {
608+
// test we do not delete a directly specified file.
609+
let tmpdir = tmpdir();
610+
let canary = tmpdir.join("do_not_delete");
611+
check!(check!(File::create(&canary)).write(b"foo"));
612+
let result = fs::remove_dir_all(&canary);
613+
#[cfg(unix)]
614+
error!(result, "Not a directory");
615+
#[cfg(windows)]
616+
error!(result, 267); // ERROR_DIRECTORY - The directory name is invalid.
617+
assert!(result.is_err());
618+
assert!(canary.exists());
619+
}
620+
604621
#[test]
605622
// only Windows makes a distinction between file and directory symlinks.
606623
#[cfg(windows)]
@@ -620,6 +637,59 @@ fn recursive_rmdir_of_file_symlink() {
620637
}
621638
}
622639

640+
#[test]
641+
#[ignore] // takes too much time
642+
fn recursive_rmdir_toctou() {
643+
// Test for time-of-check to time-of-use issues.
644+
//
645+
// Scenario:
646+
// The attacker wants to get directory contents deleted, to which he does not have access.
647+
// He has a way to get a privileged Rust binary call `std::fs::remove_dir_all()` on a
648+
// directory he controls, e.g. in his home directory.
649+
//
650+
// The POC sets up the `attack_dest/attack_file` which the attacker wants to have deleted.
651+
// The attacker repeatedly creates a directory and replaces it with a symlink from
652+
// `victim_del` to `attack_dest` while the victim code calls `std::fs::remove_dir_all()`
653+
// on `victim_del`. After a few seconds the attack has succeeded and
654+
// `attack_dest/attack_file` is deleted.
655+
let tmpdir = tmpdir();
656+
let victim_del_path = tmpdir.join("victim_del");
657+
let victim_del_path_clone = victim_del_path.clone();
658+
659+
// setup dest
660+
let attack_dest_dir = tmpdir.join("attack_dest");
661+
let attack_dest_dir = attack_dest_dir.as_path();
662+
fs::create_dir(attack_dest_dir).unwrap();
663+
let attack_dest_file = tmpdir.join("attack_dest/attack_file");
664+
File::create(&attack_dest_file).unwrap();
665+
666+
let drop_canary_arc = Arc::new(());
667+
let drop_canary_weak = Arc::downgrade(&drop_canary_arc);
668+
669+
eprintln!("x: {:?}", &victim_del_path);
670+
671+
// victim just continuously removes `victim_del`
672+
thread::spawn(move || {
673+
while drop_canary_weak.upgrade().is_some() {
674+
let _ = fs::remove_dir_all(&victim_del_path_clone);
675+
}
676+
});
677+
678+
// attacker (could of course be in a separate process)
679+
let start_time = Instant::now();
680+
while Instant::now().duration_since(start_time) < Duration::from_secs(1000) {
681+
if !attack_dest_file.exists() {
682+
panic!(
683+
"Victim deleted symlinked file outside of victim_del. Attack succeeded in {:?}.",
684+
Instant::now().duration_since(start_time)
685+
);
686+
}
687+
let _ = fs::create_dir(&victim_del_path);
688+
let _ = fs::remove_dir(&victim_del_path);
689+
let _ = symlink_dir(attack_dest_dir, &victim_del_path);
690+
}
691+
}
692+
623693
#[test]
624694
fn unicode_path_is_dir() {
625695
assert!(Path::new(".").is_dir());

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

Lines changed: 277 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ use libc::{
6464
dirent64, fstat64, ftruncate64, lseek64, lstat64, off64_t, open64, readdir64_r, stat64,
6565
};
6666

67-
pub use crate::sys_common::fs::{remove_dir_all, try_exists};
67+
pub use crate::sys_common::fs::try_exists;
6868

6969
pub struct File(FileDesc);
7070

@@ -228,7 +228,7 @@ pub struct DirEntry {
228228
target_os = "fuchsia",
229229
target_os = "redox"
230230
))]
231-
name: Box<[u8]>,
231+
name: CString,
232232
}
233233

234234
#[derive(Clone, Debug)]
@@ -455,8 +455,6 @@ impl Iterator for ReadDir {
455455
target_os = "illumos"
456456
))]
457457
fn next(&mut self) -> Option<io::Result<DirEntry>> {
458-
use crate::slice;
459-
460458
unsafe {
461459
loop {
462460
// Although readdir_r(3) would be a correct function to use here because
@@ -474,14 +472,10 @@ impl Iterator for ReadDir {
474472
};
475473
}
476474

477-
let name = (*entry_ptr).d_name.as_ptr();
478-
let namelen = libc::strlen(name) as usize;
479-
480475
let ret = DirEntry {
481476
entry: *entry_ptr,
482-
name: slice::from_raw_parts(name as *const u8, namelen as usize)
483-
.to_owned()
484-
.into_boxed_slice(),
477+
// d_name is guaranteed to be null-terminated.
478+
name: CStr::from_ptr((*entry_ptr).d_name.as_ptr()).to_owned(),
485479
dir: Arc::clone(&self.inner),
486480
};
487481
if ret.name_bytes() != b"." && ret.name_bytes() != b".." {
@@ -664,7 +658,26 @@ impl DirEntry {
664658
target_os = "redox"
665659
))]
666660
fn name_bytes(&self) -> &[u8] {
667-
&*self.name
661+
self.name.as_bytes()
662+
}
663+
664+
#[cfg(not(any(
665+
target_os = "solaris",
666+
target_os = "illumos",
667+
target_os = "fuchsia",
668+
target_os = "redox"
669+
)))]
670+
fn name_cstr(&self) -> &CStr {
671+
unsafe { CStr::from_ptr(self.entry.d_name.as_ptr()) }
672+
}
673+
#[cfg(any(
674+
target_os = "solaris",
675+
target_os = "illumos",
676+
target_os = "fuchsia",
677+
target_os = "redox"
678+
))]
679+
fn name_cstr(&self) -> &CStr {
680+
&self.name
668681
}
669682

670683
pub fn file_name_os_str(&self) -> &OsStr {
@@ -1437,3 +1450,256 @@ pub fn chroot(dir: &Path) -> io::Result<()> {
14371450
cvt(unsafe { libc::chroot(dir.as_ptr()) })?;
14381451
Ok(())
14391452
}
1453+
1454+
pub use remove_dir_impl::remove_dir_all;
1455+
1456+
// Fallback for REDOX
1457+
#[cfg(target_os = "redox")]
1458+
mod remove_dir_impl {
1459+
pub use crate::sys_common::fs::remove_dir_all;
1460+
}
1461+
1462+
// Dynamically choose implementation Macos x86-64: modern for 10.10+, fallback for older versions
1463+
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
1464+
mod remove_dir_impl {
1465+
use super::{cstr, lstat, Dir, InnerReadDir, ReadDir};
1466+
use crate::ffi::CStr;
1467+
use crate::io;
1468+
use crate::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd};
1469+
use crate::os::unix::prelude::{OwnedFd, RawFd};
1470+
use crate::path::{Path, PathBuf};
1471+
use crate::sync::Arc;
1472+
use crate::sys::weak::weak;
1473+
use crate::sys::{cvt, cvt_r};
1474+
use libc::{c_char, c_int, DIR};
1475+
1476+
pub fn openat_nofollow_dironly(parent_fd: Option<RawFd>, p: &CStr) -> io::Result<OwnedFd> {
1477+
weak!(fn openat(c_int, *const c_char, c_int) -> c_int);
1478+
let fd = cvt_r(|| unsafe {
1479+
openat.get().unwrap()(
1480+
parent_fd.unwrap_or(libc::AT_FDCWD),
1481+
p.as_ptr(),
1482+
libc::O_CLOEXEC | libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_DIRECTORY,
1483+
)
1484+
})?;
1485+
Ok(unsafe { OwnedFd::from_raw_fd(fd) })
1486+
}
1487+
1488+
fn fdreaddir(dir_fd: OwnedFd) -> io::Result<(ReadDir, RawFd)> {
1489+
weak!(fn fdopendir(c_int) -> *mut DIR, "fdopendir$INODE64");
1490+
let ptr = unsafe { fdopendir.get().unwrap()(dir_fd.as_raw_fd()) };
1491+
if ptr.is_null() {
1492+
return Err(io::Error::last_os_error());
1493+
}
1494+
let dirp = Dir(ptr);
1495+
// file descriptor is automatically closed by libc::closedir() now, so give up ownership
1496+
let new_parent_fd = dir_fd.into_raw_fd();
1497+
// a valid root is not needed because we do not call any functions involving the full path
1498+
// of the DirEntrys.
1499+
let dummy_root = PathBuf::new();
1500+
Ok((
1501+
ReadDir {
1502+
inner: Arc::new(InnerReadDir { dirp, root: dummy_root }),
1503+
end_of_stream: false,
1504+
},
1505+
new_parent_fd,
1506+
))
1507+
}
1508+
1509+
fn remove_dir_all_recursive(parent_fd: Option<RawFd>, p: &Path) -> io::Result<()> {
1510+
weak!(fn unlinkat(c_int, *const c_char, c_int) -> c_int);
1511+
1512+
let pcstr = cstr(p)?;
1513+
1514+
// entry is expected to be a directory, open as such
1515+
let fd = openat_nofollow_dironly(parent_fd, &pcstr)?;
1516+
1517+
// open the directory passing ownership of the fd
1518+
let (dir, fd) = fdreaddir(fd)?;
1519+
for child in dir {
1520+
let child = child?;
1521+
match child.entry.d_type {
1522+
libc::DT_DIR => {
1523+
remove_dir_all_recursive(Some(fd), Path::new(&child.file_name()))?;
1524+
}
1525+
libc::DT_UNKNOWN => {
1526+
match cvt(unsafe { unlinkat.get().unwrap()(fd, child.name_cstr().as_ptr(), 0) })
1527+
{
1528+
// type unknown - try to unlink
1529+
Err(err) if err.raw_os_error() == Some(libc::EPERM) => {
1530+
// if the file is a directory unlink fails with EPERM
1531+
remove_dir_all_recursive(Some(fd), Path::new(&child.file_name()))?;
1532+
}
1533+
result => {
1534+
result?;
1535+
}
1536+
}
1537+
}
1538+
_ => {
1539+
// not a directory -> unlink
1540+
cvt(unsafe { unlinkat.get().unwrap()(fd, child.name_cstr().as_ptr(), 0) })?;
1541+
}
1542+
}
1543+
}
1544+
1545+
// unlink the directory after removing its contents
1546+
cvt(unsafe {
1547+
unlinkat.get().unwrap()(
1548+
parent_fd.unwrap_or(libc::AT_FDCWD),
1549+
pcstr.as_ptr(),
1550+
libc::AT_REMOVEDIR,
1551+
)
1552+
})?;
1553+
Ok(())
1554+
}
1555+
1556+
fn remove_dir_all_modern(p: &Path) -> io::Result<()> {
1557+
// We cannot just call remove_dir_all_recursive() here because that would not delete a passed
1558+
// symlink. No need to worry about races, because remove_dir_all_recursive() does not recurse
1559+
// into symlinks.
1560+
let attr = lstat(p)?;
1561+
if attr.file_type().is_symlink() {
1562+
crate::fs::remove_file(p)
1563+
} else {
1564+
remove_dir_all_recursive(None, p)
1565+
}
1566+
}
1567+
1568+
pub fn remove_dir_all(p: &Path) -> io::Result<()> {
1569+
weak!(fn openat(c_int, *const c_char, c_int) -> c_int);
1570+
if openat.get().is_some() {
1571+
// openat() is available with macOS 10.10+, just like unlinkat() and fdopendir()
1572+
remove_dir_all_modern(p)
1573+
} else {
1574+
// fall back to classic implementation
1575+
crate::sys_common::fs::remove_dir_all(p)
1576+
}
1577+
}
1578+
}
1579+
1580+
// Modern implementation using openat(), unlinkat() and fdopendir()
1581+
#[cfg(not(any(all(target_os = "macos", target_arch = "x86_64"), target_os = "redox")))]
1582+
mod remove_dir_impl {
1583+
use super::{cstr, lstat, Dir, DirEntry, InnerReadDir, ReadDir};
1584+
use crate::ffi::CStr;
1585+
use crate::io;
1586+
use crate::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd};
1587+
use crate::os::unix::prelude::{OwnedFd, RawFd};
1588+
use crate::path::{Path, PathBuf};
1589+
use crate::sync::Arc;
1590+
use crate::sys::{cvt, cvt_r};
1591+
use libc::{fdopendir, openat, unlinkat};
1592+
1593+
pub fn openat_nofollow_dironly(parent_fd: Option<RawFd>, p: &CStr) -> io::Result<OwnedFd> {
1594+
let fd = cvt_r(|| unsafe {
1595+
openat(
1596+
parent_fd.unwrap_or(libc::AT_FDCWD),
1597+
p.as_ptr(),
1598+
libc::O_CLOEXEC | libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_DIRECTORY,
1599+
)
1600+
})?;
1601+
Ok(unsafe { OwnedFd::from_raw_fd(fd) })
1602+
}
1603+
1604+
fn fdreaddir(dir_fd: OwnedFd) -> io::Result<(ReadDir, RawFd)> {
1605+
let ptr = unsafe { fdopendir(dir_fd.as_raw_fd()) };
1606+
if ptr.is_null() {
1607+
return Err(io::Error::last_os_error());
1608+
}
1609+
let dirp = Dir(ptr);
1610+
// file descriptor is automatically closed by libc::closedir() now, so give up ownership
1611+
let new_parent_fd = dir_fd.into_raw_fd();
1612+
// a valid root is not needed because we do not call any functions involving the full path
1613+
// of the DirEntrys.
1614+
let dummy_root = PathBuf::new();
1615+
Ok((
1616+
ReadDir {
1617+
inner: Arc::new(InnerReadDir { dirp, root: dummy_root }),
1618+
#[cfg(not(any(
1619+
target_os = "solaris",
1620+
target_os = "illumos",
1621+
target_os = "fuchsia",
1622+
target_os = "redox",
1623+
)))]
1624+
end_of_stream: false,
1625+
},
1626+
new_parent_fd,
1627+
))
1628+
}
1629+
1630+
#[cfg(any(
1631+
target_os = "solaris",
1632+
target_os = "illumos",
1633+
target_os = "haiku",
1634+
target_os = "vxworks"
1635+
))]
1636+
fn is_dir(_ent: &DirEntry) -> Option<bool> {
1637+
None
1638+
}
1639+
1640+
#[cfg(not(any(
1641+
target_os = "solaris",
1642+
target_os = "illumos",
1643+
target_os = "haiku",
1644+
target_os = "vxworks"
1645+
)))]
1646+
fn is_dir(ent: &DirEntry) -> Option<bool> {
1647+
match ent.entry.d_type {
1648+
libc::DT_UNKNOWN => None,
1649+
libc::DT_DIR => Some(true),
1650+
_ => Some(false),
1651+
}
1652+
}
1653+
1654+
fn remove_dir_all_recursive(parent_fd: Option<RawFd>, p: &Path) -> io::Result<()> {
1655+
let pcstr = cstr(p)?;
1656+
1657+
// entry is expected to be a directory, open as such
1658+
let fd = openat_nofollow_dironly(parent_fd, &pcstr)?;
1659+
1660+
// open the directory passing ownership of the fd
1661+
let (dir, fd) = fdreaddir(fd)?;
1662+
for child in dir {
1663+
let child = child?;
1664+
match is_dir(&child) {
1665+
Some(true) => {
1666+
remove_dir_all_recursive(Some(fd), Path::new(&child.file_name()))?;
1667+
}
1668+
Some(false) => {
1669+
cvt(unsafe { unlinkat(fd, child.name_cstr().as_ptr(), 0) })?;
1670+
}
1671+
None => match cvt(unsafe { unlinkat(fd, child.name_cstr().as_ptr(), 0) }) {
1672+
// type unknown - try to unlink
1673+
Err(err)
1674+
if err.raw_os_error() == Some(libc::EISDIR)
1675+
|| err.raw_os_error() == Some(libc::EPERM) =>
1676+
{
1677+
// if the file is a directory unlink fails with EISDIR on Linux and EPERM everyhwere else
1678+
remove_dir_all_recursive(Some(fd), Path::new(&child.file_name()))?;
1679+
}
1680+
result => {
1681+
result?;
1682+
}
1683+
},
1684+
}
1685+
}
1686+
1687+
// unlink the directory after removing its contents
1688+
cvt(unsafe {
1689+
unlinkat(parent_fd.unwrap_or(libc::AT_FDCWD), pcstr.as_ptr(), libc::AT_REMOVEDIR)
1690+
})?;
1691+
Ok(())
1692+
}
1693+
1694+
pub fn remove_dir_all(p: &Path) -> io::Result<()> {
1695+
// We cannot just call remove_dir_all_recursive() here because that would not delete a passed
1696+
// symlink. No need to worry about races, because remove_dir_all_recursive() does not recurse
1697+
// into symlinks.
1698+
let attr = lstat(p)?;
1699+
if attr.file_type().is_symlink() {
1700+
crate::fs::remove_file(p)
1701+
} else {
1702+
remove_dir_all_recursive(None, p)
1703+
}
1704+
}
1705+
}

‎library/std/src/sys/unix/weak.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,14 @@ impl<F> ExternWeak<F> {
7373

7474
pub(crate) macro dlsym {
7575
(fn $name:ident($($t:ty),*) -> $ret:ty) => (
76+
dlsym!(fn $name($($t),*) -> $ret, stringify!($name));
77+
),
78+
(fn $name:ident($($t:ty),*) -> $ret:ty, $sym:expr) => (
7679
static DLSYM: DlsymWeak<unsafe extern "C" fn($($t),*) -> $ret> =
77-
DlsymWeak::new(concat!(stringify!($name), '\0'));
80+
DlsymWeak::new(concat!($sym, '\0'));
7881
let $name = &DLSYM;
7982
)
8083
}
81-
8284
pub(crate) struct DlsymWeak<F> {
8385
name: &'static str,
8486
addr: AtomicUsize,

0 commit comments

Comments
 (0)
Please sign in to comment.