Skip to content

Commit eecece4

Browse files
sunfishcodecyqsimon
andcommitted
Merge pull request from GHSA-c827-hfw6-qwvm
* Fix `rustix::fs::Dir` to avoid unbounded buffer growth. Fix `Dir`'s buffer size computation to avoid resizing past a fixed upper limit. This prevents it from growing without bound, such as in the case of `Dir::rewind` for repeated iterations with the same `Dir`. * Don't let `Dir` continue to try to iterate after a failure. * Handle `io::Errno::INTR` gracefully. * Write a more detailed comment on the buffer growth policy. * Also mention that no buffer can ever be big enough for everything. * Add tests against over-allocation & stuck iterator * Rm `dir_iterator_does_not_overallocate` unit test in favour of docs * Extend `test_dir` to cover `rewind`. * Consistently handle directory removal as ending the stream. libc implementations of directory iteration handle directory removal by just ending the stream. In the linux_raw backend, this looks like `ENOENT` from `getdents64`, so change the code to check for `ENOENT` and end the stream. This requires changing the `dir_iterator_does_not_get_stuck_on_io_error` test to no longer expect a failure, so it's now renamed to `dir_iterator_handles_dir_removal`. To test the error case, add a new `dir_iterator_handles_io_errors` test which uses `dup2` to induce an error, in both the linux_raw and libc backends. This exposes the fact that the libc `Dir` implementation was also assuming that users would stop iterating after hitting a failure, so add a `any_errors` flag to the libc backend as well. * Add a test for removing the directory after doing `read_from`. * In the libc backend, handle `ENOENT` when opening ".". --------- Co-authored-by: cyqsimon <[email protected]>
1 parent 7f6a13e commit eecece4

File tree

3 files changed

+225
-26
lines changed

3 files changed

+225
-26
lines changed

src/backend/libc/fs/dir.rs

Lines changed: 75 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,13 @@ use core::ptr::NonNull;
5757
use libc_errno::{errno, set_errno, Errno};
5858

5959
/// `DIR*`
60-
#[repr(transparent)]
61-
pub struct Dir(NonNull<c::DIR>);
60+
pub struct Dir {
61+
/// The `libc` `DIR` pointer.
62+
libc_dir: NonNull<c::DIR>,
63+
64+
/// Have we seen any errors in this iteration?
65+
any_errors: bool,
66+
}
6267

6368
impl Dir {
6469
/// Construct a `Dir` that reads entries from the given directory
@@ -70,20 +75,35 @@ impl Dir {
7075

7176
#[inline]
7277
fn _read_from(fd: BorrowedFd<'_>) -> io::Result<Self> {
78+
let mut any_errors = false;
79+
7380
// Given an arbitrary `OwnedFd`, it's impossible to know whether the
7481
// user holds a `dup`'d copy which could continue to modify the
7582
// file description state, which would cause Undefined Behavior after
7683
// our call to `fdopendir`. To prevent this, we obtain an independent
7784
// `OwnedFd`.
7885
let flags = fcntl_getfl(&fd)?;
79-
let fd_for_dir = openat(&fd, cstr!("."), flags | OFlags::CLOEXEC, Mode::empty())?;
86+
let fd_for_dir = match openat(&fd, cstr!("."), flags | OFlags::CLOEXEC, Mode::empty()) {
87+
Ok(fd) => fd,
88+
Err(io::Errno::NOENT) => {
89+
// If "." doesn't exist, it means the directory was removed.
90+
// We treat that as iterating through a directory with no
91+
// entries.
92+
any_errors = true;
93+
crate::io::dup(fd)?
94+
}
95+
Err(err) => return Err(err),
96+
};
8097

8198
let raw = owned_fd(fd_for_dir);
8299
unsafe {
83100
let libc_dir = c::fdopendir(raw);
84101

85102
if let Some(libc_dir) = NonNull::new(libc_dir) {
86-
Ok(Self(libc_dir))
103+
Ok(Self {
104+
libc_dir,
105+
any_errors,
106+
})
87107
} else {
88108
let err = io::Errno::last_os_error();
89109
let _ = c::close(raw);
@@ -95,20 +115,27 @@ impl Dir {
95115
/// `rewinddir(self)`
96116
#[inline]
97117
pub fn rewind(&mut self) {
98-
unsafe { c::rewinddir(self.0.as_ptr()) }
118+
self.any_errors = false;
119+
unsafe { c::rewinddir(self.libc_dir.as_ptr()) }
99120
}
100121

101122
/// `readdir(self)`, where `None` means the end of the directory.
102123
pub fn read(&mut self) -> Option<io::Result<DirEntry>> {
124+
// If we've seen errors, don't continue to try to read anyting further.
125+
if self.any_errors {
126+
return None;
127+
}
128+
103129
set_errno(Errno(0));
104-
let dirent_ptr = unsafe { libc_readdir(self.0.as_ptr()) };
130+
let dirent_ptr = unsafe { libc_readdir(self.libc_dir.as_ptr()) };
105131
if dirent_ptr.is_null() {
106132
let curr_errno = errno().0;
107133
if curr_errno == 0 {
108134
// We successfully reached the end of the stream.
109135
None
110136
} else {
111137
// `errno` is unknown or non-zero, so an error occurred.
138+
self.any_errors = true;
112139
Some(Err(io::Errno(curr_errno)))
113140
}
114141
} else {
@@ -134,7 +161,7 @@ impl Dir {
134161
/// `fstat(self)`
135162
#[inline]
136163
pub fn stat(&self) -> io::Result<Stat> {
137-
fstat(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.0.as_ptr())) })
164+
fstat(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.libc_dir.as_ptr())) })
138165
}
139166

140167
/// `fstatfs(self)`
@@ -148,7 +175,7 @@ impl Dir {
148175
)))]
149176
#[inline]
150177
pub fn statfs(&self) -> io::Result<StatFs> {
151-
fstatfs(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.0.as_ptr())) })
178+
fstatfs(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.libc_dir.as_ptr())) })
152179
}
153180

