Add eventfd shim

This commit is contained in:
tiif 2024-06-08 08:58:47 +08:00
parent 4c18f2a4c0
commit a269cf5657
6 changed files with 261 additions and 23 deletions

View file

@ -1,14 +1,20 @@
//! Linux `eventfd` implementation.
//! Currently just a stub.
use std::io;
use std::io::{Error, ErrorKind};
use rustc_target::abi::Endian;
use crate::shims::unix::*;
use crate::*;
use crate::{concurrency::VClock, *};
use self::shims::unix::fd::FileDescriptor;
/// Minimum size of u8 array to hold u64 value.
const U64_MIN_ARRAY_SIZE: usize = 8;
/// Maximum value that the eventfd counter can hold.
const MAX_COUNTER: u64 = u64::MAX - 1;
/// A kind of file descriptor created by `eventfd`.
/// The `Event` type isn't currently written to by `eventfd`.
/// The interface is meant to keep track of objects associated
@ -20,7 +26,9 @@ use self::shims::unix::fd::FileDescriptor;
struct Event {
/// The object contains an unsigned 64-bit integer (uint64_t) counter that is maintained by the
/// kernel. This counter is initialized with the value specified in the argument initval.
val: u64,
counter: u64,
is_nonblock: bool,
clock: VClock,
}
impl FileDescription for Event {
@ -35,6 +43,38 @@ impl FileDescription for Event {
Ok(Ok(()))
}
/// Read the counter in the buffer and return the counter if succeeded.
fn read<'tcx>(
&mut self,
_communicate_allowed: bool,
bytes: &mut [u8],
ecx: &mut MiriInterpCx<'tcx>,
) -> InterpResult<'tcx, io::Result<usize>> {
// Check the size of slice, and return error only if the size of the slice < 8.
let Some(bytes) = bytes.first_chunk_mut::<U64_MIN_ARRAY_SIZE>() else {
return Ok(Err(Error::from(ErrorKind::InvalidInput)));
};
// Block when counter == 0.
if self.counter == 0 {
if self.is_nonblock {
return Ok(Err(Error::from(ErrorKind::WouldBlock)));
} else {
//FIXME: blocking is not supported
throw_unsup_format!("eventfd: blocking is unsupported");
}
} else {
// Prevent false alarm in data race detection when doing synchronisation via eventfd.
ecx.acquire_clock(&self.clock);
// Return the counter in the host endianness using the buffer provided by caller.
*bytes = match ecx.tcx.sess.target.endian {
Endian::Little => self.counter.to_le_bytes(),
Endian::Big => self.counter.to_be_bytes(),
};
self.counter = 0;
return Ok(Ok(U64_MIN_ARRAY_SIZE));
}
}
/// A write call adds the 8-byte integer value supplied in
/// its buffer (in native endianness) to the counter. The maximum value that may be
/// stored in the counter is the largest unsigned 64-bit value
@ -53,16 +93,37 @@ impl FileDescription for Event {
bytes: &[u8],
ecx: &mut MiriInterpCx<'tcx>,
) -> InterpResult<'tcx, io::Result<usize>> {
let bytes: [u8; 8] = bytes.try_into().unwrap(); // FIXME fail gracefully when this has the wrong size
// Convert from target endianness to host endianness.
let num = match ecx.tcx.sess.target.endian {
Endian::Little => u64::from_le_bytes(bytes),
Endian::Big => u64::from_be_bytes(bytes),
// Check the size of slice, and return error only if the size of the slice < 8.
let Some(bytes) = bytes.first_chunk::<U64_MIN_ARRAY_SIZE>() else {
return Ok(Err(Error::from(ErrorKind::InvalidInput)));
};
// FIXME handle blocking when addition results in exceeding the max u64 value
// or fail with EAGAIN if the file descriptor is nonblocking.
self.val = self.val.checked_add(num).unwrap();
Ok(Ok(8))
// Convert from bytes to int according to host endianness.
let num = match ecx.tcx.sess.target.endian {
Endian::Little => u64::from_le_bytes(*bytes),
Endian::Big => u64::from_be_bytes(*bytes),
};
// u64::MAX as input is invalid because the maximum value of counter is u64::MAX - 1.
if num == u64::MAX {
return Ok(Err(Error::from(ErrorKind::InvalidInput)));
}
// If the addition does not let the counter to exceed the maximum value, update the counter.
// Else, block.
match self.counter.checked_add(num) {
Some(new_count @ 0..=MAX_COUNTER) => {
// Prevent false alarm in data race detection when doing synchronisation via eventfd.
self.clock.join(&ecx.release_clock().unwrap());
self.counter = new_count;
}
None | Some(u64::MAX) => {
if self.is_nonblock {
return Ok(Err(Error::from(ErrorKind::WouldBlock)));
} else {
//FIXME: blocking is not supported
throw_unsup_format!("eventfd: blocking is unsupported");
}
}
};
Ok(Ok(U64_MIN_ARRAY_SIZE))
}
}
@ -87,27 +148,41 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
fn eventfd(&mut self, val: &OpTy<'tcx>, flags: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
let this = self.eval_context_mut();
// eventfd is Linux specific.
this.assert_target_os("linux", "eventfd");
let val = this.read_scalar(val)?.to_u32()?;
let flags = this.read_scalar(flags)?.to_i32()?;
let mut flags = this.read_scalar(flags)?.to_i32()?;
let efd_cloexec = this.eval_libc_i32("EFD_CLOEXEC");
let efd_nonblock = this.eval_libc_i32("EFD_NONBLOCK");
let efd_semaphore = this.eval_libc_i32("EFD_SEMAPHORE");
if flags & (efd_cloexec | efd_nonblock | efd_semaphore) != flags {
throw_unsup_format!("eventfd: flag {flags:#x} is unsupported");
}
if flags & efd_cloexec == efd_cloexec {
// cloexec does nothing as we don't support `exec`
}
if flags & efd_nonblock == efd_nonblock {
// FIXME remember the nonblock flag
}
if flags & efd_semaphore == efd_semaphore {
throw_unsup_format!("eventfd: EFD_SEMAPHORE is unsupported");
}
let fd = this.machine.fds.insert_fd(FileDescriptor::new(Event { val: val.into() }));
let mut is_nonblock = false;
// Unload the flag that we support.
// After unloading, flags != 0 means other flags are used.
if flags & efd_cloexec == efd_cloexec {
flags &= !efd_cloexec;
}
if flags & efd_nonblock == efd_nonblock {
flags &= !efd_nonblock;
is_nonblock = true;
}
if flags != 0 {
let einval = this.eval_libc("EINVAL");
this.set_last_error(einval)?;
return Ok(Scalar::from_i32(-1));
}
let fd = this.machine.fds.insert_fd(FileDescriptor::new(Event {
counter: val.into(),
is_nonblock,
clock: VClock::default(),
}));
Ok(Scalar::from_i32(fd))
}
}

