Skip to content

Commit

Permalink
Add support for libc::readv
Browse files Browse the repository at this point in the history
- This enables vectorized reads.

Signed-off-by: shamb0 <[email protected]>
  • Loading branch information
shamb0 committed Dec 21, 2024
1 parent 601b250 commit 222bac2
Show file tree
Hide file tree
Showing 6 changed files with 802 additions and 0 deletions.
15 changes: 15 additions & 0 deletions src/shims/files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,21 @@ pub trait FileDescription: std::fmt::Debug + Any {
throw_unsup_format!("cannot read from {}", self.name());
}

/// Reads data directly into a byte buffer without using Miri's pointer abstraction.
/// This function provides an alternative to `read()` that works with byte slices,
/// making it particularly suitable for operations requiring atomic reads or
/// direct buffer manipulation.
fn read_buffer<'tcx>(
&self,
_self_ref: &FileDescriptionRef,
_communicate_allowed: bool,
_buf: &mut [u8],
_dest: &MPlaceTy<'tcx>,
_ecx: &mut MiriInterpCx<'tcx>,
) -> InterpResult<'tcx> {
throw_unsup_format!("cannot read from {}", self.name());
}

/// Writes as much as possible from the given buffer `ptr`.
/// `len` indicates how many bytes we should try to write.
/// `dest` is where the return value should be stored: number of bytes written, or `-1` in case of error.
Expand Down
245 changes: 245 additions & 0 deletions src/shims/unix/fd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::io;
use std::io::ErrorKind;

use rustc_abi::Size;
use rustc_middle::ty::layout::TyAndLayout;

use crate::helpers::check_min_arg_count;
use crate::shims::files::FileDescription;
Expand Down Expand Up @@ -257,6 +258,250 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
interp_ok(())
}

/// Reads data from a file descriptor into multiple buffers atomically (vectored I/O).
///
/// This implementation follows POSIX readv() semantics, reading data from the file descriptor
/// specified by `fd_num` into the buffers described by the array of iovec structures pointed
/// to by `iov_ptr`. The `iovcnt` argument specifies the number of iovec structures in the array.
///
/// # Arguments
/// * `fd_num` - The file descriptor to read from
/// * `iov_ptr` - Pointer to an array of iovec structures
/// * `iovcnt` - Number of iovec structures in the array
/// * `dest` - Destination for storing the number of bytes read
///
/// # Returns
/// * `Ok(())` - Operation completed successfully, with total bytes read stored in `dest`
/// * `Err(_)` - Operation failed with appropriate errno set
///
/// # Errors
/// * `EBADF` - `fd_num` is not a valid file descriptor
/// * `EFAULT` - Part of iovec array or buffers point outside accessible address space
/// * `EINVAL` - `iovcnt` is negative or exceeds system limit
/// * `EIO` - I/O error occurred while reading from the file descriptor
///
/// # POSIX Compliance
/// Implements standard POSIX readv() behavior:
/// * Performs reads atomically with respect to other threads
/// * Returns exact number of bytes read or -1 on error
/// * Handles partial reads and end-of-file conditions
/// * Respects system-imposed limits on total transfer size
fn readv(
&mut self,
fd_num: i32,
iov_ptr: &OpTy<'tcx>,
iovcnt: i32,
dest: &MPlaceTy<'tcx>,
) -> InterpResult<'tcx> {
let this = self.eval_context_mut();

// POSIX Compliance: Must handle negative values (EINVAL).
if iovcnt < 0 {
return this.set_last_error_and_return(LibcError("EINVAL"), dest);
}

// POSIX Compliance: Must handle zero properly.
if iovcnt == 0 {
return this.write_scalar(Scalar::from_i32(0), dest);
}

// POSIX Compliance: Check if iovcnt exceeds system limits.
// Most implementations limit this to IOV_MAX
// Common system default
// https://github.com/turbolent/w2c2/blob/d94227c22f8d78a04fbad70fa744481ca4a1912e/examples/clang/sys/include/limits.h#L50
const IOV_MAX: i32 = 1024;
if iovcnt > IOV_MAX {
trace!("readv: iovcnt exceeds IOV_MAX");
return this.set_last_error_and_return(LibcError("EINVAL"), dest);
}