154181
/// `fstatvfs(self)`
@@ -161,14 +188,14 @@ impl Dir {
161188
)))]
162189
#[inline]
163190
pub fn statvfs(&self) -> io::Result<StatVfs> {
164-
fstatvfs(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.0.as_ptr())) })
191+
fstatvfs(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.libc_dir.as_ptr())) })
165192
}
166193

167194
/// `fchdir(self)`
168195
#[cfg(not(any(target_os = "fuchsia", target_os = "wasi")))]
169196
#[inline]
170197
pub fn chdir(&self) -> io::Result<()> {
171-
fchdir(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.0.as_ptr())) })
198+
fchdir(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.libc_dir.as_ptr())) })
172199
}
173200
}
174201

@@ -338,7 +365,7 @@ unsafe impl Send for Dir {}
338365
impl Drop for Dir {
339366
#[inline]
340367
fn drop(&mut self) {
341-
unsafe { c::closedir(self.0.as_ptr()) };
368+
unsafe { c::closedir(self.libc_dir.as_ptr()) };
342369
}
343370
}
344371

@@ -354,7 +381,7 @@ impl Iterator for Dir {
354381
impl fmt::Debug for Dir {
355382
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
356383
f.debug_struct("Dir")
357-
.field("fd", unsafe { &c::dirfd(self.0.as_ptr()) })
384+
.field("fd", unsafe { &c::dirfd(self.libc_dir.as_ptr()) })
358385
.finish()
359386
}
360387
}
@@ -481,3 +508,39 @@ fn check_dirent_layout(dirent: &c::dirent) {
481508
}
482509
);
483510
}
511+
512+
#[test]
513+
fn dir_iterator_handles_io_errors() {
514+
// create a dir, keep the FD, then delete the dir
515+
let tmp = tempfile::tempdir().unwrap();
516+
let fd = crate::fs::openat(
517+
crate::fs::cwd(),
518+
tmp.path(),
519+
crate::fs::OFlags::RDONLY | crate::fs::OFlags::CLOEXEC,
520+
crate::fs::Mode::empty(),
521+
)
522+
.unwrap();
523+
524+
let file_fd = crate::fs::openat(
525+
&fd,
526+
tmp.path().join("test.txt"),
527+
crate::fs::OFlags::WRONLY | crate::fs::OFlags::CREATE,
528+
crate::fs::Mode::RWXU,
529+
)
530+
.unwrap();
531+
532+
let mut dir = Dir::read_from(&fd).unwrap();
533+
534+
// Reach inside the `Dir` and replace its directory with a file, which
535+
// will cause the subsequent `readdir` to fail.
536+
unsafe {
537+
let raw_fd = c::dirfd(dir.libc_dir.as_ptr());
538+
let owned_fd: crate::fd::OwnedFd = crate::fd::FromRawFd::from_raw_fd(raw_fd);
539+
let mut owned_fd: crate::io::OwnedFd = owned_fd.into();
540+
crate::io::dup2(&file_fd, &mut owned_fd).unwrap();
541+
core::mem::forget(owned_fd);
542+
}
543+
544+
assert!(matches!(dir.next(), Some(Err(_))));
545+
assert!(matches!(dir.next(), None));
546+
}

src/backend/linux_raw/fs/dir.rs

Lines changed: 82 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,17 @@ pub struct Dir {
1717
/// The `OwnedFd` that we read directory entries from.
1818
fd: OwnedFd,
1919

20+
/// Have we seen any errors in this iteration?
21+
any_errors: bool,
22+
23+
/// Should we rewind the stream on the next iteration?
24+
rewind: bool,
25+
26+
/// The buffer for `linux_dirent64` entries.
2027
buf: Vec<u8>,
28+
29+
/// Where we are in the buffer.
2130
pos: usize,
22-
next: Option<u64>,
2331
}
2432

