diff --git a/src/shims/unix/linux/eventfd.rs b/src/shims/unix/linux/eventfd.rs index 777142b25c..3080d5b8d0 100644 --- a/src/shims/unix/linux/eventfd.rs +++ b/src/shims/unix/linux/eventfd.rs @@ -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> { + // Check the size of slice, and return error only if the size of the slice < 8. + let Some(bytes) = bytes.first_chunk_mut::() 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> { - 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::() 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)) } } @@ -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)) } } diff --git a/tests/fail-dep/libc/libc_eventfd_read_block.rs b/tests/fail-dep/libc/libc_eventfd_read_block.rs new file mode 100644 index 0000000000..b24635f934 --- /dev/null +++ b/tests/fail-dep/libc/libc_eventfd_read_block.rs @@ -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 + }; +} diff --git a/tests/fail-dep/libc/libc_eventfd_read_block.stderr b/tests/fail-dep/libc/libc_eventfd_read_block.stderr new file mode 100644 index 0000000000..fdd0b4272c --- /dev/null +++ b/tests/fail-dep/libc/libc_eventfd_read_block.stderr @@ -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 + diff --git a/tests/fail-dep/libc/libc_eventfd_write_block.rs b/tests/fail-dep/libc/libc_eventfd_write_block.rs new file mode 100644 index 0000000000..32ca4a919f --- /dev/null +++ b/tests/fail-dep/libc/libc_eventfd_write_block.rs @@ -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 + }; +} diff --git a/tests/fail-dep/libc/libc_eventfd_write_block.stderr b/tests/fail-dep/libc/libc_eventfd_write_block.stderr new file mode 100644 index 0000000000..f12c0ddfb1 --- /dev/null +++ b/tests/fail-dep/libc/libc_eventfd_write_block.stderr @@ -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 + diff --git a/tests/pass-dep/libc/libc-eventfd.rs b/tests/pass-dep/libc/libc-eventfd.rs new file mode 100644 index 0000000000..6af195316e --- /dev/null +++ b/tests/pass-dep/libc/libc-eventfd.rs @@ -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(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(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(); +}