Skip to main content

miri/shims/unix/linux_like/
epoll.rs

1use std::cell::RefCell;
2use std::collections::{BTreeMap, BTreeSet, VecDeque};
3use std::io;
4use std::ops::Bound;
5use std::time::Duration;
6
7use rustc_abi::FieldIdx;
8
9use crate::concurrency::VClock;
10use crate::shims::files::{
11    DynFileDescriptionRef, FdId, FdNum, FileDescription, FileDescriptionRef, WeakFileDescriptionRef,
12};
13use crate::shims::unix::UnixFileDescription;
14use crate::*;
15
16type EpollEventKey = (FdId, FdNum);
17
18/// An `Epoll` file descriptor connects file handles and epoll events
19#[derive(Debug, Default)]
20struct Epoll {
21    /// A map of EpollEventInterests registered under this epoll instance. Each entry is
22    /// differentiated using FdId and file descriptor value.
23    interest_list: RefCell<BTreeMap<EpollEventKey, EpollEventInterest>>,
24    /// The subset of interests that is currently considered "ready". Stored separately so we
25    /// can access it more efficiently.
26    ready_set: RefCell<BTreeSet<EpollEventKey>>,
27    /// The queue of threads blocked on this epoll instance.
28    queue: RefCell<VecDeque<ThreadId>>,
29}
30
31impl VisitProvenance for Epoll {
32    fn visit_provenance(&self, _visit: &mut VisitWith<'_>) {
33        // No provenance anywhere in this type.
34    }
35}
36
37/// Returns the range of all EpollEventKey for the given FD ID.
38fn range_for_id(id: FdId) -> std::ops::RangeInclusive<EpollEventKey> {
39    (id, 0)..=(id, i32::MAX)
40}
41
42/// Tracks the events that this epoll is interested in for a given file descriptor.
43#[derive(Debug)]
44pub struct EpollEventInterest {
45    /// The events bitmask the epoll is interested in.
46    relevant_events: u32,
47    /// The currently active events for this file descriptor.
48    active_events: u32,
49    /// The vector clock for wakeups.
50    clock: VClock,
51    /// User-defined data associated with this interest.
52    /// libc's data field in epoll_event can store integer or pointer,
53    /// but only u64 is supported for now.
54    /// <https://man7.org/linux/man-pages/man3/epoll_event.3type.html>
55    data: u64,
56}
57
58/// EpollReadyEvents reflects the readiness of a file description.
59#[derive(Debug)]
60pub struct EpollEvents {
61    /// The associated file is available for read(2) operations, in the sense that a read will not block.
62    /// (I.e., returning EOF is considered "ready".)
63    pub epollin: bool,
64    /// The associated file is available for write(2) operations, in the sense that a write will not block.
65    pub epollout: bool,
66    /// Stream socket peer closed connection, or shut down writing
67    /// half of connection.
68    pub epollrdhup: bool,
69    /// For stream socket, this event merely indicates that the peer
70    /// closed its end of the channel.
71    /// Unlike epollrdhup, this should only be set when the stream is fully closed.
72    /// epollrdhup also gets set when only the write half is closed, which is possible
73    /// via `shutdown(_, SHUT_WR)`.
74    pub epollhup: bool,
75    /// Error condition happened on the associated file descriptor.
76    pub epollerr: bool,
77}
78
79impl EpollEvents {
80    pub fn new() -> Self {
81        EpollEvents {
82            epollin: false,
83            epollout: false,
84            epollrdhup: false,
85            epollhup: false,
86            epollerr: false,
87        }
88    }
89
90    pub fn get_event_bitmask<'tcx>(&self, ecx: &MiriInterpCx<'tcx>) -> u32 {
91        let epollin = ecx.eval_libc_u32("EPOLLIN");
92        let epollout = ecx.eval_libc_u32("EPOLLOUT");
93        let epollrdhup = ecx.eval_libc_u32("EPOLLRDHUP");
94        let epollhup = ecx.eval_libc_u32("EPOLLHUP");
95        let epollerr = ecx.eval_libc_u32("EPOLLERR");
96
97        let mut bitmask = 0;
98        if self.epollin {
99            bitmask |= epollin;
100        }
101        if self.epollout {
102            bitmask |= epollout;
103        }
104        if self.epollrdhup {
105            bitmask |= epollrdhup;
106        }
107        if self.epollhup {
108            bitmask |= epollhup;
109        }
110        if self.epollerr {
111            bitmask |= epollerr;
112        }
113        bitmask
114    }
115}
116
117impl FileDescription for Epoll {
118    fn name(&self) -> &'static str {
119        "epoll"
120    }
121
122    fn metadata<'tcx>(
123        &self,
124    ) -> InterpResult<'tcx, Either<io::Result<std::fs::Metadata>, &'static str>> {
125        // On Linux, epoll is an "anonymous inode" reported as S_IFREG.
126        interp_ok(Either::Right("S_IFREG"))
127    }
128
129    fn destroy<'tcx>(
130        mut self,
131        self_id: FdId,
132        _communicate_allowed: bool,
133        ecx: &mut MiriInterpCx<'tcx>,
134    ) -> InterpResult<'tcx, io::Result<()>> {
135        // If we were interested in some FDs, we can remove that now.
136        let mut ids = self.interest_list.get_mut().keys().map(|(id, _num)| *id).collect::<Vec<_>>();
137        ids.dedup(); // they come out of the map sorted
138        for id in ids {
139            ecx.machine.epoll_interests.remove(id, self_id);
140        }
141        interp_ok(Ok(()))
142    }
143
144    fn as_unix<'tcx>(&self, _ecx: &MiriInterpCx<'tcx>) -> &dyn UnixFileDescription {
145        self
146    }
147}
148
149impl UnixFileDescription for Epoll {}
150
151/// The table of all EpollEventInterest.
152/// This tracks, for each file description, which epoll instances have an interest in events
153/// for this file description. The `FdId` is the ID of the epoll instance, so that we can recognize
154/// it later when it is slated for removal. The vector is sorted by that ID.
155pub struct EpollInterestTable(BTreeMap<FdId, Vec<(FdId, WeakFileDescriptionRef<Epoll>)>>);
156
157impl EpollInterestTable {
158    pub(crate) fn new() -> Self {
159        EpollInterestTable(BTreeMap::new())
160    }
161
162    fn insert(&mut self, id: FdId, epoll: &FileDescriptionRef<Epoll>) {
163        let epolls = self.0.entry(id).or_default();
164        let idx = epolls
165            .binary_search_by_key(&epoll.id(), |&(id, _)| id)
166            .expect_err("trying to add an epoll that's already in the list");
167        epolls.insert(idx, (epoll.id(), FileDescriptionRef::downgrade(epoll)));
168    }
169
170    fn remove(&mut self, id: FdId, epoll_id: FdId) {
171        let epolls = self.0.entry(id).or_default();
172        let idx = epolls
173            .binary_search_by_key(&epoll_id, |&(id, _)| id)
174            .expect("trying to remove an epoll that's not in the list");
175        epolls.remove(idx);
176    }
177
178    fn get_epolls(&self, id: FdId) -> Option<impl Iterator<Item = &WeakFileDescriptionRef<Epoll>>> {
179        self.0.get(&id).map(|epolls| epolls.iter().map(|(_id, epoll)| epoll))
180    }
181
182    pub fn remove_epolls(&mut self, id: FdId) {
183        if let Some(epolls) = self.0.remove(&id) {
184            for epoll in epolls.iter().filter_map(|(_id, epoll)| epoll.upgrade()) {
185                // This is a still-live epoll with interest in this FD. Remove all
186                // relevant interests (including from the ready set).
187                epoll
188                    .interest_list
189                    .borrow_mut()
190                    .extract_if(range_for_id(id), |_, _| true)
191                    // Consume the iterator.
192                    .for_each(drop);
193                epoll
194                    .ready_set
195                    .borrow_mut()
196                    .extract_if(range_for_id(id), |_| true)
197                    // Consume the iterator.
198                    .for_each(drop);
199            }
200        }
201    }
202}
203
204impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
205pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
206    /// This function returns a file descriptor referring to the new `Epoll` instance. This file
207    /// descriptor is used for all subsequent calls to the epoll interface. If the `flags` argument
208    /// is 0, then this function is the same as `epoll_create()`.
209    ///
210    /// <https://linux.die.net/man/2/epoll_create1>
211    fn epoll_create1(&mut self, flags: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
212        let this = self.eval_context_mut();
213
214        let flags = this.read_scalar(flags)?.to_i32()?;
215
216        let epoll_cloexec = this.eval_libc_i32("EPOLL_CLOEXEC");
217
218        // Miri does not support exec, so EPOLL_CLOEXEC flag has no effect.
219        if flags != epoll_cloexec && flags != 0 {
220            throw_unsup_format!(
221                "epoll_create1: flag {:#x} is unsupported, only 0 or EPOLL_CLOEXEC are allowed",
222                flags
223            );
224        }
225
226        let fd = this.machine.fds.insert_new(Epoll::default());
227        interp_ok(Scalar::from_i32(fd))
228    }
229
230    /// This function performs control operations on the `Epoll` instance referred to by the file
231    /// descriptor `epfd`. It requests that the operation `op` be performed for the target file
232    /// descriptor, `fd`.
233    ///
234    /// Valid values for the op argument are:
235    /// `EPOLL_CTL_ADD` - Register the target file descriptor `fd` on the `Epoll` instance referred
236    /// to by the file descriptor `epfd` and associate the event `event` with the internal file
237    /// linked to `fd`.
238    /// `EPOLL_CTL_MOD` - Change the event `event` associated with the target file descriptor `fd`.
239    /// `EPOLL_CTL_DEL` - Deregister the target file descriptor `fd` from the `Epoll` instance
240    /// referred to by `epfd`. The `event` is ignored and can be null.
241    ///
242    /// <https://linux.die.net/man/2/epoll_ctl>
243    fn epoll_ctl(
244        &mut self,
245        epfd: &OpTy<'tcx>,
246        op: &OpTy<'tcx>,
247        fd: &OpTy<'tcx>,
248        event: &OpTy<'tcx>,
249    ) -> InterpResult<'tcx, Scalar> {
250        let this = self.eval_context_mut();
251
252        let epfd_value = this.read_scalar(epfd)?.to_i32()?;
253        let op = this.read_scalar(op)?.to_i32()?;
254        let fd = this.read_scalar(fd)?.to_i32()?;
255        let event = this.deref_pointer_as(event, this.libc_ty_layout("epoll_event"))?;
256
257        let epoll_ctl_add = this.eval_libc_i32("EPOLL_CTL_ADD");
258        let epoll_ctl_mod = this.eval_libc_i32("EPOLL_CTL_MOD");
259        let epoll_ctl_del = this.eval_libc_i32("EPOLL_CTL_DEL");
260        let epollin = this.eval_libc_u32("EPOLLIN");
261        let epollout = this.eval_libc_u32("EPOLLOUT");
262        let epollrdhup = this.eval_libc_u32("EPOLLRDHUP");
263        let epollet = this.eval_libc_u32("EPOLLET");
264        let epollhup = this.eval_libc_u32("EPOLLHUP");
265        let epollerr = this.eval_libc_u32("EPOLLERR");
266
267        // Throw EFAULT if epfd and fd have the same value.
268        if epfd_value == fd {
269            return this.set_last_error_and_return_i32(LibcError("EFAULT"));
270        }
271
272        // Check if epfd is a valid epoll file descriptor.
273        let Some(epfd) = this.machine.fds.get(epfd_value) else {
274            return this.set_last_error_and_return_i32(LibcError("EBADF"));
275        };
276        let epfd = epfd
277            .downcast::<Epoll>()
278            .ok_or_else(|| err_unsup_format!("non-epoll FD passed to `epoll_ctl`"))?;
279
280        let mut interest_list = epfd.interest_list.borrow_mut();
281
282        let Some(fd_ref) = this.machine.fds.get(fd) else {
283            return this.set_last_error_and_return_i32(LibcError("EBADF"));
284        };
285        let id = fd_ref.id();
286
287        if op == epoll_ctl_add || op == epoll_ctl_mod {
288            // Read event bitmask and data from epoll_event passed by caller.
289            let mut events =
290                this.read_scalar(&this.project_field(&event, FieldIdx::ZERO)?)?.to_u32()?;
291            let data = this.read_scalar(&this.project_field(&event, FieldIdx::ONE)?)?.to_u64()?;
292
293            // Unset the flag we support to discover if any unsupported flags are used.
294            let mut flags = events;
295            // epoll_wait(2) will always wait for epollhup and epollerr; it is not
296            // necessary to set it in events when calling epoll_ctl().
297            // So we will always set these two event types.
298            events |= epollhup;
299            events |= epollerr;
300
301            if events & epollet != epollet {
302                // We only support edge-triggered notification for now.
303                throw_unsup_format!("epoll_ctl: epollet flag must be included.");
304            } else {
305                flags &= !epollet;
306            }
307            if flags & epollin == epollin {
308                flags &= !epollin;
309            }
310            if flags & epollout == epollout {
311                flags &= !epollout;
312            }
313            if flags & epollrdhup == epollrdhup {
314                flags &= !epollrdhup;
315            }
316            if flags & epollhup == epollhup {
317                flags &= !epollhup;
318            }
319            if flags & epollerr == epollerr {
320                flags &= !epollerr;
321            }
322            if flags != 0 {
323                throw_unsup_format!(
324                    "epoll_ctl: encountered unknown unsupported flags {:#x}",
325                    flags
326                );
327            }
328
329            // Add new interest to list. Experiments show that we need to reset all state
330            // on `EPOLL_CTL_MOD`, including the edge tracking.
331            let epoll_key = (id, fd);
332            if op == epoll_ctl_add {
333                if interest_list.range(range_for_id(id)).next().is_none() {
334                    // This is the first time this FD got added to this epoll.
335                    // Remember that in the global list so we get notified about FD events.
336                    this.machine.epoll_interests.insert(id, &epfd);
337                }
338                let new_interest = EpollEventInterest {
339                    relevant_events: events,
340                    data,
341                    active_events: 0,
342                    clock: VClock::default(),
343                };
344                if interest_list.try_insert(epoll_key, new_interest).is_err() {
345                    // We already had interest in this.
346                    return this.set_last_error_and_return_i32(LibcError("EEXIST"));
347                }
348            } else {
349                // Modify the existing interest.
350                let Some(interest) = interest_list.get_mut(&epoll_key) else {
351                    return this.set_last_error_and_return_i32(LibcError("ENOENT"));
352                };
353                interest.relevant_events = events;
354                interest.data = data;
355            }
356
357            // Deliver events for the new interest.
358            update_readiness(
359                this,
360                &epfd,
361                fd_ref.as_unix(this).epoll_active_events()?.get_event_bitmask(this),
362                /* force_edge */ true,
363                move |callback| {
364                    // Need to release the RefCell when this closure returns, so we have to move
365                    // it into the closure, so we have to do a re-lookup here.
366                    callback(epoll_key, interest_list.get_mut(&epoll_key).unwrap())
367                },
368            )?;
369
370            interp_ok(Scalar::from_i32(0))
371        } else if op == epoll_ctl_del {
372            let epoll_key = (id, fd);
373
374            // Remove epoll_event_interest from interest_list and ready_set.
375            if interest_list.remove(&epoll_key).is_none() {
376                // We did not have interest in this.
377                return this.set_last_error_and_return_i32(LibcError("ENOENT"));
378            };
379            epfd.ready_set.borrow_mut().remove(&epoll_key);
380            // If this was the last interest in this FD, remove us from the global list
381            // of who is interested in this FD.
382            if interest_list.range(range_for_id(id)).next().is_none() {
383                this.machine.epoll_interests.remove(id, epfd.id());
384            }
385
386            interp_ok(Scalar::from_i32(0))
387        } else {
388            throw_unsup_format!("unsupported epoll_ctl operation: {op}");
389        }
390    }
391
392    /// The `epoll_wait()` system call waits for events on the `Epoll`
393    /// instance referred to by the file descriptor `epfd`. The buffer
394    /// pointed to by `events` is used to return information from the ready
395    /// list about file descriptors in the interest list that have some
396    /// events available. Up to `maxevents` are returned by `epoll_wait()`.
397    /// The `maxevents` argument must be greater than zero.
398    ///
399    /// The `timeout` argument specifies the number of milliseconds that
400    /// `epoll_wait()` will block. Time is measured against the
401    /// CLOCK_MONOTONIC clock. If the timeout is zero, the function will not block,
402    /// while if the timeout is -1, the function will block
403    /// until at least one event has been retrieved (or an error
404    /// occurred).
405    ///
406    /// A call to `epoll_wait()` will block until either:
407    /// • a file descriptor delivers an event;
408    /// • the call is interrupted by a signal handler; or
409    /// • the timeout expires.
410    ///
411    /// Note that the timeout interval will be rounded up to the system
412    /// clock granularity, and kernel scheduling delays mean that the
413    /// blocking interval may overrun by a small amount. Specifying a
414    /// timeout of -1 causes `epoll_wait()` to block indefinitely, while
415    /// specifying a timeout equal to zero cause `epoll_wait()` to return
416    /// immediately, even if no events are available.
417    ///
418    /// On success, `epoll_wait()` returns the number of file descriptors
419    /// ready for the requested I/O, or zero if no file descriptor became
420    /// ready during the requested timeout milliseconds. On failure,
421    /// `epoll_wait()` returns -1 and errno is set to indicate the error.
422    ///
423    /// <https://man7.org/linux/man-pages/man2/epoll_wait.2.html>
424    fn epoll_wait(
425        &mut self,
426        epfd: &OpTy<'tcx>,
427        events_op: &OpTy<'tcx>,
428        maxevents: &OpTy<'tcx>,
429        timeout: &OpTy<'tcx>,
430        dest: &MPlaceTy<'tcx>,
431    ) -> InterpResult<'tcx> {
432        let this = self.eval_context_mut();
433
434        let epfd_value = this.read_scalar(epfd)?.to_i32()?;
435        let events = this.read_immediate(events_op)?;
436        let maxevents = this.read_scalar(maxevents)?.to_i32()?;
437        let timeout = this.read_scalar(timeout)?.to_i32()?;
438
439        if epfd_value <= 0 || maxevents <= 0 {
440            return this.set_last_error_and_return(LibcError("EINVAL"), dest);
441        }
442
443        // This needs to come after the maxevents value check, or else maxevents.try_into().unwrap()
444        // will fail.
445        let event = this.deref_pointer_as(
446            &events,
447            this.libc_array_ty_layout("epoll_event", maxevents.try_into().unwrap()),
448        )?;
449
450        let Some(epfd) = this.machine.fds.get(epfd_value) else {
451            return this.set_last_error_and_return(LibcError("EBADF"), dest);
452        };
453        let Some(epfd) = epfd.downcast::<Epoll>() else {
454            return this.set_last_error_and_return(LibcError("EBADF"), dest);
455        };
456
457        if timeout == 0 || !epfd.ready_set.borrow().is_empty() {
458            // If the timeout is 0 or there is a ready event, we can return immediately.
459            return_ready_list(&epfd, dest, &event, this)?;
460        } else {
461            // Blocking
462            let timeout = match timeout {
463                0.. => {
464                    let duration = Duration::from_millis(timeout.try_into().unwrap());
465                    Some((TimeoutClock::Monotonic, TimeoutAnchor::Relative, duration))
466                }
467                -1 => None,
468                ..-1 => {
469                    throw_unsup_format!(
470                        "epoll_wait: Only timeout values greater than or equal to -1 are supported."
471                    );
472                }
473            };
474            // Record this thread as blocked.
475            epfd.queue.borrow_mut().push_back(this.active_thread());
476            // And block it.
477            let dest = dest.clone();
478            // We keep a strong ref to the underlying `Epoll` to make sure it sticks around.
479            // This means there'll be a leak if we never wake up, but that anyway would imply
480            // a thread is permanently blocked so this is fine.
481            this.block_thread(
482                BlockReason::Epoll,
483                timeout,
484                callback!(
485                    @capture<'tcx> {
486                        epfd: FileDescriptionRef<Epoll>,
487                        dest: MPlaceTy<'tcx>,
488                        event: MPlaceTy<'tcx>,
489                    }
490                    |this, unblock: UnblockKind| {
491                        match unblock {
492                            UnblockKind::Ready => {
493                                let events = return_ready_list(&epfd, &dest, &event, this)?;
494                                assert!(events > 0, "we got woken up with no events to deliver");
495                                interp_ok(())
496                            },
497                            UnblockKind::TimedOut => {
498                                // Remove the current active thread_id from the blocked thread_id list.
499                                epfd
500                                    .queue.borrow_mut()
501                                    .retain(|&id| id != this.active_thread());
502                                this.write_int(0, &dest)?;
503                                interp_ok(())
504                            },
505                        }
506                    }
507                ),
508            );
509        }
510        interp_ok(())
511    }
512
513    /// For a specific file description, get its currently active events and send it to everyone who
514    /// registered interest in this FD. This function must be called whenever the result of
515    /// `epoll_active_events` might change.
516    ///
517    /// If `force_edge` is set, edge-triggered interests will be triggered even if the set of
518    /// ready events did not change. This can lead to spurious wakeups. Use with caution!
519    fn update_epoll_active_events(
520        &mut self,
521        fd_ref: DynFileDescriptionRef,
522        force_edge: bool,
523    ) -> InterpResult<'tcx> {
524        let this = self.eval_context_mut();
525        let id = fd_ref.id();
526        // Figure out who is interested in this. We need to clone this list since we can't prove
527        // that `send_active_events_to_interest` won't mutate it.
528        let Some(epolls) = this.machine.epoll_interests.get_epolls(id) else {
529            return interp_ok(());
530        };
531        let epolls = epolls
532            .map(|weak| {
533                weak.upgrade()
534                    .expect("someone forgot to remove the garbage from `machine.epoll_interests`")
535            })
536            .collect::<Vec<_>>();
537        let active_events = fd_ref.as_unix(this).epoll_active_events()?.get_event_bitmask(this);
538        for epoll in epolls {
539            update_readiness(this, &epoll, active_events, force_edge, |callback| {
540                for (&key, interest) in epoll.interest_list.borrow_mut().range_mut(range_for_id(id))
541                {
542                    callback(key, interest)?;
543                }
544                interp_ok(())
545            })?;
546        }
547
548        interp_ok(())
549    }
550}
551
552/// Call this when the interests denoted by `for_each_interest` have their active event set changed
553/// to `active_events`. The list is provided indirectly via the `for_each_interest` closure, which
554/// will call its argument closure for each relevant interest.
555///
556/// Any `RefCell` should be released by the time `for_each_interest` returns since we will then
557/// be waking up threads which might require access to those `RefCell`.
558fn update_readiness<'tcx>(
559    ecx: &mut MiriInterpCx<'tcx>,
560    epoll: &Epoll,
561    active_events: u32,
562    force_edge: bool,
563    for_each_interest: impl FnOnce(
564        &mut dyn FnMut(EpollEventKey, &mut EpollEventInterest) -> InterpResult<'tcx>,
565    ) -> InterpResult<'tcx>,
566) -> InterpResult<'tcx> {
567    let mut ready_set = epoll.ready_set.borrow_mut();
568    for_each_interest(&mut |key, interest| {
569        // Update the ready events tracked in this interest.
570        let new_readiness = interest.relevant_events & active_events;
571        let prev_readiness = std::mem::replace(&mut interest.active_events, new_readiness);
572        if new_readiness == 0 {
573            // Un-trigger this, there's nothing left to report here.
574            ready_set.remove(&key);
575        } else if force_edge || new_readiness != prev_readiness & new_readiness {
576            // Either we force an "edge" to be detected, or there's a bit set in `new`
577            // that was not set in `prev`. In both cases, this is ready now.
578            ready_set.insert(key);
579            ecx.release_clock(|clock| {
580                interest.clock.join(clock);
581            })?;
582        }
583        interp_ok(())
584    })?;
585    // While there are events ready to be delivered, wake up a thread to receive them.
586    while !ready_set.is_empty()
587        && let Some(thread_id) = epoll.queue.borrow_mut().pop_front()
588    {
589        drop(ready_set); // release the "lock" so the unblocked thread can have it
590        ecx.unblock_thread(thread_id, BlockReason::Epoll)?;
591        ready_set = epoll.ready_set.borrow_mut();
592    }
593
594    interp_ok(())
595}
596
597/// Stores the ready list of the `epfd` epoll instance into `events` (which must be an array),
598/// and the number of returned events into `dest`.
599fn return_ready_list<'tcx>(
600    epfd: &FileDescriptionRef<Epoll>,
601    dest: &MPlaceTy<'tcx>,
602    events: &MPlaceTy<'tcx>,
603    ecx: &mut MiriInterpCx<'tcx>,
604) -> InterpResult<'tcx, i32> {
605    let mut interest_list = epfd.interest_list.borrow_mut();
606    let mut ready_set = epfd.ready_set.borrow_mut();
607    let mut num_of_events: i32 = 0;
608    let mut array_iter = ecx.project_array_fields(events)?;
609
610    // Sanity-check to ensure that all event info is up-to-date.
611    if cfg!(debug_assertions) {
612        for (key, interest) in interest_list.iter() {
613            // Ensure this matches the latest readiness of this FD.
614            // We have to do an FD lookup by ID for this. The FdNum might be already closed.
615            let fd = &ecx.machine.fds.fds.values().find(|fd| fd.id() == key.0).unwrap();
616            let current_active = fd.as_unix(ecx).epoll_active_events()?.get_event_bitmask(ecx);
617            assert_eq!(interest.active_events, current_active & interest.relevant_events);
618        }
619    }
620
621    // While there is a slot to store another event, and an event to store, deliver that event.
622    // We can't use an iterator over `ready_set` as we want to remove elements as we go,
623    // so we track the most recently delivered event to find the next one. We track it as a lower
624    // bound that we can pass to `BTreeSet::range`.
625    let mut event_lower_bound = Bound::Unbounded;
626    while let Some(slot) = array_iter.next(ecx)?
627        && let Some(&key) = ready_set.range((event_lower_bound, Bound::Unbounded)).next()
628    {
629        let interest = interest_list.get_mut(&key).expect("non-existent event in ready set");
630        // Deliver event to caller.
631        ecx.write_int_fields_named(
632            &[("events", interest.active_events.into()), ("u64", interest.data.into())],
633            &slot.1,
634        )?;
635        num_of_events = num_of_events.strict_add(1);
636        // Synchronize receiving thread with the event of interest.
637        ecx.acquire_clock(&interest.clock)?;
638        // This was an edge-triggered event, so remove it from the ready set.
639        ready_set.remove(&key);
640        // Go find the next event.
641        event_lower_bound = Bound::Excluded(key);
642    }
643    ecx.write_int(num_of_events, dest)?;
644    interp_ok(num_of_events)
645}