2533
impl Dir {
@@ -37,25 +45,39 @@ impl Dir {
3745

3846
Ok(Self {
3947
fd: fd_for_dir,
48+
any_errors: false,
49+
rewind: false,
4050
buf: Vec::new(),
4151
pos: 0,
42-
next: None,
4352
})
4453
}
4554

4655
/// `rewinddir(self)`
4756
#[inline]
4857
pub fn rewind(&mut self) {
58+
self.any_errors = false;
59+
self.rewind = true;
4960
self.pos = self.buf.len();
50-
self.next = Some(0);
5161
}
5262

5363
/// `readdir(self)`, where `None` means the end of the directory.
5464
pub fn read(&mut self) -> Option<io::Result<DirEntry>> {
55-
if let Some(next) = self.next.take() {
56-
match crate::backend::fs::syscalls::_seek(self.fd.as_fd(), next as i64, SEEK_SET) {
65+
// If we've seen errors, don't continue to try to read anyting further.
66+
if self.any_errors {
67+
return None;
68+
}
69+
70+
// If a rewind was requested, seek to the beginning.
71+
if self.rewind {
72+
self.rewind = false;
73+
match io::retry_on_intr(|| {
74+
crate::backend::fs::syscalls::_seek(self.fd.as_fd(), 0, SEEK_SET)
75+
}) {
5776
Ok(_) => (),
58-
Err(err) => return Some(Err(err)),
77+
Err(err) => {
78+
self.any_errors = true;
79+
return Some(Err(err));
80+
}
5981
}
6082
}
6183

@@ -77,7 +99,7 @@ impl Dir {
7799
if self.buf.len() - self.pos < size_of::<linux_dirent64>() {
78100
match self.read_more()? {
79101
Ok(()) => (),
80-
Err(e) => return Some(Err(e)),
102+
Err(err) => return Some(Err(err)),
81103
}
82104
}
83105

@@ -136,14 +158,31 @@ impl Dir {
136158
}
137159

138160
fn read_more(&mut self) -> Option<io::Result<()>> {
139-
let og_len = self.buf.len();
140-
// Capacity increment currently chosen by wild guess.
141-
self.buf
142-
.resize(self.buf.capacity() + 32 * size_of::<linux_dirent64>(), 0);
143-
let nread = match crate::backend::fs::syscalls::getdents(self.fd.as_fd(), &mut self.buf) {
161+
// The first few times we're called, we allocate a relatively small
162+
// buffer, because many directories are small. If we're called more,
163+
// use progressively larger allocations, up to a fixed maximum.
164+
//
165+
// The specific sizes and policy here have not been tuned in detail yet
166+
// and may need to be adjusted. In doing so, we should be careful to
167+
// avoid unbounded buffer growth. This buffer only exists to share the
168+
// cost of a `getdents` call over many entries, so if it gets too big,
169+
// cache and heap usage will outweigh the benefit. And ultimately,
170+
// directories can contain more entries than we can allocate contiguous
171+
// memory for, so we'll always need to cap the size at some point.
172+
if self.buf.len() < 1024 * size_of::<linux_dirent64>() {
173+
self.buf.reserve(32 * size_of::<linux_dirent64>());
174+
}
175+
self.buf.resize(self.buf.capacity(), 0);
176+
let nread = match io::retry_on_intr(|| {
177+
crate::backend::fs::syscalls::getdents(self.fd.as_fd(), &mut self.buf)
178+
}) {
144179
Ok(nread) => nread,
180+
Err(io::Errno::NOENT) => {
181+
self.any_errors = true;
182+
return None;
183+
}
145184
Err(err) => {
146-
self.buf.resize(og_len, 0);
185+
self.any_errors = true;
147186
return Some(Err(err));
148187
}
149188
};
@@ -223,3 +262,33 @@ impl DirEntry {
223262
self.d_ino
224263
}
225264
}
265+
266+
#[test]
267+
fn dir_iterator_handles_io_errors() {
268+
// create a dir, keep the FD, then delete the dir
269+
let tmp = tempfile::tempdir().unwrap();
270+
let fd = crate::fs::openat(
271+
crate::fs::cwd(),
272+
tmp.path(),
273+
crate::fs::OFlags::RDONLY | crate::fs::OFlags::CLOEXEC,
274+
crate::fs::Mode::empty(),
275+
)
276+
.unwrap();
277+
278+
let file_fd = crate::fs::openat(
279+
&fd,
280+
tmp.path().join("test.txt"),
281+
crate::fs::OFlags::WRONLY | crate::fs::OFlags::CREATE,
282+
crate::fs::Mode::RWXU,
283+
)
284+
.unwrap();
285+
286+
let mut dir = Dir::read_from(&fd).unwrap();
287+
288+
// Reach inside the `Dir` and replace its directory with a file, which
289+
// will cause the subsequent `getdents64` to fail.
290+
crate::io::dup2(&file_fd, &mut dir.fd).unwrap();
291+
292+
assert!(matches!(dir.next(), Some(Err(_))));
293+
assert!(matches!(dir.next(), None));
294+
}

0 commit comments

Comments
 (0)