View file

@ -0,0 +1,12 @@
//@ignore-target-windows: No eventfd on Windows
//@ignore-target-apple: No eventfd in macos
fn main() {
// eventfd read will block when EFD_NONBLOCK flag is clear and counter = 0.
// This will pass when blocking is implemented.
let flags = libc::EFD_CLOEXEC;
let fd = unsafe { libc::eventfd(0, flags) };
let mut buf: [u8; 8] = [0; 8];
let _res: i32 = unsafe {
libc::read(fd, buf.as_mut_ptr().cast(), buf.len() as libc::size_t).try_into().unwrap() //~ERROR: blocking is unsupported
};
}

View file

@ -0,0 +1,14 @@
error: unsupported operation: eventfd: blocking is unsupported
--> $DIR/libc_eventfd_read_block.rs:LL:CC
|
LL | libc::read(fd, buf.as_mut_ptr().cast(), buf.len() as libc::size_t).try_into().unwrap()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ eventfd: blocking is unsupported
|
= help: this is likely not a bug in the program; it indicates that the program performed an operation that Miri does not support
= note: BACKTRACE:
= note: inside `main` at $DIR/libc_eventfd_read_block.rs:LL:CC
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
error: aborting due to 1 previous error

View file

@ -0,0 +1,22 @@
//@ignore-target-windows: No eventfd on Windows
//@ignore-target-apple: No eventfd in macos
fn main() {
// eventfd write will block when EFD_NONBLOCK flag is clear
// and the addition caused counter to exceed u64::MAX - 1.
// This will pass when blocking is implemented.
let flags = libc::EFD_CLOEXEC;
let fd = unsafe { libc::eventfd(0, flags) };
// Write u64 - 1.
let mut sized_8_data: [u8; 8] = (u64::MAX - 1).to_ne_bytes();
let res: i64 = unsafe {
libc::write(fd, sized_8_data.as_ptr() as *const libc::c_void, 8).try_into().unwrap()
};
assert_eq!(res, 8);
// Write 1.
sized_8_data = 1_u64.to_ne_bytes();
// Write 1 to the counter.
let _res: i64 = unsafe {
libc::write(fd, sized_8_data.as_ptr() as *const libc::c_void, 8).try_into().unwrap() //~ERROR: blocking is unsupported
};
}

