From af7f76d89303cfcfb9c463a66ccc39d18d6f45a1 Mon Sep 17 00:00:00 2001 From: WhySoBad <49595640+WhySoBad@users.noreply.github.com> Date: Sat, 9 May 2026 23:41:38 +0200 Subject: [PATCH] Add shims for vectored reads and vectored writes This adds shims for the `readv`, `writev`, `preadv` and `pwritev` functions. --- src/tools/miri/src/provenance_gc.rs | 6 + src/tools/miri/src/shims/unix/fd.rs | 434 +++++++++++++++--- .../miri/src/shims/unix/foreign_items.rs | 39 +- src/tools/miri/tests/pass-dep/libc/libc-fs.rs | 287 ++++++++++++ .../miri/tests/pass-dep/libc/libc-socket.rs | 75 ++- src/tools/miri/tests/pass/shims/fs.rs | 87 +++- 6 files changed, 850 insertions(+), 78 deletions(-) diff --git a/src/tools/miri/src/provenance_gc.rs b/src/tools/miri/src/provenance_gc.rs index 02353411eb94..3656a9eaa87c 100644 --- a/src/tools/miri/src/provenance_gc.rs +++ b/src/tools/miri/src/provenance_gc.rs @@ -44,6 +44,12 @@ fn visit_provenance(&self, visit: &mut VisitWith<'_>) { } } +impl VisitProvenance for Vec { + fn visit_provenance(&self, visit: &mut VisitWith<'_>) { + self.iter().for_each(|el| el.visit_provenance(visit)); + } +} + impl VisitProvenance for std::cell::RefCell { fn visit_provenance(&self, visit: &mut VisitWith<'_>) { self.borrow().visit_provenance(visit) diff --git a/src/tools/miri/src/shims/unix/fd.rs b/src/tools/miri/src/shims/unix/fd.rs index 4a1dc1034164..ab17be0e04f4 100644 --- a/src/tools/miri/src/shims/unix/fd.rs +++ b/src/tools/miri/src/shims/unix/fd.rs @@ -5,10 +5,10 @@ use std::io::ErrorKind; use rand::RngExt; -use rustc_abi::Size; +use rustc_abi::{Align, Size}; use rustc_target::spec::Os; -use crate::shims::files::FileDescription; +use crate::shims::files::{DynFileDescriptionRef, FileDescription}; use crate::shims::sig::check_min_vararg_count; use crate::shims::unix::linux_like::epoll::EpollReadiness; use crate::shims::unix::*; @@ -316,7 +316,6 @@ fn read( .min(u64::try_from(this.target_isize_max()).unwrap()) .min(u64::try_from(isize::MAX).unwrap()); let count = usize::try_from(count).unwrap(); // now it fits in a `usize` - let communicate = this.machine.communicate(); // Get the FD. let Some(fd) = this.machine.fds.get(fd_num) else { @@ -324,33 +323,17 @@ fn read( return this.set_last_error_and_return(LibcError("EBADF"), dest); }; - // Handle the zero-sized case. The man page says: - // > If count is zero, read() may detect the errors described below. In the absence of any - // > errors, or if read() does not check for errors, a read() with a count of 0 returns zero - // > and has no other effects. - if count == 0 { - this.write_null(dest)?; - return interp_ok(()); - } - // Non-deterministically decide to further reduce the count, simulating a partial read (but - // never to 0, that would indicate EOF). - let count = if this.machine.short_fd_operations - && fd.short_fd_operations() - && count >= 2 - && this.machine.rng.get_mut().random() - { - count / 2 // since `count` is at least 2, the result is still at least 1 - } else { - count - }; - trace!("read: FD mapped to {fd:?}"); // We want to read at most `count` bytes. We are sure that `count` is not negative // because it was a target's `usize`. Also we are sure that it's smaller than // `usize::MAX` because it is bounded by the host's `isize`. - let finish = { - let dest = dest.clone(); + let dest = dest.clone(); + this.read_from_fd( + fd, + buf, + count, + offset, callback!( @capture<'tcx> { count: usize, @@ -363,22 +346,10 @@ fn read( // This must fit since `count` fits. this.write_int(u64::try_from(read_size).unwrap(), &dest) } - Err(e) => { - this.set_last_error_and_return(e, &dest) - } + Err(e) => this.set_last_error_and_return(e, &dest) }} - ) - }; - match offset { - None => fd.read(communicate, buf, count, this, finish)?, - Some(offset) => { - let Ok(offset) = u64::try_from(offset) else { - return this.set_last_error_and_return(LibcError("EINVAL"), dest); - }; - fd.as_unix(this).pread(communicate, offset, buf, count, this, finish)? - } - }; - interp_ok(()) + ), + ) } fn write( @@ -402,39 +373,18 @@ fn write( .min(u64::try_from(this.target_isize_max()).unwrap()) .min(u64::try_from(isize::MAX).unwrap()); let count = usize::try_from(count).unwrap(); // now it fits in a `usize` - let communicate = this.machine.communicate(); // We temporarily dup the FD to be able to retain mutable access to `this`. let Some(fd) = this.machine.fds.get(fd_num) else { return this.set_last_error_and_return(LibcError("EBADF"), dest); }; - // Handle the zero-sized case. The man page says: - // > If count is zero and fd refers to a regular file, then write() may return a failure - // > status if one of the errors below is detected. If no errors are detected, or error - // > detection is not performed, 0 is returned without causing any other effect. If count - // > is zero and fd refers to a file other than a regular file, the results are not - // > specified. - if count == 0 { - // For now let's not open the can of worms of what exactly "not specified" could mean... - this.write_null(dest)?; - return interp_ok(()); - } - // Non-deterministically decide to further reduce the count, simulating a partial write. - // We avoid reducing the write size to 0: the docs seem to be entirely fine with that, - // but the standard library is not (https://github.com/rust-lang/rust/issues/145959). - let count = if this.machine.short_fd_operations - && fd.short_fd_operations() - && count >= 2 - && this.machine.rng.get_mut().random() - { - count / 2 - } else { - count - }; - - let finish = { - let dest = dest.clone(); + let dest = dest.clone(); + this.write_to_fd( + fd, + buf, + count, + offset, callback!( @capture<'tcx> { count: usize, @@ -447,19 +397,355 @@ fn write( // This must fit since `count` fits. this.write_int(u64::try_from(write_size).unwrap(), &dest) } - Err(e) => { - this.set_last_error_and_return(e, &dest) - } + Err(e) => this.set_last_error_and_return(e, &dest) + }} - ) + ), + ) + } + + /// Vectored reads are implemented by first reading bytes from `fd` + /// into a temporary buffer which has the combined size of all buffers in + /// `iov`. After that we split the bytes of the combined buffer into the + /// buffers of `iov`. This ensures that the vectored read occurs atomically. + fn readv( + &mut self, + fd: &OpTy<'tcx>, + iov: &OpTy<'tcx>, + iovcnt: &OpTy<'tcx>, + offset: Option<&OpTy<'tcx>>, + dest: &MPlaceTy<'tcx>, + ) -> InterpResult<'tcx> { + let this = self.eval_context_mut(); + + let fd = this.read_scalar(fd)?.to_i32()?; + let iov_ptr = this.read_pointer(iov)?; + let iovcnt: u64 = this.read_scalar(iovcnt)?.to_i32()?.try_into().unwrap(); + // `readv` is the same as `preadv` without an offset. + let offset = if let Some(offset) = offset { + if matches!(this.tcx.sess.target.os, Os::Solaris) { + throw_unsup_format!( + "preadv: vectored reads with offsets aren't supported on Solaris" + ) + } + Some(this.read_scalar(offset)?.to_int(offset.layout.size)?) + } else { + None }; + + // Check that the FD exists. + let Some(fd) = this.machine.fds.get(fd) else { + return this.set_last_error_and_return(LibcError("EBADF"), dest); + }; + + let iovec_layout = this.libc_array_ty_layout("iovec", iovcnt); + let iov_ptr_mplace = this.ptr_to_mplace(iov_ptr, iovec_layout); + + // Read list of buffers from `iov`. + let mut buffers = Vec::new(); + + let mut array = this.project_array_fields(&iov_ptr_mplace)?; + while let Some((_idx, iovec)) = array.next(this)? { + let iov_len_field = this.project_field_named(&iovec, "iov_len")?; + let iov_len: u64 = this + .read_scalar(&iov_len_field)? + .to_int(iov_len_field.layout.size)? + .try_into() + .unwrap(); + + let iov_base_field = this.project_field_named(&iovec, "iov_base")?; + let iov_base_ptr = this.read_pointer(&iov_base_field)?; + + buffers.push((iov_base_ptr, iov_len)); + } + + let total_bytes = buffers.iter().map(|(_, len)| len).sum::(); + + // Allocate a temporary buffer which has the combined size of all buffers provided in `iov`. + let tmp_ptr: Pointer = this + .allocate_ptr( + Size::from_bytes(total_bytes), + Align::ONE, + MemoryKind::Stack, + AllocInit::Uninit, + )? + .into(); + + let dest = dest.clone(); + this.read_from_fd( + fd, + tmp_ptr, + usize::try_from(total_bytes).unwrap(), + offset, + callback!( + @capture<'tcx> { + tmp_ptr: Pointer, + buffers: Vec<(Pointer, u64)>, + dest: MPlaceTy<'tcx> + } |this, result: Result| { + let bytes_read = match result { + Ok(size) => { + this.write_scalar(Scalar::from_target_isize(size.try_into().unwrap(), this), &dest)?; + u64::try_from(size).unwrap() + }, + Err(e) => return this.set_last_error_and_return(e, &dest) + }; + let mut remaining_bytes = bytes_read; + + // Split the bytes from the temporary buffer into the buffers provided in `iov`. + // We start at the first buffer and fill them in order, until we reach the end of the + // initialized bytes in the temporary buffer. + for (buffer_ptr, buffer_len) in buffers { + // Offset temporary buffer by the amount of bytes we already copied into previous buffers. + let tmp_ptr_with_offset = + this.ptr_offset_inbounds(tmp_ptr, i64::try_from(bytes_read.strict_sub(remaining_bytes)).unwrap())?; + + // Copy at most as many bytes as the buffer fits but without reading + // any uninitialized bytes from the temporary buffer. + let copy_amount = buffer_len.min(remaining_bytes); + this.mem_copy( + tmp_ptr_with_offset, + buffer_ptr, + Size::from_bytes(copy_amount), + // The buffers are guaranteed to not overlap because we just newly allocated + // the `tmp_ptr`, and `tmp_ptr_with_offset` is guaranteed to be + // within those boundaries. + true, + )?; + + remaining_bytes = remaining_bytes.strict_sub(copy_amount); + if remaining_bytes == 0 { + // We don't have anything left to copy; exit the loop. + break; + } + } + + this.deallocate_ptr(tmp_ptr, None, MemoryKind::Stack) + }), + ) + } + + /// Vectored writes are implemented by first writing the bytes from all + /// buffers of `iov` into a combined temporary buffer and then writing this + /// combined buffer into `fd`. This ensures that the vectored write occurs atomically. + fn writev( + &mut self, + fd: &OpTy<'tcx>, + iov: &OpTy<'tcx>, + iovcnt: &OpTy<'tcx>, + offset: Option<&OpTy<'tcx>>, + dest: &MPlaceTy<'tcx>, + ) -> InterpResult<'tcx> { + let this = self.eval_context_mut(); + + let fd = this.read_scalar(fd)?.to_i32()?; + let iov_ptr = this.read_pointer(iov)?; + let iovcnt: u64 = this.read_scalar(iovcnt)?.to_i32()?.try_into().unwrap(); + // `writev` is the same as `pwritev` without an offset. + let offset = if let Some(offset) = offset { + if matches!(this.tcx.sess.target.os, Os::Solaris) { + throw_unsup_format!( + "pwritev: vectored writes with offsets aren't supported on Solaris" + ) + } + Some(this.read_scalar(offset)?.to_int(offset.layout.size)?) + } else { + None + }; + + // Check that the FD exists. + let Some(fd) = this.machine.fds.get(fd) else { + return this.set_last_error_and_return(LibcError("EBADF"), dest); + }; + + let iovec_layout = this.libc_array_ty_layout("iovec", iovcnt); + let iov_ptr_mplace = this.ptr_to_mplace(iov_ptr, iovec_layout); + + // Read list of buffers from `iov`. + let mut buffers = Vec::new(); + + let mut array = this.project_array_fields(&iov_ptr_mplace)?; + while let Some((_idx, iovec)) = array.next(this)? { + let iov_len_field = this.project_field_named(&iovec, "iov_len")?; + let iov_len: u64 = this + .read_scalar(&iov_len_field)? + .to_int(iov_len_field.layout.size)? + .try_into() + .unwrap(); + + let iov_base_field = this.project_field_named(&iovec, "iov_base")?; + let iov_base_ptr = this.read_pointer(&iov_base_field)?; + + buffers.push((iov_base_ptr, iov_len)); + } + + let total_bytes = buffers.iter().map(|(_, len)| len).sum::(); + + // Allocate a temporary buffer which has the combined size of all buffers provided in `iov`. + let tmp_ptr: Pointer = this + .allocate_ptr( + Size::from_bytes(total_bytes), + Align::ONE, + MemoryKind::Stack, + AllocInit::Uninit, + )? + .into(); + + // Copy the bytes from all buffers provided in `iov` into the temporary buffer. + // We start at the first buffer and then continue buffer by buffer. + let mut bytes_copied: u64 = 0; + for (buffer_ptr, buffer_len) in buffers { + // Offset temporary buffer by the amount of bytes we already copied from previous buffers. + let tmp_ptr_with_offset = + this.ptr_offset_inbounds(tmp_ptr, i64::try_from(bytes_copied).unwrap())?; + + this.mem_copy( + buffer_ptr, + tmp_ptr_with_offset, + Size::from_bytes(buffer_len), + // The buffers are guaranteed to not overlap because we just newly allocated + // the `tmp_ptr`, and `tmp_ptr_with_offset` is guaranteed to be + // within those boundaries. + true, + )?; + + bytes_copied = bytes_copied.strict_add(buffer_len); + } + + let dest = dest.clone(); + // Write bytes from the temporary buffer. This ensures the write is atomic. + this.write_to_fd( + fd, + tmp_ptr, + usize::try_from(total_bytes).unwrap(), + offset, + callback!( + @capture<'tcx> { + tmp_ptr: Pointer, + dest: MPlaceTy<'tcx>, + } + |this, result: Result| { + this.deallocate_ptr(tmp_ptr, None, MemoryKind::Stack)?; + match result { + Ok(size) => this.write_scalar(Scalar::from_target_isize(size.try_into().unwrap(), this), &dest), + Err(e) => this.set_last_error_and_return(e, &dest) + } + }), + ) + } +} + +impl<'tcx> EvalContextPrivExt<'tcx> for crate::MiriInterpCx<'tcx> {} +trait EvalContextPrivExt<'tcx>: crate::MiriInterpCxExt<'tcx> { + /// Read `len` bytes from the `fd` file description at `offset` into the buffer + /// pointed to by `ptr`. + /// If `offset` is [`Some`], the read occurs at the given absolute position rather + /// than the current file position (`read_at` semantics rather than `read`). + /// `finish` will be invoked when the read is done (which might be way after + /// this function returns as the read may block). + fn read_from_fd( + &mut self, + fd: DynFileDescriptionRef, + ptr: Pointer, + len: usize, + offset: Option, + finish: DynMachineCallback<'tcx, Result>, + ) -> InterpResult<'tcx> { + let this = self.eval_context_mut(); + + // Handle the zero-sized case. The man page says: + // > If count is zero, read() may detect the errors described below. In the absence of any + // > errors, or if read() does not check for errors, a read() with a count of 0 returns zero + // > and has no other effects. + if len == 0 { + return finish.call(this, Ok(0)); + } + + // Non-deterministically decide to further reduce the length, simulating a partial read (but + // never to 0, that would indicate EOF). + let len = if this.machine.short_fd_operations + && fd.short_fd_operations() + && len >= 2 + && this.machine.rng.get_mut().random() + { + len / 2 // since `len` is at least 2, the result is still at least 1 + } else { + len + }; + match offset { - None => fd.write(communicate, buf, count, this, finish)?, + None => fd.read(this.machine.communicate(), ptr, len, this, finish)?, Some(offset) => { let Ok(offset) = u64::try_from(offset) else { - return this.set_last_error_and_return(LibcError("EINVAL"), dest); + return finish.call(this, Err(LibcError("EINVAL"))); }; - fd.as_unix(this).pwrite(communicate, buf, count, offset, this, finish)? + fd.as_unix(this).pread( + this.machine.communicate(), + offset, + ptr, + len, + this, + finish, + )? + } + }; + interp_ok(()) + } + + /// Write `len` bytes at `offset` from the buffer pointed to by `ptr` into the `fd` + /// file description. + /// If `offset` is [`Some`], the write occurs at the given absolute position rather + /// than the current file position (`write_at` semantics rather than `write`). + /// `finish` will be invoked when the write is done (which might be way after + /// this function returns as the write may block). + fn write_to_fd( + &mut self, + fd: DynFileDescriptionRef, + ptr: Pointer, + len: usize, + offset: Option, + finish: DynMachineCallback<'tcx, Result>, + ) -> InterpResult<'tcx> { + let this = self.eval_context_mut(); + + // Handle the zero-sized case. The man page says: + // > If count is zero and fd refers to a regular file, then write() may return a failure + // > status if one of the errors below is detected. If no errors are detected, or error + // > detection is not performed, 0 is returned without causing any other effect. If count + // > is zero and fd refers to a file other than a regular file, the results are not + // > specified. + if len == 0 { + // For now let's not open the can of worms of what exactly "not specified" could mean... + return finish.call(this, Ok(0)); + } + + // Non-deterministically decide to further reduce the length, simulating a partial write. + // We avoid reducing the write size to 0: the docs seem to be entirely fine with that, + // but the standard library is not (https://github.com/rust-lang/rust/issues/145959). + let len = if this.machine.short_fd_operations + && fd.short_fd_operations() + && len >= 2 + && this.machine.rng.get_mut().random() + { + len / 2 + } else { + len + }; + + match offset { + None => fd.write(this.machine.communicate(), ptr, len, this, finish)?, + Some(offset) => { + let Ok(offset) = u64::try_from(offset) else { + return finish.call(this, Err(LibcError("EINVAL"))); + }; + fd.as_unix(this).pwrite( + this.machine.communicate(), + ptr, + len, + offset, + this, + finish, + )? } }; interp_ok(()) diff --git a/src/tools/miri/src/shims/unix/foreign_items.rs b/src/tools/miri/src/shims/unix/foreign_items.rs index 2ed708975c7d..67602a4fe88e 100644 --- a/src/tools/miri/src/shims/unix/foreign_items.rs +++ b/src/tools/miri/src/shims/unix/foreign_items.rs @@ -226,8 +226,25 @@ fn emulate_foreign_item_inner( trace!("Called write({:?}, {:?}, {:?})", fd, buf, count); this.write(fd, buf, count, None, dest)?; } + "readv" => { + let [fd, iov, iovcnt] = this.check_shim_sig( + shim_sig!(extern "C" fn(i32, *const _, i32) -> isize), + link_name, + abi, + args, + )?; + this.readv(fd, iov, iovcnt, None, dest)?; + } + "writev" => { + let [fd, iov, iovcnt] = this.check_shim_sig( + shim_sig!(extern "C" fn(i32, *const _, i32) -> isize), + link_name, + abi, + args, + )?; + this.writev(fd, iov, iovcnt, None, dest)?; + } "pread" => { - // FIXME: This does not have a direct test (#3179). let [fd, buf, count, offset] = this.check_shim_sig( shim_sig!(extern "C" fn(i32, *mut _, usize, libc::off_t) -> isize), link_name, @@ -241,7 +258,6 @@ fn emulate_foreign_item_inner( this.read(fd, buf, count, Some(offset), dest)?; } "pwrite" => { - // FIXME: This does not have a direct test (#3179). let [fd, buf, n, offset] = this.check_shim_sig( shim_sig!(extern "C" fn(i32, *const _, usize, libc::off_t) -> isize), link_name, @@ -255,6 +271,25 @@ fn emulate_foreign_item_inner( trace!("Called pwrite({:?}, {:?}, {:?}, {:?})", fd, buf, count, offset); this.write(fd, buf, count, Some(offset), dest)?; } + "preadv" => { + let [fd, iov, iovcnt, offset] = this.check_shim_sig( + shim_sig!(extern "C" fn(i32, *const _, i32, libc::off_t) -> isize), + link_name, + abi, + args, + )?; + this.readv(fd, iov, iovcnt, Some(offset), dest)?; + } + "pwritev" => { + let [fd, iov, iovcnt, offset] = this.check_shim_sig( + shim_sig!(extern "C" fn(i32, *const _, i32, libc::off_t) -> isize), + link_name, + abi, + args, + )?; + this.writev(fd, iov, iovcnt, Some(offset), dest)?; + } + "close" => { let [fd] = this.check_shim_sig( shim_sig!(extern "C" fn(i32) -> i32), diff --git a/src/tools/miri/tests/pass-dep/libc/libc-fs.rs b/src/tools/miri/tests/pass-dep/libc/libc-fs.rs index a388943f922f..56a4bbd27e71 100644 --- a/src/tools/miri/tests/pass-dep/libc/libc-fs.rs +++ b/src/tools/miri/tests/pass-dep/libc/libc-fs.rs @@ -10,6 +10,7 @@ use std::os::unix::ffi::OsStrExt; use std::os::unix::io::AsRawFd; use std::path::PathBuf; +use std::ptr; #[path = "../../utils/mod.rs"] mod utils; @@ -17,6 +18,8 @@ #[path = "../../utils/libc.rs"] mod libc_utils; +use libc_utils::errno_result; + fn main() { test_dup(); test_dup_stdout_stderr(); @@ -58,6 +61,16 @@ fn main() { test_statx_on_file_descriptor(); #[cfg(target_os = "linux")] test_statx_empty_path_on_pipe(); + test_readv(); + test_readv_empty_bufs(); + #[cfg(not(target_os = "solaris"))] + test_preadv(); + test_pread(); + test_writev(); + test_writev_empty_bufs(); + #[cfg(not(target_os = "solaris"))] + test_pwritev(); + test_pwrite(); } #[cfg(target_os = "linux")] @@ -846,3 +859,277 @@ pub fn check_stat_fields(stat: &libc::stat) { let _st_mtime_nsec = stat.st_mtime_nsec; let _st_ctime_nsec = stat.st_ctime_nsec; } + +/// Test vectored reads with multiple buffers. +fn test_readv() { + let file_contents = [1u8, 2, 3, 4, 5, 6]; + let path = utils::prepare_with_content("pass-libc-readv.txt", &file_contents); + let cpath = CString::new(path.into_os_string().into_encoded_bytes()).unwrap(); + let fd = unsafe { libc::open(cpath.as_ptr(), libc::O_RDONLY) }; + assert_ne!(fd, -1); + + let mut buffer = [0u8; 4]; + let (buffer1, buffer2) = buffer.split_at_mut(2); + + let iov = [ + libc::iovec { iov_base: ptr::null_mut::(), iov_len: 0 as libc::size_t }, + libc::iovec { + iov_base: buffer1.as_mut_ptr().cast::(), + iov_len: buffer1.len() as libc::size_t, + }, + libc::iovec { + iov_base: buffer2.as_mut_ptr().cast::(), + iov_len: buffer2.len() as libc::size_t, + }, + ]; + + let bytes_read = unsafe { + errno_result(libc::readv(fd, iov.as_ptr(), iov.len() as libc::c_int)).unwrap() as usize + }; + + // The vectored read should read at least one byte. + assert!(bytes_read > 0); + assert_eq!(&buffer[0..bytes_read], &file_contents[0..bytes_read]); +} + +/// Test that vectored reads without any buffers return zero. +fn test_readv_empty_bufs() { + let path = utils::prepare_with_content("pass-libc-readv-empty-bufs.txt", &[1u8, 2, 3]); + let cpath = CString::new(path.into_os_string().into_encoded_bytes()).unwrap(); + let fd = unsafe { libc::open(cpath.as_ptr(), libc::O_RDONLY) }; + assert_ne!(fd, -1); + unsafe { assert_eq!(errno_result(libc::readv(fd, ptr::null::(), 0)).unwrap(), 0) }; +} + +/// Test vectored reads with multiple buffers and a byte offset. +/// +/// **Note**: We skip this test on Solaris targets because Solaris +/// doesn't have `preadv`. +#[cfg(not(target_os = "solaris"))] +fn test_preadv() { + let file_contents = [1u8, 2, 3, 4, 5, 6]; + let path = utils::prepare_with_content("pass-libc-preadv.txt", &file_contents); + let cpath = CString::new(path.into_os_string().into_encoded_bytes()).unwrap(); + let fd = unsafe { libc::open(cpath.as_ptr(), libc::O_RDONLY) }; + assert_ne!(fd, -1); + + let mut buffer = [0u8; 4]; + let (buffer1, buffer2) = buffer.split_at_mut(2); + + let iov = [ + libc::iovec { iov_base: ptr::null_mut::(), iov_len: 0 as libc::size_t }, + libc::iovec { + iov_base: buffer1.as_mut_ptr().cast::(), + iov_len: buffer1.len() as libc::size_t, + }, + libc::iovec { + iov_base: buffer2.as_mut_ptr().cast::(), + iov_len: buffer2.len() as libc::size_t, + }, + ]; + + // Read with a 2 byte offset. + const OFFSET: usize = 2; + let bytes_read = unsafe { + errno_result(libc::preadv( + fd, + iov.as_ptr(), + iov.len() as libc::c_int, + OFFSET as libc::off_t, + )) + .unwrap() as usize + }; + + // The vectored read should read at least one byte. + assert!(bytes_read > 0); + // The vectored read should start at the provided byte offset. + assert_eq!(&buffer[0..bytes_read], &file_contents[OFFSET..(bytes_read + OFFSET)]); +} + +/// Test reading with an offset. +fn test_pread() { + let file_contents = [1u8, 2, 3, 4, 5, 6]; + let path = utils::prepare_with_content("pass-libc-pread.txt", &file_contents); + let cpath = CString::new(path.into_os_string().into_encoded_bytes()).unwrap(); + let fd = unsafe { libc::open(cpath.as_ptr(), libc::O_RDONLY) }; + assert_ne!(fd, -1); + + let mut buffer = [0u8; 2]; + + // Read with a 2 byte offset. + const OFFSET: usize = 2; + let bytes_read = unsafe { + errno_result(libc::pread( + fd, + buffer.as_mut_ptr().cast(), + buffer.len() as libc::size_t, + OFFSET as libc::off_t, + )) + .unwrap() as usize + }; + + // We should read at least one byte. + assert!(bytes_read > 0); + // The read should start at the provided byte offset. + assert_eq!(&buffer[0..bytes_read], &file_contents[OFFSET..(bytes_read + OFFSET)]); +} + +/// Test vectored writes with multiple buffers. +fn test_writev() { + let path = utils::prepare_with_content("pass-libc-writev.txt", &[]); + let cpath = CString::new(path.into_os_string().into_encoded_bytes()).unwrap(); + let fd = unsafe { libc::open(cpath.as_ptr(), libc::O_WRONLY) }; + assert_ne!(fd, -1); + + let mut write_buffer = [1u8, 2, 3, 4, 5, 6]; + let (buffer1, buffer2) = write_buffer.split_at_mut(3); + + let iov = [ + libc::iovec { iov_base: ptr::null_mut::(), iov_len: 0 as libc::size_t }, + libc::iovec { + iov_base: buffer1.as_mut_ptr().cast::(), + iov_len: buffer1.len() as libc::size_t, + }, + libc::iovec { + iov_base: buffer2.as_mut_ptr().cast::(), + iov_len: buffer2.len() as libc::size_t, + }, + ]; + + let bytes_written = unsafe { + errno_result(libc::writev(fd, iov.as_ptr(), iov.len() as libc::c_int)).unwrap() as usize + }; + // The vectored write should write at least one byte. + assert!(bytes_written > 0); + + // Open the FD again in readonly mode and with an unadvanced pointer. + let fd = unsafe { libc::open(cpath.as_ptr(), libc::O_RDONLY) }; + assert_ne!(fd, -1); + + let mut read_buffer = [0u8; 16]; + unsafe { + libc_utils::read_exact_generic( + read_buffer.as_mut_ptr().cast(), + bytes_written as libc::size_t, + libc_utils::Retry::NoRetry, + |buf, count| libc::read(fd, buf, count), + ) + .unwrap() + }; + + assert_eq!(&write_buffer[0..bytes_written], &read_buffer[0..bytes_written]); +} + +/// Test that vectored writes without any buffers return zero. +fn test_writev_empty_bufs() { + let path = utils::prepare_with_content("pass-libc-writev-empty-bufs.txt", &[1u8, 2, 3]); + let cpath = CString::new(path.into_os_string().into_encoded_bytes()).unwrap(); + let fd = unsafe { libc::open(cpath.as_ptr(), libc::O_WRONLY) }; + assert_ne!(fd, -1); + unsafe { + assert_eq!(errno_result(libc::writev(fd, ptr::null::(), 0)).unwrap(), 0) + }; +} + +/// Test vectored writes with multiple buffers and a byte offset. +/// +/// **Note**: We skip this test on Solaris targets because Solaris +/// doesn't have `pwritev`. +#[cfg(not(target_os = "solaris"))] +fn test_pwritev() { + let path = utils::prepare_with_content("pass-libc-pwritev.txt", &[]); + let cpath = CString::new(path.into_os_string().into_encoded_bytes()).unwrap(); + let fd = unsafe { libc::open(cpath.as_ptr(), libc::O_WRONLY) }; + assert_ne!(fd, -1); + + let mut write_buffer = [1u8, 2, 3, 4, 5, 6]; + let (buffer1, buffer2) = write_buffer.split_at_mut(3); + + let iov = [ + libc::iovec { iov_base: ptr::null_mut::(), iov_len: 0 as libc::size_t }, + libc::iovec { + iov_base: buffer1.as_mut_ptr().cast::(), + iov_len: buffer1.len() as libc::size_t, + }, + libc::iovec { + iov_base: buffer2.as_mut_ptr().cast::(), + iov_len: buffer2.len() as libc::size_t, + }, + ]; + + // Write with a 2 byte offset. + const OFFSET: usize = 2; + let bytes_written = unsafe { + errno_result(libc::pwritev( + fd, + iov.as_ptr(), + iov.len() as libc::c_int, + OFFSET as libc::off_t, + )) + .unwrap() as usize + }; + // The vectored write should write at least one byte. + assert!(bytes_written > 0); + + // Open the FD again in readonly mode and with an unadvanced pointer. + let fd = unsafe { libc::open(cpath.as_ptr(), libc::O_RDONLY) }; + assert_ne!(fd, -1); + + let mut read_buffer = [0u8; 16]; + // Read offset + bytes written. + unsafe { + libc_utils::read_exact_generic( + read_buffer.as_mut_ptr().cast(), + (bytes_written + OFFSET) as libc::size_t, + libc_utils::Retry::NoRetry, + |buf, count| libc::read(fd, buf, count), + ) + .unwrap() + }; + + // The vectored write should start at the provided byte offset. + assert_eq!(&write_buffer[0..bytes_written], &read_buffer[OFFSET..(bytes_written + OFFSET)]); +} + +/// Test writing with an offset. +fn test_pwrite() { + let path = utils::prepare_with_content("pass-libc-pwritev.txt", &[]); + let cpath = CString::new(path.into_os_string().into_encoded_bytes()).unwrap(); + let fd = unsafe { libc::open(cpath.as_ptr(), libc::O_WRONLY) }; + assert_ne!(fd, -1); + + let write_buffer = [1u8, 2, 3, 4, 5, 6]; + + // Write with a 2 byte offset. + const OFFSET: usize = 2; + let bytes_written = unsafe { + errno_result(libc::pwrite( + fd, + write_buffer.as_ptr().cast(), + write_buffer.len() as libc::size_t, + OFFSET as libc::off_t, + )) + .unwrap() as usize + }; + // We should write at least one byte. + assert!(bytes_written > 0); + + // Open the FD again in readonly mode and with an unadvanced pointer. + let fd = unsafe { libc::open(cpath.as_ptr(), libc::O_RDONLY) }; + assert_ne!(fd, -1); + + let mut read_buffer = [0u8; 16]; + // Read offset + bytes written. + unsafe { + libc_utils::read_exact_generic( + read_buffer.as_mut_ptr().cast(), + (bytes_written + OFFSET) as libc::size_t, + libc_utils::Retry::NoRetry, + |buf, count| libc::read(fd, buf, count), + ) + .unwrap() + }; + + // The write should start at the provided byte offset. + assert_eq!(&write_buffer[0..bytes_written], &read_buffer[OFFSET..(bytes_written + OFFSET)]); +} diff --git a/src/tools/miri/tests/pass-dep/libc/libc-socket.rs b/src/tools/miri/tests/pass-dep/libc/libc-socket.rs index e067f006325d..16ec68198db1 100644 --- a/src/tools/miri/tests/pass-dep/libc/libc-socket.rs +++ b/src/tools/miri/tests/pass-dep/libc/libc-socket.rs @@ -7,8 +7,8 @@ mod utils; use std::io::ErrorKind; -use std::thread; use std::time::Duration; +use std::{ptr, thread}; use libc_utils::*; @@ -37,6 +37,8 @@ fn main() { test_accept_connect(); test_send_peek_recv(); test_write_read(); + test_readv(); + test_writev(); test_getsockname_ipv4(); test_getsockname_ipv4_random_port(); @@ -342,6 +344,77 @@ fn test_write_read() { server_thread.join().unwrap(); } +/// Test vectored reads with multiple buffers on a connected socket. +fn test_readv() { + let (server_sockfd, addr) = net::make_listener_ipv4().unwrap(); + let client_sockfd = + unsafe { errno_result(libc::socket(libc::AF_INET, libc::SOCK_STREAM, 0)).unwrap() }; + + net::connect_ipv4(client_sockfd, addr).unwrap(); + let (peerfd, _) = net::accept_ipv4(server_sockfd).unwrap(); + + libc_utils::write_all(peerfd, TEST_BYTES).unwrap(); + + let mut buffer = [0u8; TEST_BYTES.len()]; + let (buffer1, buffer2) = buffer.split_at_mut(2); + + let iov = [ + libc::iovec { iov_base: ptr::null_mut::(), iov_len: 0 as libc::size_t }, + libc::iovec { + iov_base: buffer1.as_mut_ptr().cast::(), + iov_len: buffer1.len() as libc::size_t, + }, + libc::iovec { + iov_base: buffer2.as_mut_ptr().cast::(), + iov_len: buffer2.len() as libc::size_t, + }, + ]; + + let num = unsafe { + errno_result(libc::readv(client_sockfd, iov.as_ptr(), iov.len() as libc::c_int)).unwrap() + }; + assert_eq!(num as usize, TEST_BYTES.len()); + // The vectored read should read the entire buffer because we don't have + // short reads on sockets. + assert_eq!(&buffer, TEST_BYTES); +} + +/// Test vectored writes with multiple buffers on a connected socket. +fn test_writev() { + let (server_sockfd, addr) = net::make_listener_ipv4().unwrap(); + let client_sockfd = + unsafe { errno_result(libc::socket(libc::AF_INET, libc::SOCK_STREAM, 0)).unwrap() }; + + net::connect_ipv4(client_sockfd, addr).unwrap(); + let (peerfd, _) = net::accept_ipv4(server_sockfd).unwrap(); + + let mut write_buffer = TEST_BYTES.to_owned(); + let (buffer1, buffer2) = write_buffer.split_at_mut(3); + + let iov = [ + libc::iovec { iov_base: ptr::null_mut::(), iov_len: 0 as libc::size_t }, + libc::iovec { + iov_base: buffer1.as_mut_ptr().cast::(), + iov_len: buffer1.len() as libc::size_t, + }, + libc::iovec { + iov_base: buffer2.as_mut_ptr().cast::(), + iov_len: buffer2.len() as libc::size_t, + }, + ]; + + let num = unsafe { + errno_result(libc::writev(client_sockfd, iov.as_ptr(), iov.len() as libc::c_int)).unwrap() + }; + assert_eq!(num as usize, TEST_BYTES.len()); + + let mut buffer = [0u8; TEST_BYTES.len()]; + libc_utils::read_exact(peerfd, &mut buffer).unwrap(); + // The vectored write should write the entire buffer because we don't have + // short writes on sockets. + assert_eq!(&buffer, TEST_BYTES); +} + /// Test the `getsockname` syscall on an IPv4 socket which is bound. /// The `getsockname` syscall should return the same address as to /// which the socket was bound to. diff --git a/src/tools/miri/tests/pass/shims/fs.rs b/src/tools/miri/tests/pass/shims/fs.rs index cd3ae9f7c328..b249c065318c 100644 --- a/src/tools/miri/tests/pass/shims/fs.rs +++ b/src/tools/miri/tests/pass/shims/fs.rs @@ -2,13 +2,16 @@ #![feature(io_error_more)] #![feature(io_error_uncategorized)] +#![cfg_attr(unix, feature(unix_file_vectored_at))] use std::collections::BTreeMap; use std::ffi::OsString; use std::fs::{ self, File, OpenOptions, create_dir, read_dir, remove_dir, remove_dir_all, remove_file, rename, }; -use std::io::{Error, ErrorKind, IsTerminal, Read, Result, Seek, SeekFrom, Write}; +use std::io::{ + Error, ErrorKind, IoSlice, IoSliceMut, IsTerminal, Read, Result, Seek, SeekFrom, Write, +}; use std::path::Path; #[path = "../../utils/mod.rs"] @@ -39,6 +42,9 @@ fn main() { test_pread_pwrite(); #[cfg(not(any(target_os = "solaris", target_os = "android")))] test_flock(); + test_readv_writev(); + #[cfg(all(unix, not(any(target_os = "solaris", target_os = "android"))))] + test_preadv_pwritev(); } } @@ -427,3 +433,82 @@ fn test_flock() { // Unlock exclusive lock file1.unlock().unwrap(); } + +/// Test vectored reads and vectored writes. +fn test_readv_writev() { + let bytes = b"hello world!"; + let path = utils::prepare_with_content("miri_test_fs_readv_writev.txt", bytes); + let mut f = OpenOptions::new().read(true).write(true).open(path).unwrap(); + + let mut read_buffer = [0u8; 10]; + let (buffer1, buffer2) = read_buffer.split_at_mut(5); + + let bytes_read = + f.read_vectored(&mut [IoSliceMut::new(buffer1), IoSliceMut::new(buffer2)]).unwrap(); + + // Vectored read should read at least a byte. + assert!(bytes_read > 0); + assert_eq!(read_buffer[0..bytes_read], bytes[0..bytes_read]); + + let write_buffer = b"some additional bytes"; + let (buffer1, buffer2) = write_buffer.split_at(write_buffer.len() / 2); + + let bytes_written = f.write_vectored(&[IoSlice::new(buffer1), IoSlice::new(buffer2)]).unwrap(); + + // Vectored write should write at least a byte. + assert!(bytes_written > 0); + + // Reset file cursor to read the written bytes. + f.seek(SeekFrom::Start(bytes_read as u64)).unwrap(); + let mut written_bytes = vec![0u8; bytes_written]; + f.read_exact(&mut written_bytes).unwrap(); + assert_eq!(written_bytes.as_slice(), &write_buffer[0..bytes_written]); +} + +/// Test vectored reads and vectored writes with byte offsets. +/// +/// **Note**: We skip this test on Solaris and Android targets. This is +/// because Solaris doesn't have `preadv`/`pwritev`, and on Android the +/// standard library uses `syscall(...)` for vectored reads/writes with +/// offsets because older Android versions also didn't have `preadv`/`pwritev`. +#[cfg(all(unix, not(any(target_os = "solaris", target_os = "android"))))] +fn test_preadv_pwritev() { + use std::os::unix::fs::FileExt; + + let bytes = b"hello world!"; + let path = utils::prepare_with_content("miri_test_fs_preadv_pwritev.txt", bytes); + let mut f = OpenOptions::new().read(true).write(true).open(path).unwrap(); + + const OFFSET: usize = 2; + + let mut read_buffer = [0u8; 10]; + let (buffer1, buffer2) = read_buffer.split_at_mut(5); + + let bytes_read = f + .read_vectored_at(&mut [IoSliceMut::new(buffer1), IoSliceMut::new(buffer2)], OFFSET as u64) + .unwrap(); + + // Vectored read should read at least a byte at the provided offset. + assert!(bytes_read > 0); + assert_eq!(read_buffer[0..bytes_read], bytes[OFFSET..(bytes_read + OFFSET)]); + + let write_buffer = b"some additional bytes"; + let (buffer1, buffer2) = write_buffer.split_at(write_buffer.len() / 2); + + let bytes_written = f + .write_vectored_at( + &[IoSlice::new(buffer1), IoSlice::new(buffer2)], + (bytes.len() + OFFSET) as u64, + ) + .unwrap(); + + // Vectored write should write at least a byte at the provided offset. + assert!(bytes_written > 0); + + // Reset file cursor to read the written bytes. We move the cursor + // to include the offset. + f.seek(SeekFrom::Start((bytes.len() + OFFSET) as u64)).unwrap(); + let mut written_bytes = vec![0u8; bytes_written]; + f.read_exact(&mut written_bytes).unwrap(); + assert_eq!(written_bytes.as_slice(), &write_buffer[0..bytes_written]); +}