// POSIX Compliance: Validate file descriptor.
let Some(fd) = this.machine.fds.get(fd_num) else {
return this.set_last_error_and_return(LibcError("EBADF"), dest);
};

// Convert iovcnt to usize for array indexing.
let iovcnt = usize::try_from(iovcnt).expect("iovcnt exceeds platform size");
let iovec_layout = this.libc_ty_layout("iovec");

// Gather iovec information.
// Pre-allocate vectors for iovec information
let mut iov_info = Vec::with_capacity(iovcnt);
let mut total_size: u64 = 0;

// POSIX Compliance: Validate each iovec structure.
// Must check for EFAULT (invalid buffer addresses) and EINVAL (invalid length).
for i in 0..iovcnt {
// Calculate offset to current iovec structure.
let offset = iovec_layout
.size
.bytes()
.checked_mul(i as u64)
.expect("iovec array index overflow");

// Access current iovec structure.
let current_iov = match this
.deref_pointer_and_offset_vectored(
iov_ptr,
offset,
iovec_layout,
iovcnt,
iovec_layout,
)
.report_err()
{
Ok(iov) => iov,
Err(_) => {
return this.set_last_error_and_return(LibcError("EFAULT"), dest);
}
};

// Extract and validate buffer pointer and length.
let base_field = this.project_field_named(&current_iov, "iov_base")?;
let base = this.read_pointer(&base_field)?;

let len_field = this.project_field_named(&current_iov, "iov_len")?;
let len = this.read_target_usize(&len_field)?;

// Validate buffer alignment and accessibility.
if this
.check_ptr_access(base, Size::from_bytes(len), CheckInAllocMsg::MemoryAccessTest)
.report_err()
.is_err()
{
return this.set_last_error_and_return(LibcError("EFAULT"), dest);
}

// Update total size safely.
total_size = match total_size.checked_add(len) {
Some(new_size) => new_size,
None => {
return this.set_last_error_and_return(LibcError("EINVAL"), dest);
}
};

iov_info.push((base, len));
}

// Cap total size to platform limits.
let total_size =
total_size.min(u64::try_from(this.target_isize_max()).unwrap()).min(isize::MAX as u64);

// Early return for zero total size
if total_size == 0 {
return this.write_scalar(Scalar::from_i32(0), dest);
}

let mut temp_buf: Vec<u8> = vec![0; total_size.try_into().unwrap()];

// Perform single atomic read operation
let read_result =
fd.read_buffer(&fd, this.machine.communicate(), &mut temp_buf[..], dest, this);

// Handle read result
if read_result.report_err().is_err() {
return this.set_last_error_and_return(LibcError("EIO"), dest);
}

// Get bytes read from dest and convert to usize for slice operations.
let read_bytes: usize = this
.read_target_usize(dest)?
.try_into()
.expect("read bytes count exceeds platform usize");

if read_bytes > 0 {
// Copy data to individual iovec buffers.
let mut current_pos = 0usize;

for (base, len) in iov_info {
// Early exit if no more bytes to copy.
if current_pos >= read_bytes {
break;
}

// Convert len to usize safely and handle potential overflow.
let buffer_len = usize::try_from(len).expect("buffer length exceeds platform size");

// Calculate remaining bytes safely.
let bytes_remaining =
read_bytes.checked_sub(current_pos).expect("position calculation underflow");

// Calculate copy size as minimum of buffer length and remaining bytes.
let copy_size = buffer_len.min(bytes_remaining);

// Calculate end position safely.
let slice_end =
current_pos.checked_add(copy_size).expect("slice end calculation overflow");

// Verify slice bounds.
if slice_end > temp_buf.len() {
return this.set_last_error_and_return(LibcError("EFAULT"), dest);
}

// Write the slice to the destination buffer.
if this
.write_bytes_ptr(base, temp_buf[current_pos..slice_end].iter().copied())
.report_err()
.is_err()
{
return this.set_last_error_and_return(LibcError("EIO"), dest);
}

// Update position safely.
current_pos = slice_end;
}
}