View file

@ -0,0 +1,14 @@
error: unsupported operation: eventfd: blocking is unsupported
--> $DIR/libc_eventfd_write_block.rs:LL:CC
|
LL | libc::write(fd, sized_8_data.as_ptr() as *const libc::c_void, 8).try_into().unwrap()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ eventfd: blocking is unsupported
|
= help: this is likely not a bug in the program; it indicates that the program performed an operation that Miri does not support
= note: BACKTRACE:
= note: inside `main` at $DIR/libc_eventfd_write_block.rs:LL:CC
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
error: aborting due to 1 previous error

View file

@ -0,0 +1,101 @@
//@ignore-target-windows: No eventfd in windows
//@ignore-target-apple: No eventfd in macos
// test_race depends on a deterministic schedule.
//@compile-flags: -Zmiri-preemption-rate=0
use std::thread;
fn main() {
test_read_write();
test_race();
}
fn read_bytes<const N: usize>(fd: i32, buf: &mut [u8; N]) -> i32 {
let res: i32 = unsafe { libc::read(fd, buf.as_mut_ptr().cast(), N).try_into().unwrap() };
return res;
}
fn write_bytes<const N: usize>(fd: i32, data: [u8; N]) -> i32 {
let res: i32 =
unsafe { libc::write(fd, data.as_ptr() as *const libc::c_void, N).try_into().unwrap() };
return res;
}
fn test_read_write() {
let flags = libc::EFD_NONBLOCK | libc::EFD_CLOEXEC;
let fd = unsafe { libc::eventfd(0, flags) };
let sized_8_data: [u8; 8] = 1_u64.to_ne_bytes();
// Write 1 to the counter.
let res = write_bytes(fd, sized_8_data);
assert_eq!(res, 8);
// Read 1 from the counter.
let mut buf: [u8; 8] = [0; 8];
let res = read_bytes(fd, &mut buf);
// Read returns number of bytes has been read, which is always 8.
assert_eq!(res, 8);
// Check the value of counter read.
let counter = u64::from_ne_bytes(buf);
assert_eq!(counter, 1);
// After read, the counter is currently 0, read counter 0 should fail with return
// value -1.
let mut buf: [u8; 8] = [0; 8];
let res = read_bytes(fd, &mut buf);
assert_eq!(res, -1);
// Write with supplied buffer that > 8 bytes should be allowed.
let sized_9_data: [u8; 9];
if cfg!(target_endian = "big") {
// Adjust the data based on the endianness of host system.
sized_9_data = [0, 0, 0, 0, 0, 0, 0, 1, 0];
} else {
sized_9_data = [1, 0, 0, 0, 0, 0, 0, 0, 0];
}
let res = write_bytes(fd, sized_9_data);
assert_eq!(res, 8);
// Read with supplied buffer that < 8 bytes should fail with return
// value -1.
let mut buf: [u8; 7] = [1; 7];
let res = read_bytes(fd, &mut buf);
assert_eq!(res, -1);
// Write with supplied buffer that < 8 bytes should fail with return
// value -1.
let size_7_data: [u8; 7] = [1; 7];
let res = write_bytes(fd, size_7_data);
assert_eq!(res, -1);
// Read with supplied buffer > 8 bytes should be allowed.
let mut buf: [u8; 9] = [1; 9];
let res = read_bytes(fd, &mut buf);
assert_eq!(res, 8);
// Write u64::MAX should fail.
let u64_max_bytes: [u8; 8] = [255; 8];
let res = write_bytes(fd, u64_max_bytes);
assert_eq!(res, -1);
}
fn test_race() {
static mut VAL: u8 = 0;
let flags = libc::EFD_NONBLOCK | libc::EFD_CLOEXEC;
let fd = unsafe { libc::eventfd(0, flags) };
let thread1 = thread::spawn(move || {
let mut buf: [u8; 8] = [0; 8];
let res = read_bytes(fd, &mut buf);
// read returns number of bytes has been read, which is always 8.
assert_eq!(res, 8);
let counter = u64::from_ne_bytes(buf);
assert_eq!(counter, 1);
unsafe { assert_eq!(VAL, 1) };
});
unsafe { VAL = 1 };
let data: [u8; 8] = 1_u64.to_ne_bytes();
let res = write_bytes(fd, data);
// write returns number of bytes written, which is always 8.
assert_eq!(res, 8);
thread::yield_now();
thread1.join().unwrap();
}