Skip to content

Commit

Permalink
Auto merge of #3650 - tiif:feat/eventfd, r=oli-obk
Browse files Browse the repository at this point in the history
Add eventfd shim

Fixes #3445

Design docs: https://hackmd.io/`@tiif/rk9hlmP4R`
  • Loading branch information
bors committed Jun 8, 2024
2 parents b3ed85b + 2f206fa commit c51b733
Show file tree
Hide file tree
Showing 6 changed files with 257 additions and 19 deletions.
113 changes: 94 additions & 19 deletions src/shims/unix/linux/eventfd.rs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 {
Expand All @@ -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
Expand All @@ -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.
// 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)));
};
// 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),
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");
}
}
};
// 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))
Ok(Ok(U64_MIN_ARRAY_SIZE))
}
}

Expand All @@ -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_semaphore == efd_semaphore {
throw_unsup_format!("eventfd: EFD_SEMAPHORE is unsupported");
}

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 {
// cloexec does nothing as we don't support `exec`
flags &= !efd_cloexec;
}
if flags & efd_nonblock == efd_nonblock {
// FIXME remember the nonblock flag
flags &= !efd_nonblock;
is_nonblock = true;
}
if flags & efd_semaphore == efd_semaphore {
throw_unsup_format!("eventfd: EFD_SEMAPHORE is unsupported");
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 { val: val.into() }));
let fd = this.machine.fds.insert_fd(FileDescriptor::new(Event {
counter: val.into(),
is_nonblock,
clock: VClock::default(),
}));
Ok(Scalar::from_i32(fd))
}
}
12 changes: 12 additions & 0 deletions tests/fail-dep/libc/libc_eventfd_read_block.rs
Original file line number Diff line number Diff line change
@@ -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
};
}
14 changes: 14 additions & 0 deletions tests/fail-dep/libc/libc_eventfd_read_block.stderr
Original file line number Diff line number Diff line change
@@ -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

22 changes: 22 additions & 0 deletions tests/fail-dep/libc/libc_eventfd_write_block.rs
Original file line number Diff line number Diff line change
@@ -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
};
}
14 changes: 14 additions & 0 deletions tests/fail-dep/libc/libc_eventfd_write_block.stderr
Original file line number Diff line number Diff line change
@@ -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

101 changes: 101 additions & 0 deletions tests/pass-dep/libc/libc-eventfd.rs
Original file line number Diff line number Diff line change
@@ -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();
}

0 comments on commit c51b733

Please sign in to comment.