Skip to content

Commit df3c3a1

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 b78aeff commit df3c3a1

File tree

3 files changed

+224
-26
lines changed

3 files changed

+224
-26
lines changed

src/backend/libc/fs/dir.rs

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,13 @@ use core::ptr::NonNull;
2929
use libc_errno::{errno, set_errno, Errno};
3030

3131
/// `DIR*`
32-
#[repr(transparent)]
33-
pub struct Dir(NonNull<c::DIR>);
32+
pub struct Dir {
33+
/// The `libc` `DIR` pointer.
34+
libc_dir: NonNull<c::DIR>,
35+
36+
/// Have we seen any errors in this iteration?
37+
any_errors: bool,
38+
}
3439

3540
impl Dir {
3641
/// Construct a `Dir` that reads entries from the given directory
@@ -42,20 +47,35 @@ impl Dir {
4247

4348
#[inline]
4449
fn _read_from(fd: BorrowedFd<'_>) -> io::Result<Self> {
50+
let mut any_errors = false;
51+
4552
// Given an arbitrary `OwnedFd`, it's impossible to know whether the
4653
// user holds a `dup`'d copy which could continue to modify the
4754
// file description state, which would cause Undefined Behavior after
4855
// our call to `fdopendir`. To prevent this, we obtain an independent
4956
// `OwnedFd`.
5057
let flags = fcntl_getfl(fd)?;
51-
let fd_for_dir = openat(fd, cstr!("."), flags | OFlags::CLOEXEC, Mode::empty())?;
58+
let fd_for_dir = match openat(fd, cstr!("."), flags | OFlags::CLOEXEC, Mode::empty()) {
59+
Ok(fd) => fd,
60+
Err(io::Errno::NOENT) => {
61+
// If "." doesn't exist, it means the directory was removed.
62+
// We treat that as iterating through a directory with no
63+
// entries.
64+
any_errors = true;
65+
crate::io::dup(fd)?
66+
}
67+
Err(err) => return Err(err),
68+
};
5269

5370
let raw = owned_fd(fd_for_dir);
5471
unsafe {
5572
let libc_dir = c::fdopendir(raw);
5673

5774
if let Some(libc_dir) = NonNull::new(libc_dir) {
58-
Ok(Self(libc_dir))
75+
Ok(Self {
76+
libc_dir,
77+
any_errors,
78+
})
5979
} else {
6080
let err = io::Errno::last_os_error();
6181
let _ = c::close(raw);
@@ -67,20 +87,27 @@ impl Dir {
6787
/// `rewinddir(self)`
6888
#[inline]
6989
pub fn rewind(&mut self) {
70-
unsafe { c::rewinddir(self.0.as_ptr()) }
90+
self.any_errors = false;
91+
unsafe { c::rewinddir(self.libc_dir.as_ptr()) }
7192
}
7293

7394
/// `readdir(self)`, where `None` means the end of the directory.
7495
pub fn read(&mut self) -> Option<io::Result<DirEntry>> {
96+
// If we've seen errors, don't continue to try to read anyting further.
97+
if self.any_errors {
98+
return None;
99+
}
100+
75101
set_errno(Errno(0));
76-
let dirent_ptr = unsafe { libc_readdir(self.0.as_ptr()) };
102+
let dirent_ptr = unsafe { libc_readdir(self.libc_dir.as_ptr()) };
77103
if dirent_ptr.is_null() {
78104
let curr_errno = errno().0;
79105
if curr_errno == 0 {
80106
// We successfully reached the end of the stream.
81107
None
82108
} else {
83109
// `errno` is unknown or non-zero, so an error occurred.
110+
self.any_errors = true;
84111
Some(Err(io::Errno(curr_errno)))
85112
}
86113
} else {
@@ -114,7 +141,7 @@ impl Dir {
114141
/// `fstat(self)`
115142
#[inline]
116143
pub fn stat(&self) -> io::Result<Stat> {
117-
fstat(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.0.as_ptr())) })
144+
fstat(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.libc_dir.as_ptr())) })
118145
}
119146

120147
/// `fstatfs(self)`
@@ -127,21 +154,21 @@ impl Dir {
127154
)))]
128155
#[inline]
129156
pub fn statfs(&self) -> io::Result<StatFs> {
130-
fstatfs(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.0.as_ptr())) })
157+
fstatfs(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.libc_dir.as_ptr())) })
131158
}
132159

133160
/// `fstatvfs(self)`
134161
#[cfg(not(any(solarish, target_os = "haiku", target_os = "redox", target_os = "wasi")))]
135162
#[inline]
136163
pub fn statvfs(&self) -> io::Result<StatVfs> {
137-
fstatvfs(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.0.as_ptr())) })
164+
fstatvfs(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.libc_dir.as_ptr())) })
138165
}
139166

140167
/// `fchdir(self)`
141168
#[cfg(not(any(target_os = "fuchsia", target_os = "wasi")))]
142169
#[inline]
143170
pub fn chdir(&self) -> io::Result<()> {
144-
fchdir(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.0.as_ptr())) })
171+
fchdir(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.libc_dir.as_ptr())) })
145172
}
146173
}
147174

@@ -154,7 +181,7 @@ unsafe impl Send for Dir {}
154181
impl Drop for Dir {
155182
#[inline]
156183
fn drop(&mut self) {
157-
unsafe { c::closedir(self.0.as_ptr()) };
184+
unsafe { c::closedir(self.libc_dir.as_ptr()) };
158185
}
159186
}
160187

@@ -170,7 +197,7 @@ impl Iterator for Dir {
170197
impl fmt::Debug for Dir {
171198
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172199
f.debug_struct("Dir")
173-
.field("fd", unsafe { &c::dirfd(self.0.as_ptr()) })
200+
.field("fd", unsafe { &c::dirfd(self.libc_dir.as_ptr()) })
174201
.finish()
175202
}
176203
}
@@ -282,3 +309,38 @@ fn check_dirent_layout(dirent: &c::dirent) {
282309
}
283310
);
284311
}
312+
313+
#[test]
314+
fn dir_iterator_handles_io_errors() {
315+
// create a dir, keep the FD, then delete the dir
316+
let tmp = tempfile::tempdir().unwrap();
317+
let fd = crate::fs::openat(
318+
crate::fs::cwd(),
319+
tmp.path(),
320+
crate::fs::OFlags::RDONLY | crate::fs::OFlags::CLOEXEC,
321+
crate::fs::Mode::empty(),
322+
)
323+
.unwrap();
324+
325+
let file_fd = crate::fs::openat(
326+
&fd,
327+
tmp.path().join("test.txt"),
328+
crate::fs::OFlags::WRONLY | crate::fs::OFlags::CREATE,
329+
crate::fs::Mode::RWXU,
330+
)
331+
.unwrap();
332+
333+
let mut dir = Dir::read_from(&fd).unwrap();
334+
335+
// Reach inside the `Dir` and replace its directory with a file, which
336+
// will cause the subsequent `readdir` to fail.
337+
unsafe {
338+
let raw_fd = c::dirfd(dir.libc_dir.as_ptr());
339+
let mut owned_fd: crate::fd::OwnedFd = crate::fd::FromRawFd::from_raw_fd(raw_fd);
340+
crate::io::dup2(&file_fd, &mut owned_fd).unwrap();
341+
core::mem::forget(owned_fd);
342+
}
343+
344+
assert!(matches!(dir.next(), Some(Err(_))));
345+
assert!(matches!(dir.next(), None));
346+
}

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

@@ -135,14 +157,31 @@ impl Dir {
135157
}
136158

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

0 commit comments

Comments
 (0)