CVE-2024-43806
Description
Rustix is a set of safe Rust bindings to POSIX-ish APIs. When using rustix::fs::Dir using the linux_raw backend, it's possible for the iterator to "get stuck" when an IO error is encountered. Combined with a memory over-allocation issue in rustix::fs::Dir::read_more, this can cause quick and unbounded memory explosion (gigabytes in a few seconds if used on a hot path) and eventually lead to an OOM crash of the application. The symptoms were initially discovered in https://github.com/imsnif/bandwhich/issues/284. That post has lots of details of our investigation. Full details can be read on the GHSA-c827-hfw6-qwvm repo advisory. If a program tries to access a directory with its file descriptor after the file has been unlinked (or any other action that leaves the Dir iterator in the stuck state), and the implementation does not break after seeing an error, it can cause a memory explosion. As an example, Linux's various virtual file systems (e.g. /proc, /sys) can contain directories that spontaneously pop in and out of existence. Attempting to iterate over them using rustix::fs::Dir directly or indirectly (e.g. with the procfs crate) can trigger this fault condition if the implementation decides to continue on errors. An attacker knowledgeable about the implementation details of a vulnerable target can therefore try to trigger this fault condition via any one or a combination of several available APIs. If successful, the application host will quickly run out of memory, after which the application will likely be terminated by an OOM killer, leading to denial of service. This issue has been addressed in release versions 0.35.15, 0.36.16, 0.37.25, and 0.38.19. Users are advised to upgrade. There are no known workarounds for this issue.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Rustix's Dir iterator can get stuck on IO errors, causing memory over-allocation and potential OOM denial of service.
Vulnerability
Overview
Rustix, a set of safe Rust bindings to POSIX-ish APIs, contains a vulnerability in rustix::fs::Dir when using the linux_raw backend. An iterator can become stuck when an IO error is encountered, and this condition can combine with a memory over-allocation issue in Dir::read_more to cause unbounded memory consumption. This can lead to gigabytes of memory being allocated in seconds, ultimately resulting in an out-of-memory (OOM) crash of the application [1].
Exploitation
Conditions
The root cause is that the iterator does not properly handle errors during directory traversal. If a program attempts to iterate over a directory after its file descriptor becomes invalid (e.g., after the directory is unlinked) or encounters spontaneous errors typical of virtual filesystems like /proc or /sys, the iterator may continue without breaking, leading to repeated memory allocation. An attacker who understands the implementation details of a vulnerable target can trigger this condition through various APIs, potentially without authentication if the application exposes directory iteration functionality [1].
Impact
Successful exploitation results in rapid memory exhaustion, causing the application to be terminated by the operating system's OOM killer. This constitutes a denial of service (DoS) attack. The vulnerability is especially concerning for applications that iterate over dynamic filesystem directories, as the attack can be carried out without special privileges in many scenarios [1].
Mitigation
The issue has been addressed in Rustix release versions 0.35.15, 0.36.16, 0.37.25, and 0.38.19. The fix introduces an any_errors flag to track whether errors have occurred during iteration, preventing the infinite loop and subsequent memory explosion [3][4]. Users are advised to upgrade to a patched version; no workarounds are available [1].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
rustixcrates.io | >= 0.35.11, < 0.35.15 | 0.35.15 |
rustixcrates.io | >= 0.36.0, < 0.36.16 | 0.36.16 |
rustixcrates.io | >= 0.37.0, < 0.37.25 | 0.37.25 |
rustixcrates.io | >= 0.38.0, < 0.38.19 | 0.38.19 |
Affected products
39- Range: v0.35.11, v0.35.12, v0.35.13, …
- osv-coords38 versionspkg:apk/chainguard/batpkg:apk/chainguard/bat-docpkg:apk/chainguard/bergpkg:apk/chainguard/pgcatpkg:apk/chainguard/zedpkg:apk/chainguard/zellijpkg:apk/chainguard/zellij-bash-completionpkg:apk/chainguard/zellij-fish-completionpkg:apk/chainguard/zellij-zsh-completionpkg:apk/wolfi/batpkg:apk/wolfi/bat-docpkg:apk/wolfi/bergpkg:apk/wolfi/pgcatpkg:apk/wolfi/zedpkg:apk/wolfi/zellijpkg:apk/wolfi/zellij-bash-completionpkg:apk/wolfi/zellij-fish-completionpkg:apk/wolfi/zellij-zsh-completionpkg:cargo/rustixpkg:rpm/opensuse/389-ds&distro=openSUSE%20Tumbleweedpkg:rpm/opensuse/librsvg&distro=openSUSE%20Leap%2016.0pkg:rpm/opensuse/rage-encryption&distro=openSUSE%20Leap%2015.5pkg:rpm/opensuse/rage-encryption&distro=openSUSE%20Leap%2015.6pkg:rpm/opensuse/rage-encryption&distro=openSUSE%20Tumbleweedpkg:rpm/opensuse/rust-keylime&distro=openSUSE%20Tumbleweedpkg:rpm/suse/librsvg&distro=SUSE%20Linux%20Enterprise%20Micro%205.3pkg:rpm/suse/librsvg&distro=SUSE%20Linux%20Enterprise%20Micro%205.4pkg:rpm/suse/librsvg&distro=SUSE%20Linux%20Enterprise%20Micro%205.5pkg:rpm/suse/librsvg&distro=SUSE%20Linux%20Enterprise%20Server%2016.0pkg:rpm/suse/librsvg&distro=SUSE%20Linux%20Enterprise%20Server%20for%20SAP%20applications%2016.0pkg:rpm/suse/librsvg&distro=SUSE%20Linux%20Micro%206.2pkg:rpm/suse/rage-encryption&distro=SUSE%20Linux%20Enterprise%20Module%20for%20Basesystem%2015%20SP5pkg:rpm/suse/rage-encryption&distro=SUSE%20Linux%20Enterprise%20Module%20for%20Basesystem%2015%20SP6pkg:rpm/suse/rust-keylime&distro=SUSE%20Linux%20Enterprise%20Micro%205.3pkg:rpm/suse/rust-keylime&distro=SUSE%20Linux%20Enterprise%20Micro%205.4pkg:rpm/suse/rust-keylime&distro=SUSE%20Linux%20Enterprise%20Micro%205.5pkg:rpm/suse/rust-keylime&distro=SUSE%20Linux%20Micro%206.0pkg:rpm/suse/rust-keylime&distro=SUSE%20Linux%20Micro%206.1
< 0.24.0-r2+ 37 more
- (no CPE)range: < 0.24.0-r2
- (no CPE)range: < 0.24.0-r2
- (no CPE)range: < 0.3.5-r2
- (no CPE)range: < 1.2.0-r1
- (no CPE)range: < 0.147.2-r0
- (no CPE)range: < 0.42.1-r0
- (no CPE)range: < 0.42.1-r0
- (no CPE)range: < 0.42.1-r0
- (no CPE)range: < 0.42.1-r0
- (no CPE)range: < 0.24.0-r2
- (no CPE)range: < 0.24.0-r2
- (no CPE)range: < 0.3.5-r2
- (no CPE)range: < 1.2.0-r1
- (no CPE)range: < 0.147.2-r0
- (no CPE)range: < 0.42.1-r0
- (no CPE)range: < 0.42.1-r0
- (no CPE)range: < 0.42.1-r0
- (no CPE)range: < 0.42.1-r0
- (no CPE)range: >= 0.35.11, < 0.35.15
- (no CPE)range: < 3.1.1~git13.a9c7ff9-1.1
- (no CPE)range: < 2.60.2-160000.1.1
- (no CPE)range: < 0.10.0+0-150500.3.6.1
- (no CPE)range: < 0.10.0+0-150500.3.6.1
- (no CPE)range: < 0.10.0+0-3.1
- (no CPE)range: < 0.2.7+70-2.1
- (no CPE)range: < 2.52.12-150400.3.9.1
- (no CPE)range: < 2.52.12-150400.3.9.1
- (no CPE)range: < 2.52.12-150400.3.9.1
- (no CPE)range: < 2.60.2-160000.1.1
- (no CPE)range: < 2.60.2-160000.1.1
- (no CPE)range: < 2.60.2-160000.1.1
- (no CPE)range: < 0.10.0+0-150500.3.6.1
- (no CPE)range: < 0.10.0+0-150500.3.6.1
- (no CPE)range: < 0.2.7+141-150400.3.7.1
- (no CPE)range: < 0.2.7+141-150400.3.5.1
- (no CPE)range: < 0.2.7+141-150500.3.5.1
- (no CPE)range: < 0.2.6+13-1.1
- (no CPE)range: < 0.2.8+12-slfo.1.1_1.1
Patches
8954bb0d176416534992521aa00b84d6aac233a53dfe16cdd31fd98ca723bMerge pull request from GHSA-c827-hfw6-qwvm
3 files changed · +224 −26
src/backend/libc/fs/dir.rs+74 −12 modified@@ -30,8 +30,13 @@ use core::ptr::NonNull; use libc_errno::{errno, set_errno, Errno}; /// `DIR*` -#[repr(transparent)] -pub struct Dir(NonNull<c::DIR>); +pub struct Dir { + /// The `libc` `DIR` pointer. + libc_dir: NonNull<c::DIR>, + + /// Have we seen any errors in this iteration? + any_errors: bool, +} impl Dir { /// Construct a `Dir` that reads entries from the given directory @@ -43,20 +48,35 @@ impl Dir { #[inline] fn _read_from(fd: BorrowedFd<'_>) -> io::Result<Self> { + let mut any_errors = false; + // Given an arbitrary `OwnedFd`, it's impossible to know whether the // user holds a `dup`'d copy which could continue to modify the // file description state, which would cause Undefined Behavior after // our call to `fdopendir`. To prevent this, we obtain an independent // `OwnedFd`. let flags = fcntl_getfl(fd)?; - let fd_for_dir = openat(fd, cstr!("."), flags | OFlags::CLOEXEC, Mode::empty())?; + let fd_for_dir = match openat(fd, cstr!("."), flags | OFlags::CLOEXEC, Mode::empty()) { + Ok(fd) => fd, + Err(io::Errno::NOENT) => { + // If "." doesn't exist, it means the directory was removed. + // We treat that as iterating through a directory with no + // entries. + any_errors = true; + crate::io::dup(fd)? + } + Err(err) => return Err(err), + }; let raw = owned_fd(fd_for_dir); unsafe { let libc_dir = c::fdopendir(raw); if let Some(libc_dir) = NonNull::new(libc_dir) { - Ok(Self(libc_dir)) + Ok(Self { + libc_dir, + any_errors, + }) } else { let err = io::Errno::last_os_error(); let _ = c::close(raw); @@ -68,20 +88,27 @@ impl Dir { /// `rewinddir(self)` #[inline] pub fn rewind(&mut self) { - unsafe { c::rewinddir(self.0.as_ptr()) } + self.any_errors = false; + unsafe { c::rewinddir(self.libc_dir.as_ptr()) } } /// `readdir(self)`, where `None` means the end of the directory. pub fn read(&mut self) -> Option<io::Result<DirEntry>> { + // If we've seen errors, don't continue to try to read anyting further. + if self.any_errors { + return None; + } + set_errno(Errno(0)); - let dirent_ptr = unsafe { libc_readdir(self.0.as_ptr()) }; + let dirent_ptr = unsafe { libc_readdir(self.libc_dir.as_ptr()) }; if dirent_ptr.is_null() { let curr_errno = errno().0; if curr_errno == 0 { // We successfully reached the end of the stream. None } else { // `errno` is unknown or non-zero, so an error occurred. + self.any_errors = true; Some(Err(io::Errno(curr_errno))) } } else { @@ -120,7 +147,7 @@ impl Dir { /// `fstat(self)` #[inline] pub fn stat(&self) -> io::Result<Stat> { - fstat(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.0.as_ptr())) }) + fstat(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.libc_dir.as_ptr())) }) } /// `fstatfs(self)` @@ -134,14 +161,14 @@ impl Dir { )))] #[inline] pub fn statfs(&self) -> io::Result<StatFs> { - fstatfs(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.0.as_ptr())) }) + fstatfs(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.libc_dir.as_ptr())) }) } /// `fstatvfs(self)` #[cfg(not(any(solarish, target_os = "haiku", target_os = "redox", target_os = "wasi")))] #[inline] pub fn statvfs(&self) -> io::Result<StatVfs> { - fstatvfs(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.0.as_ptr())) }) + fstatvfs(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.libc_dir.as_ptr())) }) } /// `fchdir(self)` @@ -150,7 +177,7 @@ impl Dir { #[cfg_attr(doc_cfg, doc(cfg(feature = "process")))] #[inline] pub fn chdir(&self) -> io::Result<()> { - fchdir(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.0.as_ptr())) }) + fchdir(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.libc_dir.as_ptr())) }) } } @@ -163,7 +190,7 @@ unsafe impl Send for Dir {} impl Drop for Dir { #[inline] fn drop(&mut self) { - unsafe { c::closedir(self.0.as_ptr()) }; + unsafe { c::closedir(self.libc_dir.as_ptr()) }; } } @@ -179,7 +206,7 @@ impl Iterator for Dir { impl fmt::Debug for Dir { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Dir") - .field("fd", unsafe { &c::dirfd(self.0.as_ptr()) }) + .field("fd", unsafe { &c::dirfd(self.libc_dir.as_ptr()) }) .finish() } } @@ -293,3 +320,38 @@ fn check_dirent_layout(dirent: &c::dirent) { } ); } + +#[test] +fn dir_iterator_handles_io_errors() { + // create a dir, keep the FD, then delete the dir + let tmp = tempfile::tempdir().unwrap(); + let fd = crate::fs::openat( + crate::fs::CWD, + tmp.path(), + crate::fs::OFlags::RDONLY | crate::fs::OFlags::CLOEXEC, + crate::fs::Mode::empty(), + ) + .unwrap(); + + let file_fd = crate::fs::openat( + &fd, + tmp.path().join("test.txt"), + crate::fs::OFlags::WRONLY | crate::fs::OFlags::CREATE, + crate::fs::Mode::RWXU, + ) + .unwrap(); + + let mut dir = Dir::read_from(&fd).unwrap(); + + // Reach inside the `Dir` and replace its directory with a file, which + // will cause the subsequent `readdir` to fail. + unsafe { + let raw_fd = c::dirfd(dir.libc_dir.as_ptr()); + let mut owned_fd: crate::fd::OwnedFd = crate::fd::FromRawFd::from_raw_fd(raw_fd); + crate::io::dup2(&file_fd, &mut owned_fd).unwrap(); + core::mem::forget(owned_fd); + } + + assert!(matches!(dir.next(), Some(Err(_)))); + assert!(matches!(dir.next(), None)); +}
src/backend/linux_raw/fs/dir.rs+82 −13 modified@@ -18,9 +18,17 @@ pub struct Dir { /// The `OwnedFd` that we read directory entries from. fd: OwnedFd, + /// Have we seen any errors in this iteration? + any_errors: bool, + + /// Should we rewind the stream on the next iteration? + rewind: bool, + + /// The buffer for `linux_dirent64` entries. buf: Vec<u8>, + + /// Where we are in the buffer. pos: usize, - next: Option<u64>, } impl Dir { @@ -38,25 +46,39 @@ impl Dir { Ok(Self { fd: fd_for_dir, + any_errors: false, + rewind: false, buf: Vec::new(), pos: 0, - next: None, }) } /// `rewinddir(self)` #[inline] pub fn rewind(&mut self) { + self.any_errors = false; + self.rewind = true; self.pos = self.buf.len(); - self.next = Some(0); } /// `readdir(self)`, where `None` means the end of the directory. pub fn read(&mut self) -> Option<io::Result<DirEntry>> { - if let Some(next) = self.next.take() { - match crate::backend::fs::syscalls::_seek(self.fd.as_fd(), next as i64, SEEK_SET) { + // If we've seen errors, don't continue to try to read anyting further. + if self.any_errors { + return None; + } + + // If a rewind was requested, seek to the beginning. + if self.rewind { + self.rewind = false; + match io::retry_on_intr(|| { + crate::backend::fs::syscalls::_seek(self.fd.as_fd(), 0, SEEK_SET) + }) { Ok(_) => (), - Err(err) => return Some(Err(err)), + Err(err) => { + self.any_errors = true; + return Some(Err(err)); + } } } @@ -78,7 +100,7 @@ impl Dir { if self.buf.len() - self.pos < size_of::<linux_dirent64>() { match self.read_more()? { Ok(()) => (), - Err(e) => return Some(Err(e)), + Err(err) => return Some(Err(err)), } } @@ -136,14 +158,31 @@ impl Dir { } fn read_more(&mut self) -> Option<io::Result<()>> { - let og_len = self.buf.len(); - // Capacity increment currently chosen by wild guess. - self.buf - .resize(self.buf.capacity() + 32 * size_of::<linux_dirent64>(), 0); - let nread = match crate::backend::fs::syscalls::getdents(self.fd.as_fd(), &mut self.buf) { + // The first few times we're called, we allocate a relatively small + // buffer, because many directories are small. If we're called more, + // use progressively larger allocations, up to a fixed maximum. + // + // The specific sizes and policy here have not been tuned in detail yet + // and may need to be adjusted. In doing so, we should be careful to + // avoid unbounded buffer growth. This buffer only exists to share the + // cost of a `getdents` call over many entries, so if it gets too big, + // cache and heap usage will outweigh the benefit. And ultimately, + // directories can contain more entries than we can allocate contiguous + // memory for, so we'll always need to cap the size at some point. + if self.buf.len() < 1024 * size_of::<linux_dirent64>() { + self.buf.reserve(32 * size_of::<linux_dirent64>()); + } + self.buf.resize(self.buf.capacity(), 0); + let nread = match io::retry_on_intr(|| { + crate::backend::fs::syscalls::getdents(self.fd.as_fd(), &mut self.buf) + }) { Ok(nread) => nread, + Err(io::Errno::NOENT) => { + self.any_errors = true; + return None; + } Err(err) => { - self.buf.resize(og_len, 0); + self.any_errors = true; return Some(Err(err)); } }; @@ -225,3 +264,33 @@ impl DirEntry { self.d_ino } } + +#[test] +fn dir_iterator_handles_io_errors() { + // create a dir, keep the FD, then delete the dir + let tmp = tempfile::tempdir().unwrap(); + let fd = crate::fs::openat( + crate::fs::CWD, + tmp.path(), + crate::fs::OFlags::RDONLY | crate::fs::OFlags::CLOEXEC, + crate::fs::Mode::empty(), + ) + .unwrap(); + + let file_fd = crate::fs::openat( + &fd, + tmp.path().join("test.txt"), + crate::fs::OFlags::WRONLY | crate::fs::OFlags::CREATE, + crate::fs::Mode::RWXU, + ) + .unwrap(); + + let mut dir = Dir::read_from(&fd).unwrap(); + + // Reach inside the `Dir` and replace its directory with a file, which + // will cause the subsequent `getdents64` to fail. + crate::io::dup2(&file_fd, &mut dir.fd).unwrap(); + + assert!(matches!(dir.next(), Some(Err(_)))); + assert!(matches!(dir.next(), None)); +}
tests/fs/dir.rs+68 −1 modified@@ -8,7 +8,7 @@ fn test_dir() { ) .unwrap(); - let dir = rustix::fs::Dir::read_from(&t).unwrap(); + let mut dir = rustix::fs::Dir::read_from(&t).unwrap(); let _file = rustix::fs::openat( &t, @@ -18,6 +18,32 @@ fn test_dir() { ) .unwrap(); + // Read the directory entries. We use `while let Some(entry)` so that we + // don't consume the `Dir` so that we can run more tests on it. + let mut saw_dot = false; + let mut saw_dotdot = false; + let mut saw_cargo_toml = false; + while let Some(entry) = dir.read() { + let entry = entry.unwrap(); + if entry.file_name() == rustix::cstr!(".") { + saw_dot = true; + } else if entry.file_name() == rustix::cstr!("..") { + saw_dotdot = true; + } else if entry.file_name() == rustix::cstr!("Cargo.toml") { + saw_cargo_toml = true; + } + } + assert!(saw_dot); + assert!(saw_dotdot); + assert!(saw_cargo_toml); + + // Rewind the directory so we can iterate over the entries again. + dir.rewind(); + + // For what comes next, we don't need `mut` anymore. + let dir = dir; + + // Read the directory entries, again. This time we use `for entry in dir`. let mut saw_dot = false; let mut saw_dotdot = false; let mut saw_cargo_toml = false; @@ -35,3 +61,44 @@ fn test_dir() { assert!(saw_dotdot); assert!(saw_cargo_toml); } + +#[test] +fn dir_iterator_handles_dir_removal() { + // create a dir, keep the FD, then delete the dir + let tmp = tempfile::tempdir().unwrap(); + let fd = rustix::fs::openat( + rustix::fs::CWD, + tmp.path(), + rustix::fs::OFlags::RDONLY | rustix::fs::OFlags::CLOEXEC, + rustix::fs::Mode::empty(), + ) + .unwrap(); + + // Drop the `TempDir`, which deletes the directory. + drop(tmp); + + let mut dir = rustix::fs::Dir::read_from(&fd).unwrap(); + assert!(matches!(dir.next(), None)); +} + +// Like `dir_iterator_handles_dir_removal`, but close the directory after +// `Dir::read_from`. +#[test] +fn dir_iterator_handles_dir_removal_after_open() { + // create a dir, keep the FD, then delete the dir + let tmp = tempfile::tempdir().unwrap(); + let fd = rustix::fs::openat( + rustix::fs::CWD, + tmp.path(), + rustix::fs::OFlags::RDONLY | rustix::fs::OFlags::CLOEXEC, + rustix::fs::Mode::empty(), + ) + .unwrap(); + + let mut dir = rustix::fs::Dir::read_from(&fd).unwrap(); + + // Drop the `TempDir`, which deletes the directory. + drop(tmp); + + assert!(matches!(dir.next(), None)); +}
87481a97f436Merge pull request from GHSA-c827-hfw6-qwvm
3 files changed · +224 −26
src/backend/libc/fs/dir.rs+74 −12 modified@@ -57,8 +57,13 @@ use core::ptr::NonNull; use libc_errno::{errno, set_errno, Errno}; /// `DIR*` -#[repr(transparent)] -pub struct Dir(NonNull<c::DIR>); +pub struct Dir { + /// The `libc` `DIR` pointer. + libc_dir: NonNull<c::DIR>, + + /// Have we seen any errors in this iteration? + any_errors: bool, +} impl Dir { /// Construct a `Dir` that reads entries from the given directory @@ -70,20 +75,35 @@ impl Dir { #[inline] fn _read_from(fd: BorrowedFd<'_>) -> io::Result<Self> { + let mut any_errors = false; + // Given an arbitrary `OwnedFd`, it's impossible to know whether the // user holds a `dup`'d copy which could continue to modify the // file description state, which would cause Undefined Behavior after // our call to `fdopendir`. To prevent this, we obtain an independent // `OwnedFd`. let flags = fcntl_getfl(fd)?; - let fd_for_dir = openat(fd, cstr!("."), flags | OFlags::CLOEXEC, Mode::empty())?; + let fd_for_dir = match openat(fd, cstr!("."), flags | OFlags::CLOEXEC, Mode::empty()) { + Ok(fd) => fd, + Err(io::Errno::NOENT) => { + // If "." doesn't exist, it means the directory was removed. + // We treat that as iterating through a directory with no + // entries. + any_errors = true; + crate::io::dup(fd)? + } + Err(err) => return Err(err), + }; let raw = owned_fd(fd_for_dir); unsafe { let libc_dir = c::fdopendir(raw); if let Some(libc_dir) = NonNull::new(libc_dir) { - Ok(Self(libc_dir)) + Ok(Self { + libc_dir, + any_errors, + }) } else { let err = io::Errno::last_os_error(); let _ = c::close(raw); @@ -95,20 +115,27 @@ impl Dir { /// `rewinddir(self)` #[inline] pub fn rewind(&mut self) { - unsafe { c::rewinddir(self.0.as_ptr()) } + self.any_errors = false; + unsafe { c::rewinddir(self.libc_dir.as_ptr()) } } /// `readdir(self)`, where `None` means the end of the directory. pub fn read(&mut self) -> Option<io::Result<DirEntry>> { + // If we've seen errors, don't continue to try to read anyting further. + if self.any_errors { + return None; + } + set_errno(Errno(0)); - let dirent_ptr = unsafe { libc_readdir(self.0.as_ptr()) }; + let dirent_ptr = unsafe { libc_readdir(self.libc_dir.as_ptr()) }; if dirent_ptr.is_null() { let curr_errno = errno().0; if curr_errno == 0 { // We successfully reached the end of the stream. None } else { // `errno` is unknown or non-zero, so an error occurred. + self.any_errors = true; Some(Err(io::Errno(curr_errno))) } } else { @@ -134,7 +161,7 @@ impl Dir { /// `fstat(self)` #[inline] pub fn stat(&self) -> io::Result<Stat> { - fstat(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.0.as_ptr())) }) + fstat(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.libc_dir.as_ptr())) }) } /// `fstatfs(self)` @@ -148,7 +175,7 @@ impl Dir { )))] #[inline] pub fn statfs(&self) -> io::Result<StatFs> { - fstatfs(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.0.as_ptr())) }) + fstatfs(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.libc_dir.as_ptr())) }) } /// `fstatvfs(self)` @@ -161,14 +188,14 @@ impl Dir { )))] #[inline] pub fn statvfs(&self) -> io::Result<StatVfs> { - fstatvfs(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.0.as_ptr())) }) + fstatvfs(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.libc_dir.as_ptr())) }) } /// `fchdir(self)` #[cfg(not(any(target_os = "fuchsia", target_os = "wasi")))] #[inline] pub fn chdir(&self) -> io::Result<()> { - fchdir(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.0.as_ptr())) }) + fchdir(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.libc_dir.as_ptr())) }) } } @@ -342,7 +369,7 @@ unsafe impl Send for Dir {} impl Drop for Dir { #[inline] fn drop(&mut self) { - unsafe { c::closedir(self.0.as_ptr()) }; + unsafe { c::closedir(self.libc_dir.as_ptr()) }; } } @@ -358,7 +385,7 @@ impl Iterator for Dir { impl fmt::Debug for Dir { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Dir") - .field("fd", unsafe { &c::dirfd(self.0.as_ptr()) }) + .field("fd", unsafe { &c::dirfd(self.libc_dir.as_ptr()) }) .finish() } } @@ -485,3 +512,38 @@ fn check_dirent_layout(dirent: &c::dirent) { } ); } + +#[test] +fn dir_iterator_handles_io_errors() { + // create a dir, keep the FD, then delete the dir + let tmp = tempfile::tempdir().unwrap(); + let fd = crate::fs::openat( + crate::fs::cwd(), + tmp.path(), + crate::fs::OFlags::RDONLY | crate::fs::OFlags::CLOEXEC, + crate::fs::Mode::empty(), + ) + .unwrap(); + + let file_fd = crate::fs::openat( + &fd, + tmp.path().join("test.txt"), + crate::fs::OFlags::WRONLY | crate::fs::OFlags::CREATE, + crate::fs::Mode::RWXU, + ) + .unwrap(); + + let mut dir = Dir::read_from(&fd).unwrap(); + + // Reach inside the `Dir` and replace its directory with a file, which + // will cause the subsequent `readdir` to fail. + unsafe { + let raw_fd = c::dirfd(dir.libc_dir.as_ptr()); + let mut owned_fd: crate::fd::OwnedFd = crate::fd::FromRawFd::from_raw_fd(raw_fd); + crate::io::dup2(&file_fd, &mut owned_fd).unwrap(); + core::mem::forget(owned_fd); + } + + assert!(matches!(dir.next(), Some(Err(_)))); + assert!(matches!(dir.next(), None)); +}
src/backend/linux_raw/fs/dir.rs+82 −13 modified@@ -17,9 +17,17 @@ pub struct Dir { /// The `OwnedFd` that we read directory entries from. fd: OwnedFd, + /// Have we seen any errors in this iteration? + any_errors: bool, + + /// Should we rewind the stream on the next iteration? + rewind: bool, + + /// The buffer for `linux_dirent64` entries. buf: Vec<u8>, + + /// Where we are in the buffer. pos: usize, - next: Option<u64>, } impl Dir { @@ -37,25 +45,39 @@ impl Dir { Ok(Self { fd: fd_for_dir, + any_errors: false, + rewind: false, buf: Vec::new(), pos: 0, - next: None, }) } /// `rewinddir(self)` #[inline] pub fn rewind(&mut self) { + self.any_errors = false; + self.rewind = true; self.pos = self.buf.len(); - self.next = Some(0); } /// `readdir(self)`, where `None` means the end of the directory. pub fn read(&mut self) -> Option<io::Result<DirEntry>> { - if let Some(next) = self.next.take() { - match crate::backend::fs::syscalls::_seek(self.fd.as_fd(), next as i64, SEEK_SET) { + // If we've seen errors, don't continue to try to read anyting further. + if self.any_errors { + return None; + } + + // If a rewind was requested, seek to the beginning. + if self.rewind { + self.rewind = false; + match io::retry_on_intr(|| { + crate::backend::fs::syscalls::_seek(self.fd.as_fd(), 0, SEEK_SET) + }) { Ok(_) => (), - Err(err) => return Some(Err(err)), + Err(err) => { + self.any_errors = true; + return Some(Err(err)); + } } } @@ -77,7 +99,7 @@ impl Dir { if self.buf.len() - self.pos < size_of::<linux_dirent64>() { match self.read_more()? { Ok(()) => (), - Err(e) => return Some(Err(e)), + Err(err) => return Some(Err(err)), } } @@ -136,14 +158,31 @@ impl Dir { } fn read_more(&mut self) -> Option<io::Result<()>> { - let og_len = self.buf.len(); - // Capacity increment currently chosen by wild guess. - self.buf - .resize(self.buf.capacity() + 32 * size_of::<linux_dirent64>(), 0); - let nread = match crate::backend::fs::syscalls::getdents(self.fd.as_fd(), &mut self.buf) { + // The first few times we're called, we allocate a relatively small + // buffer, because many directories are small. If we're called more, + // use progressively larger allocations, up to a fixed maximum. + // + // The specific sizes and policy here have not been tuned in detail yet + // and may need to be adjusted. In doing so, we should be careful to + // avoid unbounded buffer growth. This buffer only exists to share the + // cost of a `getdents` call over many entries, so if it gets too big, + // cache and heap usage will outweigh the benefit. And ultimately, + // directories can contain more entries than we can allocate contiguous + // memory for, so we'll always need to cap the size at some point. + if self.buf.len() < 1024 * size_of::<linux_dirent64>() { + self.buf.reserve(32 * size_of::<linux_dirent64>()); + } + self.buf.resize(self.buf.capacity(), 0); + let nread = match io::retry_on_intr(|| { + crate::backend::fs::syscalls::getdents(self.fd.as_fd(), &mut self.buf) + }) { Ok(nread) => nread, + Err(io::Errno::NOENT) => { + self.any_errors = true; + return None; + } Err(err) => { - self.buf.resize(og_len, 0); + self.any_errors = true; return Some(Err(err)); } }; @@ -223,3 +262,33 @@ impl DirEntry { self.d_ino } } + +#[test] +fn dir_iterator_handles_io_errors() { + // create a dir, keep the FD, then delete the dir + let tmp = tempfile::tempdir().unwrap(); + let fd = crate::fs::openat( + crate::fs::cwd(), + tmp.path(), + crate::fs::OFlags::RDONLY | crate::fs::OFlags::CLOEXEC, + crate::fs::Mode::empty(), + ) + .unwrap(); + + let file_fd = crate::fs::openat( + &fd, + tmp.path().join("test.txt"), + crate::fs::OFlags::WRONLY | crate::fs::OFlags::CREATE, + crate::fs::Mode::RWXU, + ) + .unwrap(); + + let mut dir = Dir::read_from(&fd).unwrap(); + + // Reach inside the `Dir` and replace its directory with a file, which + // will cause the subsequent `getdents64` to fail. + crate::io::dup2(&file_fd, &mut dir.fd).unwrap(); + + assert!(matches!(dir.next(), Some(Err(_)))); + assert!(matches!(dir.next(), None)); +}
tests/fs/dir.rs+68 −1 modified@@ -8,7 +8,7 @@ fn test_dir() { ) .unwrap(); - let dir = rustix::fs::Dir::read_from(&t).unwrap(); + let mut dir = rustix::fs::Dir::read_from(&t).unwrap(); let _file = rustix::fs::openat( &t, @@ -18,6 +18,32 @@ fn test_dir() { ) .unwrap(); + // Read the directory entries. We use `while let Some(entry)` so that we + // don't consume the `Dir` so that we can run more tests on it. + let mut saw_dot = false; + let mut saw_dotdot = false; + let mut saw_cargo_toml = false; + while let Some(entry) = dir.read() { + let entry = entry.unwrap(); + if entry.file_name() == rustix::cstr!(".") { + saw_dot = true; + } else if entry.file_name() == rustix::cstr!("..") { + saw_dotdot = true; + } else if entry.file_name() == rustix::cstr!("Cargo.toml") { + saw_cargo_toml = true; + } + } + assert!(saw_dot); + assert!(saw_dotdot); + assert!(saw_cargo_toml); + + // Rewind the directory so we can iterate over the entries again. + dir.rewind(); + + // For what comes next, we don't need `mut` anymore. + let dir = dir; + + // Read the directory entries, again. This time we use `for entry in dir`. let mut saw_dot = false; let mut saw_dotdot = false; let mut saw_cargo_toml = false; @@ -35,3 +61,44 @@ fn test_dir() { assert!(saw_dotdot); assert!(saw_cargo_toml); } + +#[test] +fn dir_iterator_handles_dir_removal() { + // create a dir, keep the FD, then delete the dir + let tmp = tempfile::tempdir().unwrap(); + let fd = rustix::fs::openat( + rustix::fs::cwd(), + tmp.path(), + rustix::fs::OFlags::RDONLY | rustix::fs::OFlags::CLOEXEC, + rustix::fs::Mode::empty(), + ) + .unwrap(); + + // Drop the `TempDir`, which deletes the directory. + drop(tmp); + + let mut dir = rustix::fs::Dir::read_from(&fd).unwrap(); + assert!(matches!(dir.next(), None)); +} + +// Like `dir_iterator_handles_dir_removal`, but close the directory after +// `Dir::read_from`. +#[test] +fn dir_iterator_handles_dir_removal_after_open() { + // create a dir, keep the FD, then delete the dir + let tmp = tempfile::tempdir().unwrap(); + let fd = rustix::fs::openat( + rustix::fs::cwd(), + tmp.path(), + rustix::fs::OFlags::RDONLY | rustix::fs::OFlags::CLOEXEC, + rustix::fs::Mode::empty(), + ) + .unwrap(); + + let mut dir = rustix::fs::Dir::read_from(&fd).unwrap(); + + // Drop the `TempDir`, which deletes the directory. + drop(tmp); + + assert!(matches!(dir.next(), None)); +}
eecece4a84fcMerge pull request from GHSA-c827-hfw6-qwvm
3 files changed · +225 −26
src/backend/libc/fs/dir.rs+75 −12 modified@@ -57,8 +57,13 @@ use core::ptr::NonNull; use libc_errno::{errno, set_errno, Errno}; /// `DIR*` -#[repr(transparent)] -pub struct Dir(NonNull<c::DIR>); +pub struct Dir { + /// The `libc` `DIR` pointer. + libc_dir: NonNull<c::DIR>, + + /// Have we seen any errors in this iteration? + any_errors: bool, +} impl Dir { /// Construct a `Dir` that reads entries from the given directory @@ -70,20 +75,35 @@ impl Dir { #[inline] fn _read_from(fd: BorrowedFd<'_>) -> io::Result<Self> { + let mut any_errors = false; + // Given an arbitrary `OwnedFd`, it's impossible to know whether the // user holds a `dup`'d copy which could continue to modify the // file description state, which would cause Undefined Behavior after // our call to `fdopendir`. To prevent this, we obtain an independent // `OwnedFd`. let flags = fcntl_getfl(&fd)?; - let fd_for_dir = openat(&fd, cstr!("."), flags | OFlags::CLOEXEC, Mode::empty())?; + let fd_for_dir = match openat(&fd, cstr!("."), flags | OFlags::CLOEXEC, Mode::empty()) { + Ok(fd) => fd, + Err(io::Errno::NOENT) => { + // If "." doesn't exist, it means the directory was removed. + // We treat that as iterating through a directory with no + // entries. + any_errors = true; + crate::io::dup(fd)? + } + Err(err) => return Err(err), + }; let raw = owned_fd(fd_for_dir); unsafe { let libc_dir = c::fdopendir(raw); if let Some(libc_dir) = NonNull::new(libc_dir) { - Ok(Self(libc_dir)) + Ok(Self { + libc_dir, + any_errors, + }) } else { let err = io::Errno::last_os_error(); let _ = c::close(raw); @@ -95,20 +115,27 @@ impl Dir { /// `rewinddir(self)` #[inline] pub fn rewind(&mut self) { - unsafe { c::rewinddir(self.0.as_ptr()) } + self.any_errors = false; + unsafe { c::rewinddir(self.libc_dir.as_ptr()) } } /// `readdir(self)`, where `None` means the end of the directory. pub fn read(&mut self) -> Option<io::Result<DirEntry>> { + // If we've seen errors, don't continue to try to read anyting further. + if self.any_errors { + return None; + } + set_errno(Errno(0)); - let dirent_ptr = unsafe { libc_readdir(self.0.as_ptr()) }; + let dirent_ptr = unsafe { libc_readdir(self.libc_dir.as_ptr()) }; if dirent_ptr.is_null() { let curr_errno = errno().0; if curr_errno == 0 { // We successfully reached the end of the stream. None } else { // `errno` is unknown or non-zero, so an error occurred. + self.any_errors = true; Some(Err(io::Errno(curr_errno))) } } else { @@ -134,7 +161,7 @@ impl Dir { /// `fstat(self)` #[inline] pub fn stat(&self) -> io::Result<Stat> { - fstat(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.0.as_ptr())) }) + fstat(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.libc_dir.as_ptr())) }) } /// `fstatfs(self)` @@ -148,7 +175,7 @@ impl Dir { )))] #[inline] pub fn statfs(&self) -> io::Result<StatFs> { - fstatfs(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.0.as_ptr())) }) + fstatfs(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.libc_dir.as_ptr())) }) } /// `fstatvfs(self)` @@ -161,14 +188,14 @@ impl Dir { )))] #[inline] pub fn statvfs(&self) -> io::Result<StatVfs> { - fstatvfs(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.0.as_ptr())) }) + fstatvfs(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.libc_dir.as_ptr())) }) } /// `fchdir(self)` #[cfg(not(any(target_os = "fuchsia", target_os = "wasi")))] #[inline] pub fn chdir(&self) -> io::Result<()> { - fchdir(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.0.as_ptr())) }) + fchdir(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.libc_dir.as_ptr())) }) } } @@ -338,7 +365,7 @@ unsafe impl Send for Dir {} impl Drop for Dir { #[inline] fn drop(&mut self) { - unsafe { c::closedir(self.0.as_ptr()) }; + unsafe { c::closedir(self.libc_dir.as_ptr()) }; } } @@ -354,7 +381,7 @@ impl Iterator for Dir { impl fmt::Debug for Dir { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Dir") - .field("fd", unsafe { &c::dirfd(self.0.as_ptr()) }) + .field("fd", unsafe { &c::dirfd(self.libc_dir.as_ptr()) }) .finish() } } @@ -481,3 +508,39 @@ fn check_dirent_layout(dirent: &c::dirent) { } ); } + +#[test] +fn dir_iterator_handles_io_errors() { + // create a dir, keep the FD, then delete the dir + let tmp = tempfile::tempdir().unwrap(); + let fd = crate::fs::openat( + crate::fs::cwd(), + tmp.path(), + crate::fs::OFlags::RDONLY | crate::fs::OFlags::CLOEXEC, + crate::fs::Mode::empty(), + ) + .unwrap(); + + let file_fd = crate::fs::openat( + &fd, + tmp.path().join("test.txt"), + crate::fs::OFlags::WRONLY | crate::fs::OFlags::CREATE, + crate::fs::Mode::RWXU, + ) + .unwrap(); + + let mut dir = Dir::read_from(&fd).unwrap(); + + // Reach inside the `Dir` and replace its directory with a file, which + // will cause the subsequent `readdir` to fail. + unsafe { + let raw_fd = c::dirfd(dir.libc_dir.as_ptr()); + let owned_fd: crate::fd::OwnedFd = crate::fd::FromRawFd::from_raw_fd(raw_fd); + let mut owned_fd: crate::io::OwnedFd = owned_fd.into(); + crate::io::dup2(&file_fd, &mut owned_fd).unwrap(); + core::mem::forget(owned_fd); + } + + assert!(matches!(dir.next(), Some(Err(_)))); + assert!(matches!(dir.next(), None)); +}
src/backend/linux_raw/fs/dir.rs+82 −13 modified@@ -17,9 +17,17 @@ pub struct Dir { /// The `OwnedFd` that we read directory entries from. fd: OwnedFd, + /// Have we seen any errors in this iteration? + any_errors: bool, + + /// Should we rewind the stream on the next iteration? + rewind: bool, + + /// The buffer for `linux_dirent64` entries. buf: Vec<u8>, + + /// Where we are in the buffer. pos: usize, - next: Option<u64>, } impl Dir { @@ -37,25 +45,39 @@ impl Dir { Ok(Self { fd: fd_for_dir, + any_errors: false, + rewind: false, buf: Vec::new(), pos: 0, - next: None, }) } /// `rewinddir(self)` #[inline] pub fn rewind(&mut self) { + self.any_errors = false; + self.rewind = true; self.pos = self.buf.len(); - self.next = Some(0); } /// `readdir(self)`, where `None` means the end of the directory. pub fn read(&mut self) -> Option<io::Result<DirEntry>> { - if let Some(next) = self.next.take() { - match crate::backend::fs::syscalls::_seek(self.fd.as_fd(), next as i64, SEEK_SET) { + // If we've seen errors, don't continue to try to read anyting further. + if self.any_errors { + return None; + } + + // If a rewind was requested, seek to the beginning. + if self.rewind { + self.rewind = false; + match io::retry_on_intr(|| { + crate::backend::fs::syscalls::_seek(self.fd.as_fd(), 0, SEEK_SET) + }) { Ok(_) => (), - Err(err) => return Some(Err(err)), + Err(err) => { + self.any_errors = true; + return Some(Err(err)); + } } } @@ -77,7 +99,7 @@ impl Dir { if self.buf.len() - self.pos < size_of::<linux_dirent64>() { match self.read_more()? { Ok(()) => (), - Err(e) => return Some(Err(e)), + Err(err) => return Some(Err(err)), } } @@ -136,14 +158,31 @@ impl Dir { } fn read_more(&mut self) -> Option<io::Result<()>> { - let og_len = self.buf.len(); - // Capacity increment currently chosen by wild guess. - self.buf - .resize(self.buf.capacity() + 32 * size_of::<linux_dirent64>(), 0); - let nread = match crate::backend::fs::syscalls::getdents(self.fd.as_fd(), &mut self.buf) { + // The first few times we're called, we allocate a relatively small + // buffer, because many directories are small. If we're called more, + // use progressively larger allocations, up to a fixed maximum. + // + // The specific sizes and policy here have not been tuned in detail yet + // and may need to be adjusted. In doing so, we should be careful to + // avoid unbounded buffer growth. This buffer only exists to share the + // cost of a `getdents` call over many entries, so if it gets too big, + // cache and heap usage will outweigh the benefit. And ultimately, + // directories can contain more entries than we can allocate contiguous + // memory for, so we'll always need to cap the size at some point. + if self.buf.len() < 1024 * size_of::<linux_dirent64>() { + self.buf.reserve(32 * size_of::<linux_dirent64>()); + } + self.buf.resize(self.buf.capacity(), 0); + let nread = match io::retry_on_intr(|| { + crate::backend::fs::syscalls::getdents(self.fd.as_fd(), &mut self.buf) + }) { Ok(nread) => nread, + Err(io::Errno::NOENT) => { + self.any_errors = true; + return None; + } Err(err) => { - self.buf.resize(og_len, 0); + self.any_errors = true; return Some(Err(err)); } }; @@ -223,3 +262,33 @@ impl DirEntry { self.d_ino } } + +#[test] +fn dir_iterator_handles_io_errors() { + // create a dir, keep the FD, then delete the dir + let tmp = tempfile::tempdir().unwrap(); + let fd = crate::fs::openat( + crate::fs::cwd(), + tmp.path(), + crate::fs::OFlags::RDONLY | crate::fs::OFlags::CLOEXEC, + crate::fs::Mode::empty(), + ) + .unwrap(); + + let file_fd = crate::fs::openat( + &fd, + tmp.path().join("test.txt"), + crate::fs::OFlags::WRONLY | crate::fs::OFlags::CREATE, + crate::fs::Mode::RWXU, + ) + .unwrap(); + + let mut dir = Dir::read_from(&fd).unwrap(); + + // Reach inside the `Dir` and replace its directory with a file, which + // will cause the subsequent `getdents64` to fail. + crate::io::dup2(&file_fd, &mut dir.fd).unwrap(); + + assert!(matches!(dir.next(), Some(Err(_)))); + assert!(matches!(dir.next(), None)); +}
tests/fs/dir.rs+68 −1 modified@@ -8,7 +8,7 @@ fn test_dir() { ) .unwrap(); - let dir = rustix::fs::Dir::read_from(&t).unwrap(); + let mut dir = rustix::fs::Dir::read_from(&t).unwrap(); let _file = rustix::fs::openat( &t, @@ -18,6 +18,32 @@ fn test_dir() { ) .unwrap(); + // Read the directory entries. We use `while let Some(entry)` so that we + // don't consume the `Dir` so that we can run more tests on it. + let mut saw_dot = false; + let mut saw_dotdot = false; + let mut saw_cargo_toml = false; + while let Some(entry) = dir.read() { + let entry = entry.unwrap(); + if entry.file_name() == rustix::cstr!(".") { + saw_dot = true; + } else if entry.file_name() == rustix::cstr!("..") { + saw_dotdot = true; + } else if entry.file_name() == rustix::cstr!("Cargo.toml") { + saw_cargo_toml = true; + } + } + assert!(saw_dot); + assert!(saw_dotdot); + assert!(saw_cargo_toml); + + // Rewind the directory so we can iterate over the entries again. + dir.rewind(); + + // For what comes next, we don't need `mut` anymore. + let dir = dir; + + // Read the directory entries, again. This time we use `for entry in dir`. let mut saw_dot = false; let mut saw_dotdot = false; let mut saw_cargo_toml = false; @@ -35,3 +61,44 @@ fn test_dir() { assert!(saw_dotdot); assert!(saw_cargo_toml); } + +#[test] +fn dir_iterator_handles_dir_removal() { + // create a dir, keep the FD, then delete the dir + let tmp = tempfile::tempdir().unwrap(); + let fd = rustix::fs::openat( + rustix::fs::cwd(), + tmp.path(), + rustix::fs::OFlags::RDONLY | rustix::fs::OFlags::CLOEXEC, + rustix::fs::Mode::empty(), + ) + .unwrap(); + + // Drop the `TempDir`, which deletes the directory. + drop(tmp); + + let mut dir = rustix::fs::Dir::read_from(&fd).unwrap(); + assert!(matches!(dir.next(), None)); +} + +// Like `dir_iterator_handles_dir_removal`, but close the directory after +// `Dir::read_from`. +#[test] +fn dir_iterator_handles_dir_removal_after_open() { + // create a dir, keep the FD, then delete the dir + let tmp = tempfile::tempdir().unwrap(); + let fd = rustix::fs::openat( + rustix::fs::cwd(), + tmp.path(), + rustix::fs::OFlags::RDONLY | rustix::fs::OFlags::CLOEXEC, + rustix::fs::Mode::empty(), + ) + .unwrap(); + + let mut dir = rustix::fs::Dir::read_from(&fd).unwrap(); + + // Drop the `TempDir`, which deletes the directory. + drop(tmp); + + assert!(matches!(dir.next(), None)); +}
df3c3a192cf1Merge pull request from GHSA-c827-hfw6-qwvm
3 files changed · +224 −26
src/backend/libc/fs/dir.rs+74 −12 modified@@ -29,8 +29,13 @@ use core::ptr::NonNull; use libc_errno::{errno, set_errno, Errno}; /// `DIR*` -#[repr(transparent)] -pub struct Dir(NonNull<c::DIR>); +pub struct Dir { + /// The `libc` `DIR` pointer. + libc_dir: NonNull<c::DIR>, + + /// Have we seen any errors in this iteration? + any_errors: bool, +} impl Dir { /// Construct a `Dir` that reads entries from the given directory @@ -42,20 +47,35 @@ impl Dir { #[inline] fn _read_from(fd: BorrowedFd<'_>) -> io::Result<Self> { + let mut any_errors = false; + // Given an arbitrary `OwnedFd`, it's impossible to know whether the // user holds a `dup`'d copy which could continue to modify the // file description state, which would cause Undefined Behavior after // our call to `fdopendir`. To prevent this, we obtain an independent // `OwnedFd`. let flags = fcntl_getfl(fd)?; - let fd_for_dir = openat(fd, cstr!("."), flags | OFlags::CLOEXEC, Mode::empty())?; + let fd_for_dir = match openat(fd, cstr!("."), flags | OFlags::CLOEXEC, Mode::empty()) { + Ok(fd) => fd, + Err(io::Errno::NOENT) => { + // If "." doesn't exist, it means the directory was removed. + // We treat that as iterating through a directory with no + // entries. + any_errors = true; + crate::io::dup(fd)? + } + Err(err) => return Err(err), + }; let raw = owned_fd(fd_for_dir); unsafe { let libc_dir = c::fdopendir(raw); if let Some(libc_dir) = NonNull::new(libc_dir) { - Ok(Self(libc_dir)) + Ok(Self { + libc_dir, + any_errors, + }) } else { let err = io::Errno::last_os_error(); let _ = c::close(raw); @@ -67,20 +87,27 @@ impl Dir { /// `rewinddir(self)` #[inline] pub fn rewind(&mut self) { - unsafe { c::rewinddir(self.0.as_ptr()) } + self.any_errors = false; + unsafe { c::rewinddir(self.libc_dir.as_ptr()) } } /// `readdir(self)`, where `None` means the end of the directory. pub fn read(&mut self) -> Option<io::Result<DirEntry>> { + // If we've seen errors, don't continue to try to read anyting further. + if self.any_errors { + return None; + } + set_errno(Errno(0)); - let dirent_ptr = unsafe { libc_readdir(self.0.as_ptr()) }; + let dirent_ptr = unsafe { libc_readdir(self.libc_dir.as_ptr()) }; if dirent_ptr.is_null() { let curr_errno = errno().0; if curr_errno == 0 { // We successfully reached the end of the stream. None } else { // `errno` is unknown or non-zero, so an error occurred. + self.any_errors = true; Some(Err(io::Errno(curr_errno))) } } else { @@ -114,7 +141,7 @@ impl Dir { /// `fstat(self)` #[inline] pub fn stat(&self) -> io::Result<Stat> { - fstat(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.0.as_ptr())) }) + fstat(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.libc_dir.as_ptr())) }) } /// `fstatfs(self)` @@ -127,21 +154,21 @@ impl Dir { )))] #[inline] pub fn statfs(&self) -> io::Result<StatFs> { - fstatfs(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.0.as_ptr())) }) + fstatfs(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.libc_dir.as_ptr())) }) } /// `fstatvfs(self)` #[cfg(not(any(solarish, target_os = "haiku", target_os = "redox", target_os = "wasi")))] #[inline] pub fn statvfs(&self) -> io::Result<StatVfs> { - fstatvfs(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.0.as_ptr())) }) + fstatvfs(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.libc_dir.as_ptr())) }) } /// `fchdir(self)` #[cfg(not(any(target_os = "fuchsia", target_os = "wasi")))] #[inline] pub fn chdir(&self) -> io::Result<()> { - fchdir(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.0.as_ptr())) }) + fchdir(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.libc_dir.as_ptr())) }) } } @@ -154,7 +181,7 @@ unsafe impl Send for Dir {} impl Drop for Dir { #[inline] fn drop(&mut self) { - unsafe { c::closedir(self.0.as_ptr()) }; + unsafe { c::closedir(self.libc_dir.as_ptr()) }; } } @@ -170,7 +197,7 @@ impl Iterator for Dir { impl fmt::Debug for Dir { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Dir") - .field("fd", unsafe { &c::dirfd(self.0.as_ptr()) }) + .field("fd", unsafe { &c::dirfd(self.libc_dir.as_ptr()) }) .finish() } } @@ -282,3 +309,38 @@ fn check_dirent_layout(dirent: &c::dirent) { } ); } + +#[test] +fn dir_iterator_handles_io_errors() { + // create a dir, keep the FD, then delete the dir + let tmp = tempfile::tempdir().unwrap(); + let fd = crate::fs::openat( + crate::fs::cwd(), + tmp.path(), + crate::fs::OFlags::RDONLY | crate::fs::OFlags::CLOEXEC, + crate::fs::Mode::empty(), + ) + .unwrap(); + + let file_fd = crate::fs::openat( + &fd, + tmp.path().join("test.txt"), + crate::fs::OFlags::WRONLY | crate::fs::OFlags::CREATE, + crate::fs::Mode::RWXU, + ) + .unwrap(); + + let mut dir = Dir::read_from(&fd).unwrap(); + + // Reach inside the `Dir` and replace its directory with a file, which + // will cause the subsequent `readdir` to fail. + unsafe { + let raw_fd = c::dirfd(dir.libc_dir.as_ptr()); + let mut owned_fd: crate::fd::OwnedFd = crate::fd::FromRawFd::from_raw_fd(raw_fd); + crate::io::dup2(&file_fd, &mut owned_fd).unwrap(); + core::mem::forget(owned_fd); + } + + assert!(matches!(dir.next(), Some(Err(_)))); + assert!(matches!(dir.next(), None)); +}
src/backend/linux_raw/fs/dir.rs+82 −13 modified@@ -17,9 +17,17 @@ pub struct Dir { /// The `OwnedFd` that we read directory entries from. fd: OwnedFd, + /// Have we seen any errors in this iteration? + any_errors: bool, + + /// Should we rewind the stream on the next iteration? + rewind: bool, + + /// The buffer for `linux_dirent64` entries. buf: Vec<u8>, + + /// Where we are in the buffer. pos: usize, - next: Option<u64>, } impl Dir { @@ -37,25 +45,39 @@ impl Dir { Ok(Self { fd: fd_for_dir, + any_errors: false, + rewind: false, buf: Vec::new(), pos: 0, - next: None, }) } /// `rewinddir(self)` #[inline] pub fn rewind(&mut self) { + self.any_errors = false; + self.rewind = true; self.pos = self.buf.len(); - self.next = Some(0); } /// `readdir(self)`, where `None` means the end of the directory. pub fn read(&mut self) -> Option<io::Result<DirEntry>> { - if let Some(next) = self.next.take() { - match crate::backend::fs::syscalls::_seek(self.fd.as_fd(), next as i64, SEEK_SET) { + // If we've seen errors, don't continue to try to read anyting further. + if self.any_errors { + return None; + } + + // If a rewind was requested, seek to the beginning. + if self.rewind { + self.rewind = false; + match io::retry_on_intr(|| { + crate::backend::fs::syscalls::_seek(self.fd.as_fd(), 0, SEEK_SET) + }) { Ok(_) => (), - Err(err) => return Some(Err(err)), + Err(err) => { + self.any_errors = true; + return Some(Err(err)); + } } } @@ -77,7 +99,7 @@ impl Dir { if self.buf.len() - self.pos < size_of::<linux_dirent64>() { match self.read_more()? { Ok(()) => (), - Err(e) => return Some(Err(e)), + Err(err) => return Some(Err(err)), } } @@ -135,14 +157,31 @@ impl Dir { } fn read_more(&mut self) -> Option<io::Result<()>> { - let og_len = self.buf.len(); - // Capacity increment currently chosen by wild guess. - self.buf - .resize(self.buf.capacity() + 32 * size_of::<linux_dirent64>(), 0); - let nread = match crate::backend::fs::syscalls::getdents(self.fd.as_fd(), &mut self.buf) { + // The first few times we're called, we allocate a relatively small + // buffer, because many directories are small. If we're called more, + // use progressively larger allocations, up to a fixed maximum. + // + // The specific sizes and policy here have not been tuned in detail yet + // and may need to be adjusted. In doing so, we should be careful to + // avoid unbounded buffer growth. This buffer only exists to share the + // cost of a `getdents` call over many entries, so if it gets too big, + // cache and heap usage will outweigh the benefit. And ultimately, + // directories can contain more entries than we can allocate contiguous + // memory for, so we'll always need to cap the size at some point. + if self.buf.len() < 1024 * size_of::<linux_dirent64>() { + self.buf.reserve(32 * size_of::<linux_dirent64>()); + } + self.buf.resize(self.buf.capacity(), 0); + let nread = match io::retry_on_intr(|| { + crate::backend::fs::syscalls::getdents(self.fd.as_fd(), &mut self.buf) + }) { Ok(nread) => nread, + Err(io::Errno::NOENT) => { + self.any_errors = true; + return None; + } Err(err) => { - self.buf.resize(og_len, 0); + self.any_errors = true; return Some(Err(err)); } }; @@ -222,3 +261,33 @@ impl DirEntry { self.d_ino } } + +#[test] +fn dir_iterator_handles_io_errors() { + // create a dir, keep the FD, then delete the dir + let tmp = tempfile::tempdir().unwrap(); + let fd = crate::fs::openat( + crate::fs::cwd(), + tmp.path(), + crate::fs::OFlags::RDONLY | crate::fs::OFlags::CLOEXEC, + crate::fs::Mode::empty(), + ) + .unwrap(); + + let file_fd = crate::fs::openat( + &fd, + tmp.path().join("test.txt"), + crate::fs::OFlags::WRONLY | crate::fs::OFlags::CREATE, + crate::fs::Mode::RWXU, + ) + .unwrap(); + + let mut dir = Dir::read_from(&fd).unwrap(); + + // Reach inside the `Dir` and replace its directory with a file, which + // will cause the subsequent `getdents64` to fail. + crate::io::dup2(&file_fd, &mut dir.fd).unwrap(); + + assert!(matches!(dir.next(), Some(Err(_)))); + assert!(matches!(dir.next(), None)); +}
tests/fs/dir.rs+68 −1 modified@@ -8,7 +8,7 @@ fn test_dir() { ) .unwrap(); - let dir = rustix::fs::Dir::read_from(&t).unwrap(); + let mut dir = rustix::fs::Dir::read_from(&t).unwrap(); let _file = rustix::fs::openat( &t, @@ -18,6 +18,32 @@ fn test_dir() { ) .unwrap(); + // Read the directory entries. We use `while let Some(entry)` so that we + // don't consume the `Dir` so that we can run more tests on it. + let mut saw_dot = false; + let mut saw_dotdot = false; + let mut saw_cargo_toml = false; + while let Some(entry) = dir.read() { + let entry = entry.unwrap(); + if entry.file_name() == rustix::cstr!(".") { + saw_dot = true; + } else if entry.file_name() == rustix::cstr!("..") { + saw_dotdot = true; + } else if entry.file_name() == rustix::cstr!("Cargo.toml") { + saw_cargo_toml = true; + } + } + assert!(saw_dot); + assert!(saw_dotdot); + assert!(saw_cargo_toml); + + // Rewind the directory so we can iterate over the entries again. + dir.rewind(); + + // For what comes next, we don't need `mut` anymore. + let dir = dir; + + // Read the directory entries, again. This time we use `for entry in dir`. let mut saw_dot = false; let mut saw_dotdot = false; let mut saw_cargo_toml = false; @@ -35,3 +61,44 @@ fn test_dir() { assert!(saw_dotdot); assert!(saw_cargo_toml); } + +#[test] +fn dir_iterator_handles_dir_removal() { + // create a dir, keep the FD, then delete the dir + let tmp = tempfile::tempdir().unwrap(); + let fd = rustix::fs::openat( + rustix::fs::cwd(), + tmp.path(), + rustix::fs::OFlags::RDONLY | rustix::fs::OFlags::CLOEXEC, + rustix::fs::Mode::empty(), + ) + .unwrap(); + + // Drop the `TempDir`, which deletes the directory. + drop(tmp); + + let mut dir = rustix::fs::Dir::read_from(&fd).unwrap(); + assert!(matches!(dir.next(), None)); +} + +// Like `dir_iterator_handles_dir_removal`, but close the directory after +// `Dir::read_from`. +#[test] +fn dir_iterator_handles_dir_removal_after_open() { + // create a dir, keep the FD, then delete the dir + let tmp = tempfile::tempdir().unwrap(); + let fd = rustix::fs::openat( + rustix::fs::cwd(), + tmp.path(), + rustix::fs::OFlags::RDONLY | rustix::fs::OFlags::CLOEXEC, + rustix::fs::Mode::empty(), + ) + .unwrap(); + + let mut dir = rustix::fs::Dir::read_from(&fd).unwrap(); + + // Drop the `TempDir`, which deletes the directory. + drop(tmp); + + assert!(matches!(dir.next(), None)); +}
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
10- github.com/advisories/GHSA-c827-hfw6-qwvmghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-43806ghsaADVISORY
- discord.com/channels/273534239310479360/1161137828395237556ghsaWEB
- github.com/bytecodealliance/rustix/commit/31fd98ca723b93cc6101a3e29843ea5cf094e159ghsaWEB
- github.com/bytecodealliance/rustix/commit/87481a97f4364d12d5d6f30cdd025a0fc509b8ecghsaWEB
- github.com/bytecodealliance/rustix/commit/df3c3a192cf144af0da8a57417fb4addbdc611f6ghsaWEB
- github.com/bytecodealliance/rustix/commit/eecece4a84fc58eafdc809cc2cedd374dee876a5ghsaWEB
- github.com/bytecodealliance/rustix/security/advisories/GHSA-c827-hfw6-qwvmnvdWEB
- github.com/imsnif/bandwhich/issues/284nvdWEB
- github.com/imsnif/bandwhich/issues/284ghsaWEB
News mentions
0No linked articles in our index yet.