Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add eventfd shim #3650

Merged
merged 1 commit into from
Jun 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
tiif marked this conversation as resolved.
Show resolved Hide resolved

/// 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.
tiif marked this conversation as resolved.
Show resolved Hide resolved
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.
tiif marked this conversation as resolved.
Show resolved Hide resolved
self.clock.join(&ecx.release_clock().unwrap());
tiif marked this conversation as resolved.
Show resolved Hide resolved
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.
tiif marked this conversation as resolved.
Show resolved Hide resolved
if flags & efd_cloexec == efd_cloexec {
// cloexec does nothing as we don't support `exec`
flags &= !efd_cloexec;
tiif marked this conversation as resolved.
Show resolved Hide resolved
}
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));
tiif marked this conversation as resolved.
Show resolved Hide resolved
}

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
tiif marked this conversation as resolved.
Show resolved Hide resolved
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
tiif marked this conversation as resolved.
Show resolved Hide resolved
// value -1.
let size_7_data: [u8; 7] = [1; 7];
let res = write_bytes(fd, size_7_data);
assert_eq!(res, -1);
tiif marked this conversation as resolved.
Show resolved Hide resolved

// 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();
}