about summary refs log tree commit diff
path: root/library/std/src/sys/fd
diff options
context:
space:
mode:
authorThalia Archibald <thalia@archibald.dev>2025-03-24 23:01:13 -0700
committerThalia Archibald <thalia@archibald.dev>2025-04-04 20:11:08 -0700
commit4085af01836c3af3bd7c68060274ba7a0c334261 (patch)
tree4b279b982b05de0b3b19a914b556719c2863e3a3 /library/std/src/sys/fd
parent5cc60728e7ee10eb2ae5f61f7d412d9805b22f0c (diff)
Move fd into sys
Diffstat (limited to 'library/std/src/sys/fd')
-rw-r--r--library/std/src/sys/fd/hermit.rs175
-rw-r--r--library/std/src/sys/fd/mod.rs19
-rw-r--r--library/std/src/sys/fd/sgx.rs85
-rw-r--r--library/std/src/sys/fd/unix.rs679
-rw-r--r--library/std/src/sys/fd/unix/tests.rs12
-rw-r--r--library/std/src/sys/fd/wasi.rs331
6 files changed, 1301 insertions, 0 deletions
diff --git a/library/std/src/sys/fd/hermit.rs b/library/std/src/sys/fd/hermit.rs
new file mode 100644
index 00000000000..7e8ba065f1b
--- /dev/null
+++ b/library/std/src/sys/fd/hermit.rs
@@ -0,0 +1,175 @@
+#![unstable(reason = "not public", issue = "none", feature = "fd")]
+
+use crate::cmp;
+use crate::io::{self, BorrowedCursor, IoSlice, IoSliceMut, Read, SeekFrom};
+use crate::os::hermit::hermit_abi;
+use crate::os::hermit::io::{AsFd, AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd, RawFd};
+use crate::sys::{cvt, unsupported};
+use crate::sys_common::{AsInner, FromInner, IntoInner};
+
+const fn max_iov() -> usize {
+    hermit_abi::IOV_MAX
+}
+
+#[derive(Debug)]
+pub struct FileDesc {
+    fd: OwnedFd,
+}
+
+impl FileDesc {
+    pub fn read(&self, buf: &mut [u8]) -> io::Result<usize> {
+        let result =
+            cvt(unsafe { hermit_abi::read(self.fd.as_raw_fd(), buf.as_mut_ptr(), buf.len()) })?;
+        Ok(result as usize)
+    }
+
+    pub fn read_buf(&self, mut buf: BorrowedCursor<'_>) -> io::Result<()> {
+        // SAFETY: The `read` syscall does not read from the buffer, so it is
+        // safe to use `&mut [MaybeUninit<u8>]`.
+        let result = cvt(unsafe {
+            hermit_abi::read(
+                self.fd.as_raw_fd(),
+                buf.as_mut().as_mut_ptr() as *mut u8,
+                buf.capacity(),
+            )
+        })?;
+        // SAFETY: Exactly `result` bytes have been filled.
+        unsafe { buf.advance_unchecked(result as usize) };
+        Ok(())
+    }
+
+    pub fn read_vectored(&self, bufs: &mut [IoSliceMut<'_>]) -> io::Result<usize> {
+        let ret = cvt(unsafe {
+            hermit_abi::readv(
+                self.as_raw_fd(),
+                bufs.as_mut_ptr() as *mut hermit_abi::iovec as *const hermit_abi::iovec,
+                cmp::min(bufs.len(), max_iov()),
+            )
+        })?;
+        Ok(ret as usize)
+    }
+
+    #[inline]
+    pub fn is_read_vectored(&self) -> bool {
+        true
+    }
+
+    pub fn read_to_end(&self, buf: &mut Vec<u8>) -> io::Result<usize> {
+        let mut me = self;
+        (&mut me).read_to_end(buf)
+    }
+
+    pub fn write(&self, buf: &[u8]) -> io::Result<usize> {
+        let result =
+            cvt(unsafe { hermit_abi::write(self.fd.as_raw_fd(), buf.as_ptr(), buf.len()) })?;
+        Ok(result as usize)
+    }
+
+    pub fn write_vectored(&self, bufs: &[IoSlice<'_>]) -> io::Result<usize> {
+        let ret = cvt(unsafe {
+            hermit_abi::writev(
+                self.as_raw_fd(),
+                bufs.as_ptr() as *const hermit_abi::iovec,
+                cmp::min(bufs.len(), max_iov()),
+            )
+        })?;
+        Ok(ret as usize)
+    }
+
+    #[inline]
+    pub fn is_write_vectored(&self) -> bool {
+        true
+    }
+
+    pub fn seek(&self, pos: SeekFrom) -> io::Result<u64> {
+        let (whence, pos) = match pos {
+            // Casting to `i64` is fine, too large values will end up as
+            // negative which will cause an error in `lseek`.
+            SeekFrom::Start(off) => (hermit_abi::SEEK_SET, off as i64),
+            SeekFrom::End(off) => (hermit_abi::SEEK_END, off),
+            SeekFrom::Current(off) => (hermit_abi::SEEK_CUR, off),
+        };
+        let n = cvt(unsafe { hermit_abi::lseek(self.as_raw_fd(), pos as isize, whence) })?;
+        Ok(n as u64)
+    }
+
+    pub fn tell(&self) -> io::Result<u64> {
+        self.seek(SeekFrom::Current(0))
+    }
+
+    pub fn duplicate(&self) -> io::Result<FileDesc> {
+        self.duplicate_path(&[])
+    }
+
+    pub fn duplicate_path(&self, _path: &[u8]) -> io::Result<FileDesc> {
+        unsupported()
+    }
+
+    pub fn nonblocking(&self) -> io::Result<bool> {
+        Ok(false)
+    }
+
+    pub fn set_cloexec(&self) -> io::Result<()> {
+        unsupported()
+    }
+
+    pub fn set_nonblocking(&self, _nonblocking: bool) -> io::Result<()> {
+        unsupported()
+    }
+
+    pub fn fstat(&self, stat: *mut hermit_abi::stat) -> io::Result<()> {
+        cvt(unsafe { hermit_abi::fstat(self.fd.as_raw_fd(), stat) })?;
+        Ok(())
+    }
+}
+
+impl<'a> Read for &'a FileDesc {
+    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+        (**self).read(buf)
+    }
+}
+
+impl IntoInner<OwnedFd> for FileDesc {
+    fn into_inner(self) -> OwnedFd {
+        self.fd
+    }
+}
+
+impl FromInner<OwnedFd> for FileDesc {
+    fn from_inner(owned_fd: OwnedFd) -> Self {
+        Self { fd: owned_fd }
+    }
+}
+
+impl FromRawFd for FileDesc {
+    unsafe fn from_raw_fd(raw_fd: RawFd) -> Self {
+        let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
+        Self { fd }
+    }
+}
+
+impl AsInner<OwnedFd> for FileDesc {
+    #[inline]
+    fn as_inner(&self) -> &OwnedFd {
+        &self.fd
+    }
+}
+
+impl AsFd for FileDesc {
+    fn as_fd(&self) -> BorrowedFd<'_> {
+        self.fd.as_fd()
+    }
+}
+
+impl AsRawFd for FileDesc {
+    #[inline]
+    fn as_raw_fd(&self) -> RawFd {
+        self.fd.as_raw_fd()
+    }
+}
+
+impl IntoRawFd for FileDesc {
+    fn into_raw_fd(self) -> RawFd {
+        self.fd.into_raw_fd()
+    }
+}
diff --git a/library/std/src/sys/fd/mod.rs b/library/std/src/sys/fd/mod.rs
new file mode 100644
index 00000000000..86cfa44c39d
--- /dev/null
+++ b/library/std/src/sys/fd/mod.rs
@@ -0,0 +1,19 @@
+//! Platform-dependent file descriptor abstraction.
+
+#![deny(unsafe_op_in_unsafe_fn)]
+
+cfg_if::cfg_if! {
+    if #[cfg(target_family = "unix")] {
+        mod unix;
+        pub use unix::*;
+    } else if #[cfg(target_os = "hermit")] {
+        mod hermit;
+        pub use hermit::*;
+    } else if #[cfg(all(target_vendor = "fortanix", target_env = "sgx"))] {
+        mod sgx;
+        pub use sgx::*;
+    } else if #[cfg(target_os = "wasi")] {
+        mod wasi;
+        pub use wasi::*;
+    }
+}
diff --git a/library/std/src/sys/fd/sgx.rs b/library/std/src/sys/fd/sgx.rs
new file mode 100644
index 00000000000..1ef768db64c
--- /dev/null
+++ b/library/std/src/sys/fd/sgx.rs
@@ -0,0 +1,85 @@
+use fortanix_sgx_abi::Fd;
+
+use crate::io::{self, BorrowedCursor, IoSlice, IoSliceMut};
+use crate::mem::ManuallyDrop;
+use crate::sys::pal::abi::usercalls;
+use crate::sys::{AsInner, FromInner, IntoInner};
+
+#[derive(Debug)]
+pub struct FileDesc {
+    fd: Fd,
+}
+
+impl FileDesc {
+    pub fn new(fd: Fd) -> FileDesc {
+        FileDesc { fd }
+    }
+
+    pub fn raw(&self) -> Fd {
+        self.fd
+    }
+
+    /// Extracts the actual file descriptor without closing it.
+    pub fn into_raw(self) -> Fd {
+        ManuallyDrop::new(self).fd
+    }
+
+    pub fn read(&self, buf: &mut [u8]) -> io::Result<usize> {
+        usercalls::read(self.fd, &mut [IoSliceMut::new(buf)])
+    }
+
+    pub fn read_buf(&self, buf: BorrowedCursor<'_>) -> io::Result<()> {
+        usercalls::read_buf(self.fd, buf)
+    }
+
+    pub fn read_vectored(&self, bufs: &mut [IoSliceMut<'_>]) -> io::Result<usize> {
+        usercalls::read(self.fd, bufs)
+    }
+
+    #[inline]
+    pub fn is_read_vectored(&self) -> bool {
+        true
+    }
+
+    pub fn write(&self, buf: &[u8]) -> io::Result<usize> {
+        usercalls::write(self.fd, &[IoSlice::new(buf)])
+    }
+
+    pub fn write_vectored(&self, bufs: &[IoSlice<'_>]) -> io::Result<usize> {
+        usercalls::write(self.fd, bufs)
+    }
+
+    #[inline]
+    pub fn is_write_vectored(&self) -> bool {
+        true
+    }
+
+    pub fn flush(&self) -> io::Result<()> {
+        usercalls::flush(self.fd)
+    }
+}
+
+impl AsInner<Fd> for FileDesc {
+    #[inline]
+    fn as_inner(&self) -> &Fd {
+        &self.fd
+    }
+}
+
+impl IntoInner<Fd> for FileDesc {
+    fn into_inner(self) -> Fd {
+        ManuallyDrop::new(self).fd
+    }
+}
+
+impl FromInner<Fd> for FileDesc {
+    fn from_inner(fd: Fd) -> FileDesc {
+        FileDesc { fd }
+    }
+}
+
+impl Drop for FileDesc {
+    fn drop(&mut self) {
+        usercalls::close(self.fd)
+    }
+}
diff --git a/library/std/src/sys/fd/unix.rs b/library/std/src/sys/fd/unix.rs
new file mode 100644
index 00000000000..3fd92ccbff3
--- /dev/null
+++ b/library/std/src/sys/fd/unix.rs
@@ -0,0 +1,679 @@
+#![unstable(reason = "not public", issue = "none", feature = "fd")]
+#![allow(unsafe_op_in_unsafe_fn)]
+
+#[cfg(test)]
+mod tests;
+
+#[cfg(not(any(
+    target_os = "linux",
+    target_os = "l4re",
+    target_os = "android",
+    target_os = "hurd",
+)))]
+use libc::off_t as off64_t;
+#[cfg(any(
+    target_os = "android",
+    target_os = "linux",
+    target_os = "l4re",
+    target_os = "hurd",
+))]
+use libc::off64_t;
+
+use crate::cmp;
+use crate::io::{self, BorrowedCursor, IoSlice, IoSliceMut, Read};
+use crate::os::unix::io::{AsFd, AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd, RawFd};
+use crate::sys::cvt;
+#[cfg(all(target_os = "android", target_pointer_width = "64"))]
+use crate::sys::pal::weak::syscall;
+#[cfg(any(all(target_os = "android", target_pointer_width = "32"), target_vendor = "apple"))]
+use crate::sys::pal::weak::weak;
+use crate::sys_common::{AsInner, FromInner, IntoInner};
+
+#[derive(Debug)]
+pub struct FileDesc(OwnedFd);
+
+// The maximum read limit on most POSIX-like systems is `SSIZE_MAX`,
+// with the man page quoting that if the count of bytes to read is
+// greater than `SSIZE_MAX` the result is "unspecified".
+//
+// On Apple targets however, apparently the 64-bit libc is either buggy or
+// intentionally showing odd behavior by rejecting any read with a size
+// larger than or equal to INT_MAX. To handle both of these the read
+// size is capped on both platforms.
+const READ_LIMIT: usize = if cfg!(target_vendor = "apple") {
+    libc::c_int::MAX as usize - 1
+} else {
+    libc::ssize_t::MAX as usize
+};
+
+#[cfg(any(
+    target_os = "dragonfly",
+    target_os = "freebsd",
+    target_os = "netbsd",
+    target_os = "openbsd",
+    target_vendor = "apple",
+    target_os = "cygwin",
+))]
+const fn max_iov() -> usize {
+    libc::IOV_MAX as usize
+}
+
+#[cfg(any(
+    target_os = "android",
+    target_os = "emscripten",
+    target_os = "linux",
+    target_os = "nto",
+))]
+const fn max_iov() -> usize {
+    libc::UIO_MAXIOV as usize
+}
+
+#[cfg(not(any(
+    target_os = "android",
+    target_os = "dragonfly",
+    target_os = "emscripten",
+    target_os = "freebsd",
+    target_os = "linux",
+    target_os = "netbsd",
+    target_os = "nto",
+    target_os = "openbsd",
+    target_os = "horizon",
+    target_os = "vita",
+    target_vendor = "apple",
+    target_os = "cygwin",
+)))]
+const fn max_iov() -> usize {
+    16 // The minimum value required by POSIX.
+}
+
+impl FileDesc {
+    #[inline]
+    pub fn try_clone(&self) -> io::Result<Self> {
+        self.duplicate()
+    }
+
+    pub fn read(&self, buf: &mut [u8]) -> io::Result<usize> {
+        let ret = cvt(unsafe {
+            libc::read(
+                self.as_raw_fd(),
+                buf.as_mut_ptr() as *mut libc::c_void,
+                cmp::min(buf.len(), READ_LIMIT),
+            )
+        })?;
+        Ok(ret as usize)
+    }
+
+    #[cfg(not(any(
+        target_os = "espidf",
+        target_os = "horizon",
+        target_os = "vita",
+        target_os = "nuttx"
+    )))]
+    pub fn read_vectored(&self, bufs: &mut [IoSliceMut<'_>]) -> io::Result<usize> {
+        let ret = cvt(unsafe {
+            libc::readv(
+                self.as_raw_fd(),
+                bufs.as_mut_ptr() as *mut libc::iovec as *const libc::iovec,
+                cmp::min(bufs.len(), max_iov()) as libc::c_int,
+            )
+        })?;
+        Ok(ret as usize)
+    }
+
+    #[cfg(any(
+        target_os = "espidf",
+        target_os = "horizon",
+        target_os = "vita",
+        target_os = "nuttx"
+    ))]
+    pub fn read_vectored(&self, bufs: &mut [IoSliceMut<'_>]) -> io::Result<usize> {
+        io::default_read_vectored(|b| self.read(b), bufs)
+    }
+
+    #[inline]
+    pub fn is_read_vectored(&self) -> bool {
+        cfg!(not(any(
+            target_os = "espidf",
+            target_os = "horizon",
+            target_os = "vita",
+            target_os = "nuttx"
+        )))
+    }
+
+    pub fn read_to_end(&self, buf: &mut Vec<u8>) -> io::Result<usize> {
+        let mut me = self;
+        (&mut me).read_to_end(buf)
+    }
+
+    #[cfg_attr(target_os = "vxworks", allow(unused_unsafe))]
+    pub fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<usize> {
+        #[cfg(not(any(
+            all(target_os = "linux", not(target_env = "musl")),
+            target_os = "android",
+            target_os = "hurd"
+        )))]
+        use libc::pread as pread64;
+        #[cfg(any(
+            all(target_os = "linux", not(target_env = "musl")),
+            target_os = "android",
+            target_os = "hurd"
+        ))]
+        use libc::pread64;
+
+        unsafe {
+            cvt(pread64(
+                self.as_raw_fd(),
+                buf.as_mut_ptr() as *mut libc::c_void,
+                cmp::min(buf.len(), READ_LIMIT),
+                offset as off64_t,
+            ))
+            .map(|n| n as usize)
+        }
+    }
+
+    pub fn read_buf(&self, mut cursor: BorrowedCursor<'_>) -> io::Result<()> {
+        let ret = cvt(unsafe {
+            libc::read(
+                self.as_raw_fd(),
+                cursor.as_mut().as_mut_ptr() as *mut libc::c_void,
+                cmp::min(cursor.capacity(), READ_LIMIT),
+            )
+        })?;
+
+        // Safety: `ret` bytes were written to the initialized portion of the buffer
+        unsafe {
+            cursor.advance_unchecked(ret as usize);
+        }
+        Ok(())
+    }
+
+    #[cfg(any(
+        target_os = "aix",
+        target_os = "dragonfly", // DragonFly 1.5
+        target_os = "emscripten",
+        target_os = "freebsd",
+        target_os = "fuchsia",
+        target_os = "hurd",
+        target_os = "illumos",
+        target_os = "linux",
+        target_os = "netbsd",
+        target_os = "openbsd", // OpenBSD 2.7
+    ))]
+    pub fn read_vectored_at(&self, bufs: &mut [IoSliceMut<'_>], offset: u64) -> io::Result<usize> {
+        let ret = cvt(unsafe {
+            libc::preadv(
+                self.as_raw_fd(),
+                bufs.as_mut_ptr() as *mut libc::iovec as *const libc::iovec,
+                cmp::min(bufs.len(), max_iov()) as libc::c_int,
+                offset as _,
+            )
+        })?;
+        Ok(ret as usize)
+    }
+
+    #[cfg(not(any(
+        target_os = "aix",
+        target_os = "android",
+        target_os = "dragonfly",
+        target_os = "emscripten",
+        target_os = "freebsd",
+        target_os = "fuchsia",
+        target_os = "hurd",
+        target_os = "illumos",
+        target_os = "linux",
+        target_os = "netbsd",
+        target_os = "openbsd",
+        target_vendor = "apple",
+    )))]
+    pub fn read_vectored_at(&self, bufs: &mut [IoSliceMut<'_>], offset: u64) -> io::Result<usize> {
+        io::default_read_vectored(|b| self.read_at(b, offset), bufs)
+    }
+
+    // We support some old Android versions that do not have `preadv` in libc,
+    // so we use weak linkage and fallback to a direct syscall if not available.
+    //
+    // On 32-bit targets, we don't want to deal with weird ABI issues around
+    // passing 64-bits parameters to syscalls, so we fallback to the default
+    // implementation if `preadv` is not available.
+    #[cfg(all(target_os = "android", target_pointer_width = "64"))]
+    pub fn read_vectored_at(&self, bufs: &mut [IoSliceMut<'_>], offset: u64) -> io::Result<usize> {
+        syscall!(
+            fn preadv(
+                fd: libc::c_int,
+                iovec: *const libc::iovec,
+                n_iovec: libc::c_int,
+                offset: off64_t,
+            ) -> isize;
+        );
+
+        let ret = cvt(unsafe {
+            preadv(
+                self.as_raw_fd(),
+                bufs.as_mut_ptr() as *mut libc::iovec as *const libc::iovec,
+                cmp::min(bufs.len(), max_iov()) as libc::c_int,
+                offset as _,
+            )
+        })?;
+        Ok(ret as usize)
+    }
+
+    #[cfg(all(target_os = "android", target_pointer_width = "32"))]
+    // FIXME(#115199): Rust currently omits weak function definitions
+    // and its metadata from LLVM IR.
+    #[no_sanitize(cfi)]
+    pub fn read_vectored_at(&self, bufs: &mut [IoSliceMut<'_>], offset: u64) -> io::Result<usize> {
+        weak!(
+            fn preadv64(
+                fd: libc::c_int,
+                iovec: *const libc::iovec,
+                n_iovec: libc::c_int,
+                offset: off64_t,
+            ) -> isize;
+        );
+
+        match preadv64.get() {
+            Some(preadv) => {
+                let ret = cvt(unsafe {
+                    preadv(
+                        self.as_raw_fd(),
+                        bufs.as_mut_ptr() as *mut libc::iovec as *const libc::iovec,
+                        cmp::min(bufs.len(), max_iov()) as libc::c_int,
+                        offset as _,
+                    )
+                })?;
+                Ok(ret as usize)
+            }
+            None => io::default_read_vectored(|b| self.read_at(b, offset), bufs),
+        }
+    }
+
+    // We support old MacOS, iOS, watchOS, tvOS and visionOS. `preadv` was added in the following
+    // Apple OS versions:
+    // ios 14.0
+    // tvos 14.0
+    // macos 11.0
+    // watchos 7.0
+    //
+    // These versions may be newer than the minimum supported versions of OS's we support so we must
+    // use "weak" linking.
+    #[cfg(target_vendor = "apple")]
+    pub fn read_vectored_at(&self, bufs: &mut [IoSliceMut<'_>], offset: u64) -> io::Result<usize> {
+        weak!(
+            fn preadv(
+                fd: libc::c_int,
+                iovec: *const libc::iovec,
+                n_iovec: libc::c_int,
+                offset: off64_t,
+            ) -> isize;
+        );
+
+        match preadv.get() {
+            Some(preadv) => {
+                let ret = cvt(unsafe {
+                    preadv(
+                        self.as_raw_fd(),
+                        bufs.as_mut_ptr() as *mut libc::iovec as *const libc::iovec,
+                        cmp::min(bufs.len(), max_iov()) as libc::c_int,
+                        offset as _,
+                    )
+                })?;
+                Ok(ret as usize)
+            }
+            None => io::default_read_vectored(|b| self.read_at(b, offset), bufs),
+        }
+    }
+
+    pub fn write(&self, buf: &[u8]) -> io::Result<usize> {
+        let ret = cvt(unsafe {
+            libc::write(
+                self.as_raw_fd(),
+                buf.as_ptr() as *const libc::c_void,
+                cmp::min(buf.len(), READ_LIMIT),
+            )
+        })?;
+        Ok(ret as usize)
+    }
+
+    #[cfg(not(any(
+        target_os = "espidf",
+        target_os = "horizon",
+        target_os = "vita",
+        target_os = "nuttx"
+    )))]
+    pub fn write_vectored(&self, bufs: &[IoSlice<'_>]) -> io::Result<usize> {
+        let ret = cvt(unsafe {
+            libc::writev(
+                self.as_raw_fd(),
+                bufs.as_ptr() as *const libc::iovec,
+                cmp::min(bufs.len(), max_iov()) as libc::c_int,
+            )
+        })?;
+        Ok(ret as usize)
+    }
+
+    #[cfg(any(
+        target_os = "espidf",
+        target_os = "horizon",
+        target_os = "vita",
+        target_os = "nuttx"
+    ))]
+    pub fn write_vectored(&self, bufs: &[IoSlice<'_>]) -> io::Result<usize> {
+        io::default_write_vectored(|b| self.write(b), bufs)
+    }
+
+    #[inline]
+    pub fn is_write_vectored(&self) -> bool {
+        cfg!(not(any(
+            target_os = "espidf",
+            target_os = "horizon",
+            target_os = "vita",
+            target_os = "nuttx"
+        )))
+    }
+
+    #[cfg_attr(target_os = "vxworks", allow(unused_unsafe))]
+    pub fn write_at(&self, buf: &[u8], offset: u64) -> io::Result<usize> {
+        #[cfg(not(any(
+            all(target_os = "linux", not(target_env = "musl")),
+            target_os = "android",
+            target_os = "hurd"
+        )))]
+        use libc::pwrite as pwrite64;
+        #[cfg(any(
+            all(target_os = "linux", not(target_env = "musl")),
+            target_os = "android",
+            target_os = "hurd"
+        ))]
+        use libc::pwrite64;
+
+        unsafe {
+            cvt(pwrite64(
+                self.as_raw_fd(),
+                buf.as_ptr() as *const libc::c_void,
+                cmp::min(buf.len(), READ_LIMIT),
+                offset as off64_t,
+            ))
+            .map(|n| n as usize)
+        }
+    }
+
+    #[cfg(any(
+        target_os = "aix",
+        target_os = "dragonfly", // DragonFly 1.5
+        target_os = "emscripten",
+        target_os = "freebsd",
+        target_os = "fuchsia",
+        target_os = "hurd",
+        target_os = "illumos",
+        target_os = "linux",
+        target_os = "netbsd",
+        target_os = "openbsd", // OpenBSD 2.7
+    ))]
+    pub fn write_vectored_at(&self, bufs: &[IoSlice<'_>], offset: u64) -> io::Result<usize> {
+        let ret = cvt(unsafe {
+            libc::pwritev(
+                self.as_raw_fd(),
+                bufs.as_ptr() as *const libc::iovec,
+                cmp::min(bufs.len(), max_iov()) as libc::c_int,
+                offset as _,
+            )
+        })?;
+        Ok(ret as usize)
+    }
+
+    #[cfg(not(any(
+        target_os = "aix",
+        target_os = "android",
+        target_os = "dragonfly",
+        target_os = "emscripten",
+        target_os = "freebsd",
+        target_os = "fuchsia",
+        target_os = "hurd",
+        target_os = "illumos",
+        target_os = "linux",
+        target_os = "netbsd",
+        target_os = "openbsd",
+        target_vendor = "apple",
+    )))]
+    pub fn write_vectored_at(&self, bufs: &[IoSlice<'_>], offset: u64) -> io::Result<usize> {
+        io::default_write_vectored(|b| self.write_at(b, offset), bufs)
+    }
+
+    // We support some old Android versions that do not have `pwritev` in libc,
+    // so we use weak linkage and fallback to a direct syscall if not available.
+    //
+    // On 32-bit targets, we don't want to deal with weird ABI issues around
+    // passing 64-bits parameters to syscalls, so we fallback to the default
+    // implementation if `pwritev` is not available.
+    #[cfg(all(target_os = "android", target_pointer_width = "64"))]
+    pub fn write_vectored_at(&self, bufs: &[IoSlice<'_>], offset: u64) -> io::Result<usize> {
+        syscall!(
+            fn pwritev(
+                fd: libc::c_int,
+                iovec: *const libc::iovec,
+                n_iovec: libc::c_int,
+                offset: off64_t,
+            ) -> isize;
+        );
+
+        let ret = cvt(unsafe {
+            pwritev(
+                self.as_raw_fd(),
+                bufs.as_ptr() as *const libc::iovec,
+                cmp::min(bufs.len(), max_iov()) as libc::c_int,
+                offset as _,
+            )
+        })?;
+        Ok(ret as usize)
+    }
+
+    #[cfg(all(target_os = "android", target_pointer_width = "32"))]
+    pub fn write_vectored_at(&self, bufs: &[IoSlice<'_>], offset: u64) -> io::Result<usize> {
+        weak!(
+            fn pwritev64(
+                fd: libc::c_int,
+                iovec: *const libc::iovec,
+                n_iovec: libc::c_int,
+                offset: off64_t,
+            ) -> isize;
+        );
+
+        match pwritev64.get() {
+            Some(pwritev) => {
+                let ret = cvt(unsafe {
+                    pwritev(
+                        self.as_raw_fd(),
+                        bufs.as_ptr() as *const libc::iovec,
+                        cmp::min(bufs.len(), max_iov()) as libc::c_int,
+                        offset as _,
+                    )
+                })?;
+                Ok(ret as usize)
+            }
+            None => io::default_write_vectored(|b| self.write_at(b, offset), bufs),
+        }
+    }
+
+    // We support old MacOS, iOS, watchOS, tvOS and visionOS. `pwritev` was added in the following
+    // Apple OS versions:
+    // ios 14.0
+    // tvos 14.0
+    // macos 11.0
+    // watchos 7.0
+    //
+    // These versions may be newer than the minimum supported versions of OS's we support so we must
+    // use "weak" linking.
+    #[cfg(target_vendor = "apple")]
+    pub fn write_vectored_at(&self, bufs: &[IoSlice<'_>], offset: u64) -> io::Result<usize> {
+        weak!(
+            fn pwritev(
+                fd: libc::c_int,
+                iovec: *const libc::iovec,
+                n_iovec: libc::c_int,
+                offset: off64_t,
+            ) -> isize;
+        );
+
+        match pwritev.get() {
+            Some(pwritev) => {
+                let ret = cvt(unsafe {
+                    pwritev(
+                        self.as_raw_fd(),
+                        bufs.as_ptr() as *const libc::iovec,
+                        cmp::min(bufs.len(), max_iov()) as libc::c_int,
+                        offset as _,
+                    )
+                })?;
+                Ok(ret as usize)
+            }
+            None => io::default_write_vectored(|b| self.write_at(b, offset), bufs),
+        }
+    }
+
+    #[cfg(not(any(
+        target_env = "newlib",
+        target_os = "solaris",
+        target_os = "illumos",
+        target_os = "emscripten",
+        target_os = "fuchsia",
+        target_os = "l4re",
+        target_os = "linux",
+        target_os = "cygwin",
+        target_os = "haiku",
+        target_os = "redox",
+        target_os = "vxworks",
+        target_os = "nto",
+    )))]
+    pub fn set_cloexec(&self) -> io::Result<()> {
+        unsafe {
+            cvt(libc::ioctl(self.as_raw_fd(), libc::FIOCLEX))?;
+            Ok(())
+        }
+    }
+    #[cfg(any(
+        all(
+            target_env = "newlib",
+            not(any(target_os = "espidf", target_os = "horizon", target_os = "vita"))
+        ),
+        target_os = "solaris",
+        target_os = "illumos",
+        target_os = "emscripten",
+        target_os = "fuchsia",
+        target_os = "l4re",
+        target_os = "linux",
+        target_os = "cygwin",
+        target_os = "haiku",
+        target_os = "redox",
+        target_os = "vxworks",
+        target_os = "nto",
+    ))]
+    pub fn set_cloexec(&self) -> io::Result<()> {
+        unsafe {
+            let previous = cvt(libc::fcntl(self.as_raw_fd(), libc::F_GETFD))?;
+            let new = previous | libc::FD_CLOEXEC;
+            if new != previous {
+                cvt(libc::fcntl(self.as_raw_fd(), libc::F_SETFD, new))?;
+            }
+            Ok(())
+        }
+    }
+    #[cfg(any(target_os = "espidf", target_os = "horizon", target_os = "vita"))]
+    pub fn set_cloexec(&self) -> io::Result<()> {
+        // FD_CLOEXEC is not supported in ESP-IDF, Horizon OS and Vita but there's no need to,
+        // because none of them supports spawning processes.
+        Ok(())
+    }
+
+    #[cfg(target_os = "linux")]
+    pub fn set_nonblocking(&self, nonblocking: bool) -> io::Result<()> {
+        unsafe {
+            let v = nonblocking as libc::c_int;
+            cvt(libc::ioctl(self.as_raw_fd(), libc::FIONBIO, &v))?;
+            Ok(())
+        }
+    }
+
+    #[cfg(not(target_os = "linux"))]
+    pub fn set_nonblocking(&self, nonblocking: bool) -> io::Result<()> {
+        unsafe {
+            let previous = cvt(libc::fcntl(self.as_raw_fd(), libc::F_GETFL))?;
+            let new = if nonblocking {
+                previous | libc::O_NONBLOCK
+            } else {
+                previous & !libc::O_NONBLOCK
+            };
+            if new != previous {
+                cvt(libc::fcntl(self.as_raw_fd(), libc::F_SETFL, new))?;
+            }
+            Ok(())
+        }
+    }
+
+    #[inline]
+    pub fn duplicate(&self) -> io::Result<FileDesc> {
+        Ok(Self(self.0.try_clone()?))
+    }
+}
+
+impl<'a> Read for &'a FileDesc {
+    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+        (**self).read(buf)
+    }
+
+    fn read_buf(&mut self, cursor: BorrowedCursor<'_>) -> io::Result<()> {
+        (**self).read_buf(cursor)
+    }
+
+    fn read_vectored(&mut self, bufs: &mut [IoSliceMut<'_>]) -> io::Result<usize> {
+        (**self).read_vectored(bufs)
+    }
+
+    #[inline]
+    fn is_read_vectored(&self) -> bool {
+        (**self).is_read_vectored()
+    }
+}
+
+impl AsInner<OwnedFd> for FileDesc {
+    #[inline]
+    fn as_inner(&self) -> &OwnedFd {
+        &self.0
+    }
+}
+
+impl IntoInner<OwnedFd> for FileDesc {
+    fn into_inner(self) -> OwnedFd {
+        self.0
+    }
+}
+
+impl FromInner<OwnedFd> for FileDesc {
+    fn from_inner(owned_fd: OwnedFd) -> Self {
+        Self(owned_fd)
+    }
+}
+
+impl AsFd for FileDesc {
+    fn as_fd(&self) -> BorrowedFd<'_> {
+        self.0.as_fd()
+    }
+}
+
+impl AsRawFd for FileDesc {
+    #[inline]
+    fn as_raw_fd(&self) -> RawFd {
+        self.0.as_raw_fd()
+    }
+}
+
+impl IntoRawFd for FileDesc {
+    fn into_raw_fd(self) -> RawFd {
+        self.0.into_raw_fd()
+    }
+}
+
+impl FromRawFd for FileDesc {
+    unsafe fn from_raw_fd(raw_fd: RawFd) -> Self {
+        Self(FromRawFd::from_raw_fd(raw_fd))
+    }
+}
diff --git a/library/std/src/sys/fd/unix/tests.rs b/library/std/src/sys/fd/unix/tests.rs
new file mode 100644
index 00000000000..fcd66c71707
--- /dev/null
+++ b/library/std/src/sys/fd/unix/tests.rs
@@ -0,0 +1,12 @@
+use core::mem::ManuallyDrop;
+
+use super::FileDesc;
+use crate::io::IoSlice;
+use crate::os::unix::io::FromRawFd;
+
+#[test]
+fn limit_vector_count() {
+    let stdout = ManuallyDrop::new(unsafe { FileDesc::from_raw_fd(1) });
+    let bufs = (0..1500).map(|_| IoSlice::new(&[])).collect::<Vec<_>>();
+    assert!(stdout.write_vectored(&bufs).is_ok());
+}
diff --git a/library/std/src/sys/fd/wasi.rs b/library/std/src/sys/fd/wasi.rs
new file mode 100644
index 00000000000..80a5143ff0b
--- /dev/null
+++ b/library/std/src/sys/fd/wasi.rs
@@ -0,0 +1,331 @@
+#![expect(dead_code)]
+
+use crate::io::{self, BorrowedCursor, IoSlice, IoSliceMut, SeekFrom};
+use crate::mem;
+use crate::net::Shutdown;
+use crate::os::wasi::io::{AsFd, AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd, RawFd};
+use crate::sys::pal::err2io;
+use crate::sys_common::{AsInner, AsInnerMut, FromInner, IntoInner};
+
+#[derive(Debug)]
+pub struct WasiFd {
+    fd: OwnedFd,
+}
+
+fn iovec<'a>(a: &'a mut [IoSliceMut<'_>]) -> &'a [wasi::Iovec] {
+    assert_eq!(size_of::<IoSliceMut<'_>>(), size_of::<wasi::Iovec>());
+    assert_eq!(align_of::<IoSliceMut<'_>>(), align_of::<wasi::Iovec>());
+    // SAFETY: `IoSliceMut` and `IoVec` have exactly the same memory layout.
+    // We decorate our `IoSliceMut` with `repr(transparent)` (see `io.rs`), and
+    // `crate::io::IoSliceMut` is a `repr(transparent)` wrapper around our type, so this is
+    // guaranteed.
+    unsafe { mem::transmute(a) }
+}
+
+fn ciovec<'a>(a: &'a [IoSlice<'_>]) -> &'a [wasi::Ciovec] {
+    assert_eq!(size_of::<IoSlice<'_>>(), size_of::<wasi::Ciovec>());
+    assert_eq!(align_of::<IoSlice<'_>>(), align_of::<wasi::Ciovec>());
+    // SAFETY: `IoSlice` and `CIoVec` have exactly the same memory layout.
+    // We decorate our `IoSlice` with `repr(transparent)` (see `io.rs`), and
+    // `crate::io::IoSlice` is a `repr(transparent)` wrapper around our type, so this is
+    // guaranteed.
+    unsafe { mem::transmute(a) }
+}
+
+impl WasiFd {
+    pub fn datasync(&self) -> io::Result<()> {
+        unsafe { wasi::fd_datasync(self.as_raw_fd() as wasi::Fd).map_err(err2io) }
+    }
+
+    pub fn pread(&self, bufs: &mut [IoSliceMut<'_>], offset: u64) -> io::Result<usize> {
+        unsafe { wasi::fd_pread(self.as_raw_fd() as wasi::Fd, iovec(bufs), offset).map_err(err2io) }
+    }
+
+    pub fn pwrite(&self, bufs: &[IoSlice<'_>], offset: u64) -> io::Result<usize> {
+        unsafe {
+            wasi::fd_pwrite(self.as_raw_fd() as wasi::Fd, ciovec(bufs), offset).map_err(err2io)
+        }
+    }
+
+    pub fn read(&self, bufs: &mut [IoSliceMut<'_>]) -> io::Result<usize> {
+        unsafe { wasi::fd_read(self.as_raw_fd() as wasi::Fd, iovec(bufs)).map_err(err2io) }
+    }
+
+    pub fn read_buf(&self, mut buf: BorrowedCursor<'_>) -> io::Result<()> {
+        unsafe {
+            let bufs = [wasi::Iovec {
+                buf: buf.as_mut().as_mut_ptr() as *mut u8,
+                buf_len: buf.capacity(),
+            }];
+            match wasi::fd_read(self.as_raw_fd() as wasi::Fd, &bufs) {
+                Ok(n) => {
+                    buf.advance_unchecked(n);
+                    Ok(())
+                }
+                Err(e) => Err(err2io(e)),
+            }
+        }
+    }
+
+    pub fn write(&self, bufs: &[IoSlice<'_>]) -> io::Result<usize> {
+        unsafe { wasi::fd_write(self.as_raw_fd() as wasi::Fd, ciovec(bufs)).map_err(err2io) }
+    }
+
+    pub fn seek(&self, pos: SeekFrom) -> io::Result<u64> {
+        let (whence, offset) = match pos {
+            SeekFrom::Start(pos) => (wasi::WHENCE_SET, pos as i64),
+            SeekFrom::End(pos) => (wasi::WHENCE_END, pos),
+            SeekFrom::Current(pos) => (wasi::WHENCE_CUR, pos),
+        };
+        unsafe { wasi::fd_seek(self.as_raw_fd() as wasi::Fd, offset, whence).map_err(err2io) }
+    }
+
+    pub fn tell(&self) -> io::Result<u64> {
+        unsafe { wasi::fd_tell(self.as_raw_fd() as wasi::Fd).map_err(err2io) }
+    }
+
+    // FIXME: __wasi_fd_fdstat_get
+
+    pub fn set_flags(&self, flags: wasi::Fdflags) -> io::Result<()> {
+        unsafe { wasi::fd_fdstat_set_flags(self.as_raw_fd() as wasi::Fd, flags).map_err(err2io) }
+    }
+
+    pub fn set_rights(&self, base: wasi::Rights, inheriting: wasi::Rights) -> io::Result<()> {
+        unsafe {
+            wasi::fd_fdstat_set_rights(self.as_raw_fd() as wasi::Fd, base, inheriting)
+                .map_err(err2io)
+        }
+    }
+
+    pub fn sync(&self) -> io::Result<()> {
+        unsafe { wasi::fd_sync(self.as_raw_fd() as wasi::Fd).map_err(err2io) }
+    }
+
+    pub(crate) fn advise(&self, offset: u64, len: u64, advice: wasi::Advice) -> io::Result<()> {
+        unsafe {
+            wasi::fd_advise(self.as_raw_fd() as wasi::Fd, offset, len, advice).map_err(err2io)
+        }
+    }
+
+    pub fn allocate(&self, offset: u64, len: u64) -> io::Result<()> {
+        unsafe { wasi::fd_allocate(self.as_raw_fd() as wasi::Fd, offset, len).map_err(err2io) }
+    }
+
+    pub fn create_directory(&self, path: &str) -> io::Result<()> {
+        unsafe { wasi::path_create_directory(self.as_raw_fd() as wasi::Fd, path).map_err(err2io) }
+    }
+
+    pub fn link(
+        &self,
+        old_flags: wasi::Lookupflags,
+        old_path: &str,
+        new_fd: &WasiFd,
+        new_path: &str,
+    ) -> io::Result<()> {
+        unsafe {
+            wasi::path_link(
+                self.as_raw_fd() as wasi::Fd,
+                old_flags,
+                old_path,
+                new_fd.as_raw_fd() as wasi::Fd,
+                new_path,
+            )
+            .map_err(err2io)
+        }
+    }
+
+    pub fn open(
+        &self,
+        dirflags: wasi::Lookupflags,
+        path: &str,
+        oflags: wasi::Oflags,
+        fs_rights_base: wasi::Rights,
+        fs_rights_inheriting: wasi::Rights,
+        fs_flags: wasi::Fdflags,
+    ) -> io::Result<WasiFd> {
+        unsafe {
+            wasi::path_open(
+                self.as_raw_fd() as wasi::Fd,
+                dirflags,
+                path,
+                oflags,
+                fs_rights_base,
+                fs_rights_inheriting,
+                fs_flags,
+            )
+            .map(|fd| WasiFd::from_raw_fd(fd as RawFd))
+            .map_err(err2io)
+        }
+    }
+
+    pub fn readdir(&self, buf: &mut [u8], cookie: wasi::Dircookie) -> io::Result<usize> {
+        unsafe {
+            wasi::fd_readdir(self.as_raw_fd() as wasi::Fd, buf.as_mut_ptr(), buf.len(), cookie)
+                .map_err(err2io)
+        }
+    }
+
+    pub fn readlink(&self, path: &str, buf: &mut [u8]) -> io::Result<usize> {
+        unsafe {
+            wasi::path_readlink(self.as_raw_fd() as wasi::Fd, path, buf.as_mut_ptr(), buf.len())
+                .map_err(err2io)
+        }
+    }
+
+    pub fn rename(&self, old_path: &str, new_fd: &WasiFd, new_path: &str) -> io::Result<()> {
+        unsafe {
+            wasi::path_rename(
+                self.as_raw_fd() as wasi::Fd,
+                old_path,
+                new_fd.as_raw_fd() as wasi::Fd,
+                new_path,
+            )
+            .map_err(err2io)
+        }
+    }
+
+    pub(crate) fn filestat_get(&self) -> io::Result<wasi::Filestat> {
+        unsafe { wasi::fd_filestat_get(self.as_raw_fd() as wasi::Fd).map_err(err2io) }
+    }
+
+    pub fn filestat_set_times(
+        &self,
+        atim: wasi::Timestamp,
+        mtim: wasi::Timestamp,
+        fstflags: wasi::Fstflags,
+    ) -> io::Result<()> {
+        unsafe {
+            wasi::fd_filestat_set_times(self.as_raw_fd() as wasi::Fd, atim, mtim, fstflags)
+                .map_err(err2io)
+        }
+    }
+
+    pub fn filestat_set_size(&self, size: u64) -> io::Result<()> {
+        unsafe { wasi::fd_filestat_set_size(self.as_raw_fd() as wasi::Fd, size).map_err(err2io) }
+    }
+
+    pub(crate) fn path_filestat_get(
+        &self,
+        flags: wasi::Lookupflags,
+        path: &str,
+    ) -> io::Result<wasi::Filestat> {
+        unsafe {
+            wasi::path_filestat_get(self.as_raw_fd() as wasi::Fd, flags, path).map_err(err2io)
+        }
+    }
+
+    pub fn path_filestat_set_times(
+        &self,
+        flags: wasi::Lookupflags,
+        path: &str,
+        atim: wasi::Timestamp,
+        mtim: wasi::Timestamp,
+        fstflags: wasi::Fstflags,
+    ) -> io::Result<()> {
+        unsafe {
+            wasi::path_filestat_set_times(
+                self.as_raw_fd() as wasi::Fd,
+                flags,
+                path,
+                atim,
+                mtim,
+                fstflags,
+            )
+            .map_err(err2io)
+        }
+    }
+
+    pub fn symlink(&self, old_path: &str, new_path: &str) -> io::Result<()> {
+        unsafe {
+            wasi::path_symlink(old_path, self.as_raw_fd() as wasi::Fd, new_path).map_err(err2io)
+        }
+    }
+
+    pub fn unlink_file(&self, path: &str) -> io::Result<()> {
+        unsafe { wasi::path_unlink_file(self.as_raw_fd() as wasi::Fd, path).map_err(err2io) }
+    }
+
+    pub fn remove_directory(&self, path: &str) -> io::Result<()> {
+        unsafe { wasi::path_remove_directory(self.as_raw_fd() as wasi::Fd, path).map_err(err2io) }
+    }
+
+    pub fn sock_accept(&self, flags: wasi::Fdflags) -> io::Result<wasi::Fd> {
+        unsafe { wasi::sock_accept(self.as_raw_fd() as wasi::Fd, flags).map_err(err2io) }
+    }
+
+    pub fn sock_recv(
+        &self,
+        ri_data: &mut [IoSliceMut<'_>],
+        ri_flags: wasi::Riflags,
+    ) -> io::Result<(usize, wasi::Roflags)> {
+        unsafe {
+            wasi::sock_recv(self.as_raw_fd() as wasi::Fd, iovec(ri_data), ri_flags).map_err(err2io)
+        }
+    }
+
+    pub fn sock_send(&self, si_data: &[IoSlice<'_>], si_flags: wasi::Siflags) -> io::Result<usize> {
+        unsafe {
+            wasi::sock_send(self.as_raw_fd() as wasi::Fd, ciovec(si_data), si_flags).map_err(err2io)
+        }
+    }
+
+    pub fn sock_shutdown(&self, how: Shutdown) -> io::Result<()> {
+        let how = match how {
+            Shutdown::Read => wasi::SDFLAGS_RD,
+            Shutdown::Write => wasi::SDFLAGS_WR,
+            Shutdown::Both => wasi::SDFLAGS_WR | wasi::SDFLAGS_RD,
+        };
+        unsafe { wasi::sock_shutdown(self.as_raw_fd() as wasi::Fd, how).map_err(err2io) }
+    }
+}
+
+impl AsInner<OwnedFd> for WasiFd {
+    #[inline]
+    fn as_inner(&self) -> &OwnedFd {
+        &self.fd
+    }
+}
+
+impl AsInnerMut<OwnedFd> for WasiFd {
+    #[inline]
+    fn as_inner_mut(&mut self) -> &mut OwnedFd {
+        &mut self.fd
+    }
+}
+
+impl IntoInner<OwnedFd> for WasiFd {
+    fn into_inner(self) -> OwnedFd {
+        self.fd
+    }
+}
+
+impl FromInner<OwnedFd> for WasiFd {
+    fn from_inner(owned_fd: OwnedFd) -> Self {
+        Self { fd: owned_fd }
+    }
+}
+
+impl AsFd for WasiFd {
+    fn as_fd(&self) -> BorrowedFd<'_> {
+        self.fd.as_fd()
+    }
+}
+
+impl AsRawFd for WasiFd {
+    #[inline]
+    fn as_raw_fd(&self) -> RawFd {
+        self.fd.as_raw_fd()
+    }
+}
+
+impl IntoRawFd for WasiFd {
+    fn into_raw_fd(self) -> RawFd {
+        self.fd.into_raw_fd()
+    }
+}
+
+impl FromRawFd for WasiFd {
+    unsafe fn from_raw_fd(raw_fd: RawFd) -> Self {
+        unsafe { Self { fd: FromRawFd::from_raw_fd(raw_fd) } }
+    }
+}