From 3d401897d76b59c19faf0ef0544d0c4edecd8472 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kr=C3=B6ning?= Date: Tue, 30 Jun 2026 18:02:35 +0200 Subject: [PATCH 1/6] Hermit: Don't cast `i32` to `i32` --- library/std/src/sys/fs/hermit.rs | 4 ++-- library/std/src/sys/net/connection/socket/hermit.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/library/std/src/sys/fs/hermit.rs b/library/std/src/sys/fs/hermit.rs index 5f69560998293..9fdfb962ceda8 100644 --- a/library/std/src/sys/fs/hermit.rs +++ b/library/std/src/sys/fs/hermit.rs @@ -342,7 +342,7 @@ impl File { } let fd = unsafe { cvt(hermit_abi::open(path.as_ptr(), flags, mode))? }; - Ok(File(unsafe { FileDesc::from_raw_fd(fd as i32) })) + Ok(File(unsafe { FileDesc::from_raw_fd(fd) })) } pub fn file_attr(&self) -> io::Result { @@ -516,7 +516,7 @@ pub fn readdir(path: &Path) -> io::Result { let fd_raw = run_path_with_cstr(path, &|path| { cvt(unsafe { hermit_abi::open(path.as_ptr(), O_RDONLY | O_DIRECTORY, 0) }) })?; - let fd = unsafe { FileDesc::from_raw_fd(fd_raw as i32) }; + let fd = unsafe { FileDesc::from_raw_fd(fd_raw) }; let root = path.to_path_buf(); // read all director entries diff --git a/library/std/src/sys/net/connection/socket/hermit.rs b/library/std/src/sys/net/connection/socket/hermit.rs index ba40da4035b6f..f43395ce8fd80 100644 --- a/library/std/src/sys/net/connection/socket/hermit.rs +++ b/library/std/src/sys/net/connection/socket/hermit.rs @@ -304,8 +304,8 @@ impl Socket { } pub fn take_error(&self) -> io::Result> { - let raw: c_int = unsafe { getsockopt(self, libc::SOL_SOCKET, libc::SO_ERROR)? }; - if raw == 0 { Ok(None) } else { Ok(Some(io::Error::from_raw_os_error(raw as i32))) } + let raw = unsafe { getsockopt(self, libc::SOL_SOCKET, libc::SO_ERROR)? }; + if raw == 0 { Ok(None) } else { Ok(Some(io::Error::from_raw_os_error(raw))) } } pub fn as_raw(&self) -> RawFd { From 87900d9adefcc65eb93c958481c996fd2545b36f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kr=C3=B6ning?= Date: Tue, 30 Jun 2026 17:32:52 +0200 Subject: [PATCH 2/6] Hermit: Inline `InnerReadDir::new` --- library/std/src/sys/fs/hermit.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/library/std/src/sys/fs/hermit.rs b/library/std/src/sys/fs/hermit.rs index 9fdfb962ceda8..947d3c0b27762 100644 --- a/library/std/src/sys/fs/hermit.rs +++ b/library/std/src/sys/fs/hermit.rs @@ -36,12 +36,6 @@ struct InnerReadDir { dir: Vec, } -impl InnerReadDir { - pub fn new(root: PathBuf, dir: Vec) -> Self { - Self { root, dir } - } -} - pub struct ReadDir { inner: Arc, pos: usize, @@ -551,7 +545,7 @@ pub fn readdir(path: &Path) -> io::Result { } } - Ok(ReadDir::new(InnerReadDir::new(root, vec))) + Ok(ReadDir::new(InnerReadDir { root, dir: vec })) } pub fn unlink(path: &Path) -> io::Result<()> { From 149dc77a1c5d5aab0d368788b0281fc9e22b3555 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kr=C3=B6ning?= Date: Tue, 30 Jun 2026 17:39:05 +0200 Subject: [PATCH 3/6] Hermit: Avoid unsoundness around dirent64 This is also how it is done on other platforms. --- library/std/src/sys/fs/hermit.rs | 44 +++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/library/std/src/sys/fs/hermit.rs b/library/std/src/sys/fs/hermit.rs index 947d3c0b27762..244e85e32f65d 100644 --- a/library/std/src/sys/fs/hermit.rs +++ b/library/std/src/sys/fs/hermit.rs @@ -1,4 +1,4 @@ -use crate::ffi::{CStr, OsStr, OsString, c_char}; +use crate::ffi::{CStr, OsStr, OsString}; use crate::fs::TryLockError; use crate::io::{self, BorrowedCursor, Error, ErrorKind, IoSlice, IoSliceMut, SeekFrom}; use crate::os::hermit::ffi::OsStringExt; @@ -189,31 +189,45 @@ impl Iterator for ReadDir { return None; } - let dir = unsafe { &*(self.inner.dir.as_ptr().add(offset) as *const dirent64) }; + let entry_ptr = unsafe { self.inner.dir.as_ptr().add(offset).cast::() }; + + // The dirent64 struct is a weird imaginary thing that isn't ever supposed + // to be worked with by value. Its trailing d_name field is declared + // variously as [c_char; 256] or [c_char; 1] on different systems but + // either way that size is meaningless; only the offset of d_name is + // meaningful. The dirent64 pointers that libc returns from getdents64 are + // allowed to point to allocations smaller _or_ LARGER than implied by the + // definition of the struct. + // + // As such, we need to be even more careful with dirent64 than if its + // contents were "simply" partially initialized data. + // + // Like for uninitialized contents, converting entry_ptr to `&dirent64` + // would not be legal. However, we can use `&raw const (*entry_ptr).d_name` + // to refer the fields individually, because that operation is equivalent + // to `byte_offset` and thus does not require the full extent of `*entry_ptr` + // to be in bounds of the same allocation, only the offset of the field + // being referenced. if counter == self.pos { self.pos += 1; - // After dirent64, the file name is stored. d_reclen represents the length of the dirent64 - // plus the length of the file name. Consequently, file name has a size of d_reclen minus - // the size of dirent64. The file name is always a C string and terminated by `\0`. - // Consequently, we are able to ignore the last byte. - let name_bytes = - unsafe { CStr::from_ptr(&dir.d_name as *const _ as *const c_char).to_bytes() }; - let entry = DirEntry { + // d_name is guaranteed to be null-terminated. + let name = unsafe { CStr::from_ptr((&raw const (*entry_ptr).d_name).cast()) }; + let name_bytes = name.to_bytes(); + + return Some(Ok(DirEntry { root: self.inner.root.clone(), - ino: dir.d_ino, - type_: dir.d_type, + ino: unsafe { (*entry_ptr).d_ino }, + type_: unsafe { (*entry_ptr).d_type }, name: OsString::from_vec(name_bytes.to_vec()), - }; - - return Some(Ok(entry)); + })); } counter += 1; // move to the next dirent64, which is directly stored after the previous one - offset = offset + usize::from(dir.d_reclen); + offset = offset + unsafe { usize::from((*entry_ptr).d_reclen) }; } } } From 736fa96466592abe7227173fdfb0b967282947e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kr=C3=B6ning?= Date: Tue, 30 Jun 2026 15:41:59 +0200 Subject: [PATCH 4/6] Hermit: Avoid cloning `InnerReadDir::root` This optimization was already partially in place, but not used. Other platforms already do this. --- library/std/src/sys/fs/hermit.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/library/std/src/sys/fs/hermit.rs b/library/std/src/sys/fs/hermit.rs index 244e85e32f65d..3bf9fb1fe2a77 100644 --- a/library/std/src/sys/fs/hermit.rs +++ b/library/std/src/sys/fs/hermit.rs @@ -48,8 +48,7 @@ impl ReadDir { } pub struct DirEntry { - /// path to the entry - root: PathBuf, + dir: Arc, /// 64-bit inode number ino: u64, /// File type @@ -217,7 +216,7 @@ impl Iterator for ReadDir { let name_bytes = name.to_bytes(); return Some(Ok(DirEntry { - root: self.inner.root.clone(), + dir: Arc::clone(&self.inner), ino: unsafe { (*entry_ptr).d_ino }, type_: unsafe { (*entry_ptr).d_type }, name: OsString::from_vec(name_bytes.to_vec()), @@ -234,7 +233,7 @@ impl Iterator for ReadDir { impl DirEntry { pub fn path(&self) -> PathBuf { - self.root.join(self.file_name_os_str()) + self.dir.root.join(self.file_name_os_str()) } pub fn file_name(&self) -> OsString { From bf6c7188cfd897e3052285a04bea66ec3baf876a Mon Sep 17 00:00:00 2001 From: Jonathan Klimt Date: Fri, 26 Jun 2026 11:22:47 +0200 Subject: [PATCH 5/6] Hermit: Rework `getdents64` implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, we read all entries into a huge initialized buffer up front, which does not work correctly since on Hermit 0.12, getdents64 behaves more reasonable and does no longer fail on buffers which cannot hold all entries. Additionally, for each new entry, we started searching for the current position from the start instead of just saving the position directly. The new design uses a fixed-size uninitialized buffer that is read into as necessary. Co-authored-by: Martin Kröning --- library/std/src/sys/fs/hermit.rs | 140 +++++++++++++++++-------------- 1 file changed, 78 insertions(+), 62 deletions(-) diff --git a/library/std/src/sys/fs/hermit.rs b/library/std/src/sys/fs/hermit.rs index 3bf9fb1fe2a77..41c3f048e6b9a 100644 --- a/library/std/src/sys/fs/hermit.rs +++ b/library/std/src/sys/fs/hermit.rs @@ -1,6 +1,7 @@ use crate::ffi::{CStr, OsStr, OsString}; use crate::fs::TryLockError; use crate::io::{self, BorrowedCursor, Error, ErrorKind, IoSlice, IoSliceMut, SeekFrom}; +use crate::mem::MaybeUninit; use crate::os::hermit::ffi::OsStringExt; use crate::os::hermit::hermit_abi::{ self, DT_DIR, DT_LNK, DT_REG, DT_UNKNOWN, O_APPEND, O_CREAT, O_DIRECTORY, O_EXCL, O_RDONLY, @@ -12,9 +13,10 @@ use crate::sync::Arc; use crate::sys::fd::FileDesc; pub use crate::sys::fs::common::{Dir, copy, exists}; use crate::sys::helpers::run_path_with_cstr; +use crate::sys::io::DEFAULT_BUF_SIZE; use crate::sys::time::SystemTime; use crate::sys::{AsInner, AsInnerMut, FromInner, IntoInner, cvt, unsupported, unsupported_err}; -use crate::{fmt, mem}; +use crate::{cmp, fmt, mem}; #[derive(Debug)] pub struct File(FileDesc); @@ -33,17 +35,66 @@ impl FileAttr { // all DirEntry's will have a reference to this struct struct InnerReadDir { root: PathBuf, - dir: Vec, } pub struct ReadDir { inner: Arc, + fd: FileDesc, + buf: GetdentsBuffer, +} + +/// A buffer containing [`dirent64`]s, filled with [`getdents64`]. +/// +/// This struct is roughly modeled after the `BufReader`'s `Buffer`. +struct GetdentsBuffer { + // The buffer. + buf: Box<[MaybeUninit]>, + // The current seek offset into `buf`, must always be <= `filled`. pos: usize, + // Each call to `fill_buf` sets `filled` to indicate how many bytes at the start of `buf` are + // initialized with bytes from a read. + filled: usize, } -impl ReadDir { - fn new(inner: InnerReadDir) -> Self { - Self { inner: Arc::new(inner), pos: 0 } +impl GetdentsBuffer { + fn with_capacity(capacity: usize) -> Self { + let buf = Box::new_uninit_slice(capacity); + Self { buf, pos: 0, filled: 0 } + } + + fn buffer(&self) -> &[u8] { + // SAFETY: self.pos and self.filled are valid, and self.filled >= self.pos, and + // that region is initialized because those are all invariants of this type. + unsafe { self.buf.get_unchecked(self.pos..self.filled).assume_init_ref() } + } + + fn consume(&mut self, amt: usize) { + self.pos = cmp::min(self.pos + amt, self.filled); + } + + fn fill_buf(&mut self, fd: BorrowedFd<'_>) -> io::Result<&[u8]> { + // If we've reached the end of our internal buffer then we need to fetch + // some more data from the reader. + // Branch using `>=` instead of the more correct `==` + // to tell the compiler that the pos..cap slice is always valid. + if self.pos >= self.filled { + debug_assert!(self.pos == self.filled); + + let result = unsafe { + cvt(hermit_abi::getdents64( + fd.as_raw_fd(), + self.buf.as_mut_ptr().cast::(), + self.buf.len(), + )) + }; + + self.pos = 0; + self.filled = 0; + + self.filled = result? as usize; + } + + Ok(self.buffer()) } } @@ -178,17 +229,18 @@ impl Iterator for ReadDir { type Item = io::Result; fn next(&mut self) -> Option> { - let mut counter: usize = 0; - let mut offset: usize = 0; - - // loop over all directory entries and search the entry for the current position loop { - // leave function, if the loop reaches the of the buffer (with all entries) - if offset >= self.inner.dir.len() { + let buf = match self.buf.fill_buf(self.fd.as_fd()) { + Ok(buf) => buf, + Err(err) => return Some(Err(err)), + }; + + if buf.len() == 0 { + // No more entries left. return None; } - let entry_ptr = unsafe { self.inner.dir.as_ptr().add(offset).cast::() }; + let entry_ptr = buf.as_ptr().cast::(); // The dirent64 struct is a weird imaginary thing that isn't ever supposed // to be worked with by value. Its trailing d_name field is declared @@ -208,25 +260,18 @@ impl Iterator for ReadDir { // to be in bounds of the same allocation, only the offset of the field // being referenced. - if counter == self.pos { - self.pos += 1; + self.buf.consume(usize::from(unsafe { (*entry_ptr).d_reclen })); - // d_name is guaranteed to be null-terminated. - let name = unsafe { CStr::from_ptr((&raw const (*entry_ptr).d_name).cast()) }; - let name_bytes = name.to_bytes(); + // d_name is guaranteed to be null-terminated. + let name = unsafe { CStr::from_ptr((&raw const (*entry_ptr).d_name).cast()) }; + let name_bytes = name.to_bytes(); - return Some(Ok(DirEntry { - dir: Arc::clone(&self.inner), - ino: unsafe { (*entry_ptr).d_ino }, - type_: unsafe { (*entry_ptr).d_type }, - name: OsString::from_vec(name_bytes.to_vec()), - })); - } - - counter += 1; - - // move to the next dirent64, which is directly stored after the previous one - offset = offset + unsafe { usize::from((*entry_ptr).d_reclen) }; + return Some(Ok(DirEntry { + dir: Arc::clone(&self.inner), + ino: unsafe { (*entry_ptr).d_ino }, + type_: unsafe { (*entry_ptr).d_type }, + name: OsString::from_vec(name_bytes.to_vec()), + })); } } } @@ -524,41 +569,12 @@ pub fn readdir(path: &Path) -> io::Result { cvt(unsafe { hermit_abi::open(path.as_ptr(), O_RDONLY | O_DIRECTORY, 0) }) })?; let fd = unsafe { FileDesc::from_raw_fd(fd_raw) }; - let root = path.to_path_buf(); - - // read all director entries - let mut vec: Vec = Vec::new(); - let mut sz = 512; - loop { - // reserve memory to receive all directory entries - vec.resize(sz, 0); - - let readlen = unsafe { - hermit_abi::getdents64(fd.as_raw_fd(), vec.as_mut_ptr() as *mut dirent64, sz) - }; - if readlen > 0 { - // shrink down to the minimal size - vec.resize(readlen.try_into().unwrap(), 0); - break; - } - - // if the buffer is too small, getdents64 returns EINVAL - // otherwise, getdents64 returns an error number - if readlen != (-hermit_abi::errno::EINVAL).into() { - return Err(Error::from_raw_os_error(readlen.try_into().unwrap())); - } - - // we don't have enough memory => try to increase the vector size - sz = sz * 2; - // 1 MB for directory entries should be enough - // stop here to avoid an endless loop - if sz > 0x100000 { - return Err(Error::from(ErrorKind::Uncategorized)); - } - } + let root = path.to_path_buf(); + let inner = Arc::new(InnerReadDir { root }); + let buf = GetdentsBuffer::with_capacity(DEFAULT_BUF_SIZE); - Ok(ReadDir::new(InnerReadDir { root, dir: vec })) + Ok(ReadDir { inner, fd, buf }) } pub fn unlink(path: &Path) -> io::Result<()> { From a2a9878e10df7bd1b221b66df15098e3769055cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kr=C3=B6ning?= Date: Wed, 1 Jul 2026 11:18:11 +0200 Subject: [PATCH 6/6] Hermit: Filter out `.` and `..` from `getdents64` While Hermit does not return these yet, it will do so in the future. --- library/std/src/sys/fs/hermit.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/std/src/sys/fs/hermit.rs b/library/std/src/sys/fs/hermit.rs index 41c3f048e6b9a..bb5db2d596fa9 100644 --- a/library/std/src/sys/fs/hermit.rs +++ b/library/std/src/sys/fs/hermit.rs @@ -265,6 +265,9 @@ impl Iterator for ReadDir { // d_name is guaranteed to be null-terminated. let name = unsafe { CStr::from_ptr((&raw const (*entry_ptr).d_name).cast()) }; let name_bytes = name.to_bytes(); + if name_bytes == b"." || name_bytes == b".." { + continue; + } return Some(Ok(DirEntry { dir: Arc::clone(&self.inner),