Skip to main content

miri/shims/unix/linux_like/
eventfd.rs

1//! Linux `eventfd` implementation.
2use std::cell::{Cell, RefCell};
3use std::io;
4use std::io::ErrorKind;
5
6use crate::concurrency::VClock;
7use crate::shims::files::{FileDescription, FileDescriptionRef};
8use crate::shims::unix::UnixFileDescription;
9use crate::*;
10
11/// Maximum value that the eventfd counter can hold.
12const MAX_COUNTER: u64 = u64::MAX - 1;
13
14/// A kind of file descriptor created by `eventfd`.
15/// The `Event` type isn't currently written to by `eventfd`.
16/// The interface is meant to keep track of objects associated
17/// with a file descriptor. For more information see the man
18/// page below:
19///
20/// <https://man.netbsd.org/eventfd.2>
21#[derive(Debug)]
22struct EventFd {
23    /// The object contains an unsigned 64-bit integer (uint64_t) counter that is maintained by the
24    /// kernel. This counter is initialized with the value specified in the argument initval.
25    counter: Cell<u64>,
26    is_nonblock: bool,
27    clock: RefCell<VClock>,
28    /// A list of thread ids blocked on eventfd::read.
29    blocked_read_tid: RefCell<Vec<ThreadId>>,
30    /// A list of thread ids blocked on eventfd::write.
31    blocked_write_tid: RefCell<Vec<ThreadId>>,
32    /// State for being watched by epoll.
33    watched: ReadinessWatched,
34}
35
36impl FileDescription for EventFd {
37    fn name(&self) -> &'static str {
38        "event"
39    }
40
41    fn metadata<'tcx>(
42        &self,
43    ) -> InterpResult<'tcx, Either<io::Result<std::fs::Metadata>, &'static str>> {
44        // On Linux, eventfd is an "anonymous inode" reported as S_IFREG.
45        interp_ok(Either::Right("S_IFREG"))
46    }
47
48    /// Read the counter in the buffer and return the counter if succeeded.
49    fn read<'tcx>(
50        self: FileDescriptionRef<Self>,
51        _communicate_allowed: bool,
52        ptr: Pointer,
53        len: usize,
54        ecx: &mut MiriInterpCx<'tcx>,
55        finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
56    ) -> InterpResult<'tcx> {
57        // We're treating the buffer as a `u64`.
58        let ty = ecx.machine.layouts.u64;
59        // Check the size of slice, and return error only if the size of the slice < 8.
60        if len < ty.size.bytes_usize() {
61            return finish.call(ecx, Err(ErrorKind::InvalidInput.into()));
62        }
63
64        // Turn the pointer into a place at the right type.
65        let buf_place = ecx.ptr_to_mplace_unaligned(ptr, ty);
66
67        eventfd_read(buf_place, self, ecx, finish)
68    }
69
70    /// A write call adds the 8-byte integer value supplied in
71    /// its buffer (in native endianness) to the counter.  The maximum value that may be
72    /// stored in the counter is the largest unsigned 64-bit value
73    /// minus 1 (i.e., 0xfffffffffffffffe).  If the addition would
74    /// cause the counter's value to exceed the maximum, then the
75    /// write either blocks until a read is performed on the
76    /// file descriptor, or fails with the error EAGAIN if the
77    /// file descriptor has been made nonblocking.
78    ///
79    /// A write fails with the error EINVAL if the size of the
80    /// supplied buffer is less than 8 bytes, or if an attempt is
81    /// made to write the value 0xffffffffffffffff.
82    fn write<'tcx>(
83        self: FileDescriptionRef<Self>,
84        _communicate_allowed: bool,
85        ptr: Pointer,
86        len: usize,
87        ecx: &mut MiriInterpCx<'tcx>,
88        finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
89    ) -> InterpResult<'tcx> {
90        // We're treating the buffer as a `u64`.
91        let ty = ecx.machine.layouts.u64;
92        // Check the size of slice, and return error if the size is wrong. The docs say we only
93        // error when the size is too small, but Linux seems to also error when the size is too big.
94        if len != ty.layout.size.bytes_usize() {
95            return finish.call(ecx, Err(ErrorKind::InvalidInput.into()));
96        }
97
98        // Turn the pointer into a place at the right type.
99        let buf_place = ecx.ptr_to_mplace_unaligned(ptr, ty);
100
101        eventfd_write(buf_place, self, ecx, finish)
102    }
103
104    fn readiness_watched(&self) -> Option<&ReadinessWatched> {
105        Some(&self.watched)
106    }
107
108    fn readiness(&self) -> Readiness {
109        // We only check the "readable" and "writable" readiness for eventfd. If other event flags
110        // need to be supported in the future, the check should be added here.
111
112        Readiness {
113            readable: self.counter.get() != 0,
114            writable: self.counter.get() != MAX_COUNTER,
115            ..Readiness::EMPTY
116        }
117    }
118
119    fn as_unix(self: FileDescriptionRef<Self>) -> FileDescriptionRef<dyn UnixFileDescription> {
120        self
121    }
122}
123
124impl UnixFileDescription for EventFd {}
125
126impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
127pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
128    /// This function creates an `Event` that is used as an event wait/notify mechanism by
129    /// user-space applications, and by the kernel to notify user-space applications of events.
130    /// The `Event` contains an `u64` counter maintained by the kernel. The counter is initialized
131    /// with the value specified in the `initval` argument.
132    ///
133    /// A new file descriptor referring to the `Event` is returned. The `read`, `write`, `poll`,
134    /// `select`, and `close` operations can be performed on the file descriptor. For more
135    /// information on these operations, see the man page linked below.
136    ///
137    /// The `flags` are not currently implemented for eventfd.
138    /// The `flags` may be bitwise ORed to change the behavior of `eventfd`:
139    /// `EFD_CLOEXEC` - Set the close-on-exec (`FD_CLOEXEC`) flag on the new file descriptor.
140    /// `EFD_NONBLOCK` - Set the `O_NONBLOCK` file status flag on the new open file description.
141    /// `EFD_SEMAPHORE` - miri does not support semaphore-like semantics.
142    ///
143    /// <https://linux.die.net/man/2/eventfd>
144    fn eventfd(&mut self, val: &OpTy<'tcx>, flags: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
145        let this = self.eval_context_mut();
146
147        let val = this.read_scalar(val)?.to_u32()?;
148        let mut flags = this.read_scalar(flags)?.to_i32()?;
149
150        let efd_cloexec = this.eval_libc_i32("EFD_CLOEXEC");
151        let efd_nonblock = this.eval_libc_i32("EFD_NONBLOCK");
152        let efd_semaphore = this.eval_libc_i32("EFD_SEMAPHORE");
153
154        if flags & efd_semaphore == efd_semaphore {
155            throw_unsup_format!("eventfd: EFD_SEMAPHORE is unsupported");
156        }
157
158        let mut is_nonblock = false;
159        // Unset the flag that we support.
160        // After unloading, flags != 0 means other flags are used.
161        if flags & efd_cloexec == efd_cloexec {
162            // cloexec is ignored because Miri does not support exec.
163            flags &= !efd_cloexec;
164        }
165        if flags & efd_nonblock == efd_nonblock {
166            flags &= !efd_nonblock;
167            is_nonblock = true;
168        }
169        if flags != 0 {
170            throw_unsup_format!("eventfd: encountered unknown unsupported flags {:#x}", flags);
171        }
172
173        let fds = &mut this.machine.fds;
174
175        let fd_value = fds.insert_new(EventFd {
176            counter: Cell::new(val.into()),
177            is_nonblock,
178            clock: RefCell::new(VClock::default()),
179            blocked_read_tid: RefCell::new(Vec::new()),
180            blocked_write_tid: RefCell::new(Vec::new()),
181            watched: ReadinessWatched::default(),
182        });
183
184        interp_ok(Scalar::from_i32(fd_value))
185    }
186}
187
188/// Block thread if the value addition will exceed u64::MAX -1,
189/// else just add the user-supplied value to current counter.
190fn eventfd_write<'tcx>(
191    buf_place: MPlaceTy<'tcx>,
192    eventfd: FileDescriptionRef<EventFd>,
193    ecx: &mut MiriInterpCx<'tcx>,
194    finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
195) -> InterpResult<'tcx> {
196    // Figure out which value we should add.
197    let num = ecx.read_scalar(&buf_place)?.to_u64()?;
198    // u64::MAX as input is invalid because the maximum value of counter is u64::MAX - 1.
199    if num == u64::MAX {
200        return finish.call(ecx, Err(ErrorKind::InvalidInput.into()));
201    }
202
203    match eventfd.counter.get().checked_add(num) {
204        Some(new_count @ 0..=MAX_COUNTER) => {
205            // Future `read` calls will synchronize with this write, so update the FD clock.
206            ecx.release_clock(|clock| {
207                eventfd.clock.borrow_mut().join(clock);
208            })?;
209
210            // Store new counter value.
211            eventfd.counter.set(new_count);
212
213            // Unblock *all* threads previously blocked on `read`.
214            // We need to take out the blocked thread ids and unblock them together,
215            // because `unblock_threads` may block them again and end up re-adding the
216            // thread to the blocked list.
217            let waiting_threads = std::mem::take(&mut *eventfd.blocked_read_tid.borrow_mut());
218            // FIXME: We can randomize the order of unblocking.
219            for thread_id in waiting_threads {
220                ecx.unblock_thread(thread_id, BlockReason::Eventfd)?;
221            }
222
223            // The state changed; we check and update the status of all supported event
224            // types for current file description.
225            // Linux seems to cause spurious wakeups here, and Tokio seems to rely on that
226            // (see <https://github.com/rust-lang/miri/pull/4676#discussion_r2510528994>
227            // and also <https://www.illumos.org/issues/16700>).
228            ecx.update_fd_readiness(eventfd, ReadinessUpdateFlags::FORCE_EDGE)?;
229
230            // Return how many bytes we consumed from the user-provided buffer.
231            return finish.call(ecx, Ok(buf_place.layout.size.bytes_usize()));
232        }
233        None | Some(u64::MAX) => {
234            // We can't update the state, so we have to block.
235            if eventfd.is_nonblock {
236                return finish.call(ecx, Err(ErrorKind::WouldBlock.into()));
237            }
238
239            eventfd.blocked_write_tid.borrow_mut().push(ecx.active_thread());
240
241            ecx.block_thread(
242                BlockReason::Eventfd,
243                None,
244                callback!(
245                    @capture<'tcx> {
246                        num: u64,
247                        buf_place: MPlaceTy<'tcx>,
248                        finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
249                        eventfd: FileDescriptionRef<EventFd>,
250                    }
251                    |this, unblock: UnblockKind| {
252                        assert_eq!(unblock, UnblockKind::Ready);
253                        eventfd_write(buf_place, eventfd, this, finish)
254                    }
255                ),
256            );
257        }
258    };
259    interp_ok(())
260}
261
262/// Block thread if the current counter is 0,
263/// else just return the current counter value to the caller and set the counter to 0.
264fn eventfd_read<'tcx>(
265    buf_place: MPlaceTy<'tcx>,
266    eventfd: FileDescriptionRef<EventFd>,
267    ecx: &mut MiriInterpCx<'tcx>,
268    finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
269) -> InterpResult<'tcx> {
270    // Set counter to 0, get old value.
271    let counter = eventfd.counter.replace(0);
272
273    // Block when counter == 0.
274    if counter == 0 {
275        if eventfd.is_nonblock {
276            return finish.call(ecx, Err(ErrorKind::WouldBlock.into()));
277        }
278
279        eventfd.blocked_read_tid.borrow_mut().push(ecx.active_thread());
280
281        ecx.block_thread(
282            BlockReason::Eventfd,
283            None,
284            callback!(
285                @capture<'tcx> {
286                    buf_place: MPlaceTy<'tcx>,
287                    finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
288                    eventfd: FileDescriptionRef<EventFd>,
289                }
290                |this, unblock: UnblockKind| {
291                    assert_eq!(unblock, UnblockKind::Ready);
292                    eventfd_read(buf_place, eventfd, this, finish)
293                }
294            ),
295        );
296    } else {
297        // Synchronize with all prior `write` calls to this FD.
298        ecx.acquire_clock(&eventfd.clock.borrow())?;
299
300        // Return old counter value into user-space buffer.
301        ecx.write_int(counter, &buf_place)?;
302
303        // Unblock *all* threads previously blocked on `write`.
304        // We need to take out the blocked thread ids and unblock them together,
305        // because `unblock_threads` may block them again and end up re-adding the
306        // thread to the blocked list.
307        let waiting_threads = std::mem::take(&mut *eventfd.blocked_write_tid.borrow_mut());
308        // FIXME: We can randomize the order of unblocking.
309        for thread_id in waiting_threads {
310            ecx.unblock_thread(thread_id, BlockReason::Eventfd)?;
311        }
312
313        // The state changed; we check and update the status of all supported event
314        // types for current file description.
315        // Linux seems to always emit do notifications here, even if we were already writable.
316        ecx.update_fd_readiness(eventfd, ReadinessUpdateFlags::FORCE_EDGE)?;
317
318        // Tell userspace how many bytes we put into the buffer.
319        return finish.call(ecx, Ok(buf_place.layout.size.bytes_usize()));
320    }
321    interp_ok(())
322}