Merge pull request #5019 from WhySoBad/file-description-readv-writev

Add vectored read and vectored write shims for Unix targets
This commit is contained in:
Ralf Jung
2026-05-16 16:38:38 +00:00
committed by GitHub
6 changed files with 850 additions and 78 deletions
+6
View File
@@ -44,6 +44,12 @@ fn visit_provenance(&self, visit: &mut VisitWith<'_>) {
}
}
impl<T: VisitProvenance> VisitProvenance for Vec<T> {
fn visit_provenance(&self, visit: &mut VisitWith<'_>) {
self.iter().for_each(|el| el.visit_provenance(visit));
}
}
impl<T: VisitProvenance> VisitProvenance for std::cell::RefCell<T> {
fn visit_provenance(&self, visit: &mut VisitWith<'_>) {
self.borrow().visit_provenance(visit)
+360 -74
View File
@@ -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::<u64>();
// 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<usize, IoError>| {
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::<u64>();
// 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<usize, IoError>| {
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<i128>,
finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
) -> 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<i128>,
finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
) -> 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(())
+37 -2
View File
@@ -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),
@@ -7,6 +7,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;
@@ -14,6 +15,8 @@
#[path = "../../utils/libc.rs"]
mod libc_utils;
use libc_utils::errno_result;
fn main() {
test_dup();
test_dup_stdout_stderr();
@@ -55,6 +58,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")]
@@ -852,3 +865,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::<libc::c_void>(), iov_len: 0 as libc::size_t },
libc::iovec {
iov_base: buffer1.as_mut_ptr().cast::<libc::c_void>(),
iov_len: buffer1.len() as libc::size_t,
},
libc::iovec {
iov_base: buffer2.as_mut_ptr().cast::<libc::c_void>(),
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::<libc::iovec>(), 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::<libc::c_void>(), iov_len: 0 as libc::size_t },
libc::iovec {
iov_base: buffer1.as_mut_ptr().cast::<libc::c_void>(),
iov_len: buffer1.len() as libc::size_t,
},
libc::iovec {
iov_base: buffer2.as_mut_ptr().cast::<libc::c_void>(),
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::<libc::c_void>(), iov_len: 0 as libc::size_t },
libc::iovec {
iov_base: buffer1.as_mut_ptr().cast::<libc::c_void>(),
iov_len: buffer1.len() as libc::size_t,
},
libc::iovec {
iov_base: buffer2.as_mut_ptr().cast::<libc::c_void>(),
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::<libc::iovec>(), 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::<libc::c_void>(), iov_len: 0 as libc::size_t },
libc::iovec {
iov_base: buffer1.as_mut_ptr().cast::<libc::c_void>(),
iov_len: buffer1.len() as libc::size_t,
},
libc::iovec {
iov_base: buffer2.as_mut_ptr().cast::<libc::c_void>(),
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)]);
}
@@ -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::<libc::c_void>(), iov_len: 0 as libc::size_t },
libc::iovec {
iov_base: buffer1.as_mut_ptr().cast::<libc::c_void>(),
iov_len: buffer1.len() as libc::size_t,
},
libc::iovec {
iov_base: buffer2.as_mut_ptr().cast::<libc::c_void>(),
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::<libc::c_void>(), iov_len: 0 as libc::size_t },
libc::iovec {
iov_base: buffer1.as_mut_ptr().cast::<libc::c_void>(),
iov_len: buffer1.len() as libc::size_t,
},
libc::iovec {
iov_base: buffer2.as_mut_ptr().cast::<libc::c_void>(),
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.
+86 -1
View File
@@ -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]);
}