interp_ok(())
}

/// Dereferences a pointer to access an element within a source array, with specialized bounds checking
/// for vectored I/O operations like readv().
///
/// This function provides array-aware bounds checking that is specifically designed for situations
/// where we need to access multiple independent memory regions, such as when processing an array
/// of iovec structures. Unlike simple pointer arithmetic bounds checking, this implementation
/// understands and validates array-based access patterns.
fn deref_pointer_and_offset_vectored(
&self,
op: &impl Projectable<'tcx, Provenance>,
offset_bytes: u64,
base_layout: TyAndLayout<'tcx>,
count: usize,
value_layout: TyAndLayout<'tcx>,
) -> InterpResult<'tcx, MPlaceTy<'tcx>> {
// 1. Validate the iovec array bounds.
let array_size = base_layout
.size
.bytes()
.checked_mul(count as u64)
.ok_or_else(|| err_ub_format!("iovec array size overflow"))?;

// 2. Check if our offset is within the array.
if offset_bytes >= array_size {
throw_ub_format!(
"{}",
format!(
"iovec array access out of bounds: offset {} in array of size {}",
offset_bytes, array_size
)
);
}

// 3. Ensure the iovec structure we're accessing is fully contained.
if offset_bytes.checked_add(base_layout.size.bytes()).is_none_or(|end| end > array_size) {
throw_ub_format!("iovec structure would extend past array bounds");
}

// 4. Proceed with the dereferencing.
let this = self.eval_context_ref();
let op_place = this.deref_pointer_as(op, base_layout)?;
let offset = Size::from_bytes(offset_bytes);

let value_place = op_place.offset(offset, value_layout, this)?;
interp_ok(value_place)
}

fn write(
&mut self,
fd_num: i32,
Expand Down
6 changes: 6 additions & 0 deletions src/shims/unix/foreign_items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,12 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
let count = this.read_target_usize(count)?;
this.read(fd, buf, count, None, dest)?;
}
"readv" => {
let [fd, iov, iovcnt] = this.check_shim(abi, ExternAbi::C { unwind: false }, link_name, args)?;
let fd = this.read_scalar(fd)?.to_i32()?;
let iovcnt = this.read_scalar(iovcnt)?.to_i32()?;
this.readv(fd, iov, iovcnt, dest)?;
}
"write" => {
let [fd, buf, n] = this.check_shim(abi, Conv::C , link_name, args)?;
let fd = this.read_scalar(fd)?.to_i32()?;
Expand Down
31 changes: 31 additions & 0 deletions src/shims/unix/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,37 @@ impl FileDescription for FileHandle {
}
}

/// Reads data directly into a byte buffer without using Miri's pointer abstraction.
/// This function provides an alternative to `read()` that works with byte slices,
/// making it particularly suitable for operations requiring atomic reads or
/// direct buffer manipulation.
fn read_buffer<'tcx>(
&self,
_self_ref: &FileDescriptionRef,
communicate_allowed: bool,
buf: &mut [u8],
dest: &MPlaceTy<'tcx>,
ecx: &mut MiriInterpCx<'tcx>,
) -> InterpResult<'tcx> {
let this = ecx.eval_context_mut();

// Check isolation mode
if !communicate_allowed {
helpers::isolation_abort_error("`read` operation")?;
}

// Perform read operation
let result = (&mut &self.file).read(buf);

match result {
Ok(read_size) => {
this.write_int(u64::try_from(read_size).unwrap(), dest)?;
interp_ok(())
}
Err(e) => this.set_last_error_and_return(e, dest),
}
}

fn write<'tcx>(
&self,
_self_ref: &FileDescriptionRef,
Expand Down
Loading

0 comments on commit 222bac2

Please sign in to comment.