Skip to main content

miri/concurrency/
blocking_io.rs

1use std::cell::RefMut;
2use std::collections::BTreeMap;
3use std::io;
4use std::time::Duration;
5
6use mio::event::Source;
7use mio::{Events, Interest, Poll, Token};
8
9use crate::shims::{FdId, FileDescription, FileDescriptionRef, WeakFileDescriptionRef};
10use crate::*;
11
12/// Capacity of the event queue which can be polled at a time.
13/// Since we don't expect many simultaneous blocking I/O events
14/// this value can be set rather low.
15const IO_EVENT_CAPACITY: usize = 16;
16
17/// Trait for file descriptions that contain a mio [`Source`].
18pub trait SourceFileDescription: FileDescription {
19    /// Invoke `f` on the source inside `self`.
20    fn with_source(&self, f: &mut dyn FnMut(&mut dyn Source) -> io::Result<()>) -> io::Result<()>;
21
22    /// Get a mutable reference to the readiness of the source.
23    fn get_readiness_mut(&self) -> RefMut<'_, Readiness>;
24}
25
26/// An I/O interest for a blocked thread. Note that all threads are always considered
27/// to be interested in "error" events.
28#[derive(Debug, Clone, Copy)]
29pub enum BlockingIoInterest {
30    /// The blocked thread is interested in [`Interest::READABLE`].
31    Read,
32    /// The blocked thread is interested in [`Interest::WRITABLE`].
33    Write,
34    /// The blocked thread is interested in [`Interest::READABLE`] and
35    /// [`Interest::WRITABLE`].
36    ReadWrite,
37}
38
39impl BlockingIoInterest {
40    /// Check whether the [`Readiness`] fulfills this blocking I/O interest.
41    /// This function also returns `true` if the error readiness is set
42    /// even when the requested interest might not be fulfilled.
43    fn is_fulfilled_by(&self, readiness: &Readiness) -> bool {
44        match self {
45            BlockingIoInterest::Read => readiness.readable || readiness.error,
46            BlockingIoInterest::Write => readiness.writable || readiness.error,
47            BlockingIoInterest::ReadWrite =>
48                readiness.readable || readiness.writable || readiness.error,
49        }
50    }
51}
52
53impl From<&mio::event::Event> for Readiness {
54    fn from(event: &mio::event::Event) -> Self {
55        Self {
56            readable: event.is_readable(),
57            writable: event.is_writable(),
58            read_closed: event.is_read_closed(),
59            write_closed: event.is_write_closed(),
60            error: event.is_error(),
61        }
62    }
63}
64
65struct BlockingIoSource {
66    /// The source file description which is registered into the poll.
67    /// We only store weak references such that source file descriptions
68    /// can be destroyed whilst they are registered. However, they are required
69    /// to deregister themselves when [`FileDescription::destroy`] is called.
70    fd: WeakFileDescriptionRef<dyn SourceFileDescription>,
71    /// The threads which are blocked on the I/O source, and the interest indicating
72    /// when they should be unblocked.
73    blocked_threads: BTreeMap<ThreadId, BlockingIoInterest>,
74}
75
76/// Manager for managing blocking host I/O in a non-blocking manner.
77/// We use [`Poll`] to poll for new I/O events from the OS for sources
78/// registered using this manager.
79///
80/// The semantics of this manager are that host I/O sources are registered
81/// to a [`Poll`] for their entire lifespan. Once host readiness events happen
82/// on a registered source, its internal readiness gets updated -- even when
83/// the source isn't part of an active [`ReadinessWatcher`]. Also, for the entire
84/// lifespan of the source, threads can be added which should be unblocked
85/// once a certain [`Readiness`] for an I/O source is satisfied.
86///
87/// Since blocking host I/O is inherently non-deterministic, no method on this
88/// manager should be called when isolation is enabled. The only exception is
89/// the [`BlockingIoManager::new`] function to create the manager. Everywhere else,
90/// we assert that isolation is disabled!
91pub struct BlockingIoManager {
92    /// Poll instance to monitor I/O events from the OS.
93    /// This is only [`None`] when Miri is run with isolation enabled.
94    poll: Option<Poll>,
95    /// Buffer used to store the ready I/O events when calling [`Poll::poll`].
96    /// This is not part of the state and only stored to avoid allocating a
97    /// new buffer for every poll.
98    events: Events,
99    /// Map from source file description ids to the actual sources and their
100    /// blocked threads.
101    sources: BTreeMap<FdId, BlockingIoSource>,
102}
103
104impl BlockingIoManager {
105    /// Create a new blocking I/O manager instance based on the availability
106    /// of communication with the host.
107    pub fn new(communicate: bool) -> Result<Self, io::Error> {
108        let manager = Self {
109            poll: communicate.then_some(Poll::new()?),
110            events: Events::with_capacity(IO_EVENT_CAPACITY),
111            sources: BTreeMap::default(),
112        };
113        Ok(manager)
114    }
115
116    /// Poll for new I/O events from the OS or wait until the timeout expired.
117    /// The timeout semantics are the same as described in [`Poll::poll`].
118    /// The events also immediately get processed: threads get unblocked, and fd readiness gets updated.
119    fn poll<'tcx>(
120        ecx: &mut MiriInterpCx<'tcx>,
121        timeout: Option<Duration>,
122    ) -> InterpResult<'tcx, Result<(), io::Error>> {
123        let poll = ecx
124            .machine
125            .blocking_io
126            .poll
127            .as_mut()
128            .expect("Blocking I/O should not be called with isolation enabled");
129
130        // Poll for new I/O events from OS and store them in the events buffer.
131        if let Err(err) = poll.poll(&mut ecx.machine.blocking_io.events, timeout) {
132            return interp_ok(Err(err));
133        };
134
135        let event_fds = ecx
136            .machine
137            .blocking_io
138            .events
139            .iter()
140            .map(|event| {
141                let token = event.token();
142                // We know all tokens are valid `FdId`.
143                let fd_id = FdId::new_unchecked(token.0);
144                let source = ecx
145                    .machine
146                    .blocking_io
147                    .sources
148                    .get(&fd_id)
149                    .expect("Source should be registered");
150                let fd = source.fd.upgrade().expect(
151                    "Source file description shouldn't be destroyed whilst being registered",
152                );
153
154                assert_eq!(fd.id(), fd_id);
155                // Update the readiness of the source.
156                *fd.get_readiness_mut() |= Readiness::from(event);
157                // Put FD into `event_fds` list.
158                fd
159            })
160            .collect::<Vec<_>>();
161
162        // Update the readiness for all source file descriptions which received an event. Also,
163        // unblock the threads which are blocked on such a source and whose interests are now fulfilled.
164        for fd in event_fds.into_iter() {
165            // Update readiness for the `fd` source.
166            ecx.update_fd_readiness(fd.clone(), false)?;
167
168            let source =
169                ecx.machine.blocking_io.sources.get(&fd.id()).expect(
170                    "Source file description shouldn't be destroyed whilst being registered",
171                );
172
173            // List of all thread id's whose interests are currently fulfilled
174            // and which are blocked on the `fd` source. This also includes
175            // threads whose interests were already fulfilled before the
176            // `poll` invocation.
177            let threads = source
178                .blocked_threads
179                .iter()
180                .filter_map(|(thread_id, interest)| {
181                    interest.is_fulfilled_by(&fd.get_readiness_mut()).then_some(*thread_id)
182                })
183                .collect::<Vec<_>>();
184
185            // Unblock all threads whose interests are currently fulfilled and
186            // which are blocked on the `fd` source.
187            threads
188                .into_iter()
189                .try_for_each(|thread_id| ecx.unblock_thread(thread_id, BlockReason::IO))?;
190        }
191
192        interp_ok(Ok(()))
193    }
194
195    /// Register a source file description to the blocking I/O poll.
196    pub fn register(&mut self, source_fd: FileDescriptionRef<dyn SourceFileDescription>) {
197        let poll =
198            self.poll.as_ref().expect("Blocking I/O should not be called with isolation enabled");
199
200        let id = source_fd.id();
201        let token = Token(id.to_usize());
202
203        // All possible interests.
204        // We only care about the readable and writable interests because those are the only
205        // interests which are available on all platforms. Internally, mio also
206        // registers an error interest.
207        let interest = Interest::READABLE | Interest::WRITABLE;
208
209        // Treat errors from registering as fatal. On UNIX hosts this can only
210        // fail due to system resource errors (e.g. ENOMEM or ENOSPC) or when the source is already registered.
211        source_fd
212            .with_source(&mut |source| poll.registry().register(source, token, interest))
213            .unwrap();
214
215        let source = BlockingIoSource {
216            fd: FileDescriptionRef::downgrade(&source_fd),
217            blocked_threads: BTreeMap::default(),
218        };
219
220        self.sources
221            .try_insert(id, source)
222            .unwrap_or_else(|_| panic!("Source should not already be registered"));
223    }
224
225    /// Deregister a source file description from the blocking I/O poll.
226    ///
227    /// It's assumed that the file description with id `source_id` is already
228    /// removed from the file description table.
229    pub fn deregister(&mut self, source_id: FdId, source: impl SourceFileDescription) {
230        let poll =
231            self.poll.as_ref().expect("Blocking I/O should not be called with isolation enabled");
232
233        let stored_source = self.sources.remove(&source_id).expect("Source should be registered");
234        // Ensure that the source file description is already removed from the file
235        // description table.
236        assert!(
237            stored_source.fd.upgrade().is_none(),
238            "Sources must only be deregistered when they are destroyed"
239        );
240
241        // Because we only store `WeakFileDescriptionRef`s and the `stored_source` file description
242        // is already destroyed, the weak reference can no longer be upgraded. Thus, we cannot use
243        // it to deregister the source from the poll and instead use the `source` argument to deregister.
244
245        // Treat errors from deregistering as fatal. On UNIX hosts this can only
246        // fail due to system resource errors (e.g. ENOMEM or ENOSPC).
247        source.with_source(&mut |source| poll.registry().deregister(source)).unwrap();
248    }
249
250    /// Add a new blocked thread to a registered source. The thread gets unblocked
251    /// once its [`BlockingIoInterest`] is fulfilled when calling
252    /// [`BlockingIoManager::poll`].
253    ///
254    /// It's assumed that the thread of `thread_id` isn't already blocked on
255    /// the source with id `source_id` and that this source is currently
256    /// registered.
257    fn add_blocked_thread(
258        &mut self,
259        source_id: FdId,
260        thread_id: ThreadId,
261        interest: BlockingIoInterest,
262    ) {
263        let source = self.sources.get_mut(&source_id).expect("Source should be registered");
264
265        source
266            .blocked_threads
267            .try_insert(thread_id, interest)
268            .expect("Thread cannot be blocked multiple times on the same source");
269    }
270
271    /// Remove a blocked thread from a registered source.
272    ///
273    /// It's assumed that the thread of `thread_id` is blocked on the
274    /// source with id `source_id` and that this source is currently
275    /// registered.
276    pub fn remove_blocked_thread(&mut self, source_id: FdId, thread_id: ThreadId) {
277        let source = self.sources.get_mut(&source_id).expect("Source should be registered");
278        source.blocked_threads.remove(&thread_id).expect("Thread should be blocked on source");
279    }
280}
281
282impl<'tcx> EvalContextExt<'tcx> for MiriInterpCx<'tcx> {}
283pub trait EvalContextExt<'tcx>: MiriInterpCxExt<'tcx> {
284    /// Block the current thread until some interests on an I/O source
285    /// are fulfilled or the optional timeout exceeded.
286    /// The callback will be invoked when the thread gets unblocked.
287    ///
288    /// Note that an error interest is implicitly added to `interest`.
289    /// This means that the thread will also be unblocked when the error
290    /// readiness gets set for the source even when the requested interest
291    /// might not be fulfilled.
292    ///
293    /// The callback function will immediately be executed with [`UnblockKind::Ready`]
294    /// when `interest` is already fulfilled for `source_fd`.
295    ///
296    /// There can also be spurious wake-ups by the OS and thus it's the callers
297    /// responsibility to verify that the requested I/O interests are
298    /// really ready and to block again if they're not.
299    ///
300    /// It's the callers responsibility to remove the [`BlockingIoInterest`]
301    /// from the blocking I/O manager in the provided callback function.
302    #[inline]
303    fn block_thread_for_io(
304        &mut self,
305        source_fd: FileDescriptionRef<dyn SourceFileDescription>,
306        interest: BlockingIoInterest,
307        deadline: Option<Deadline>,
308        callback: DynUnblockCallback<'tcx>,
309    ) -> InterpResult<'tcx> {
310        let this = self.eval_context_mut();
311
312        // We always have to do this since the thread will de-register itself.
313        this.machine.blocking_io.add_blocked_thread(source_fd.id(), this.active_thread(), interest);
314
315        if interest.is_fulfilled_by(&source_fd.get_readiness_mut()) {
316            // The requested readiness is currently already fulfilled for the provided source.
317            // Instead of actually blocking the thread, we just run the callback function.
318            callback.call(this, UnblockKind::Ready)
319        } else {
320            // The I/O readiness is currently not fulfilled. We block the thread
321            // until the readiness is fulfilled and execute the callback then.
322            this.block_thread(BlockReason::IO, deadline, callback);
323            interp_ok(())
324        }
325    }
326
327    /// Poll for I/O events until either an I/O event happened or the timeout expired.
328    ///
329    /// - If the timeout is [`Some`] and contains [`Duration::ZERO`], the poll doesn't block and just
330    ///   reads all events since the last poll.
331    /// - If the timeout is [`Some`] and contains a non-zero duration, it blocks at most for the
332    ///   specified duration.
333    /// - If the timeout is [`None`] the poll blocks indefinitely until an event occurs.
334    ///
335    /// Unblocks all threads which are blocked on I/O and whose I/O interests
336    /// are currently fulfilled.
337    fn poll_and_unblock(&mut self, timeout: Option<Duration>) -> InterpResult<'tcx> {
338        let this = self.eval_context_mut();
339
340        match BlockingIoManager::poll(this, timeout)? {
341            Ok(_) => interp_ok(()),
342            // We can ignore errors originating from interrupts; that's just a spurious wakeup.
343            Err(e) if e.kind() == io::ErrorKind::Interrupted => interp_ok(()),
344            // For other errors we panic. On Linux and BSD hosts this should only be
345            // reachable when a system resource error (e.g. ENOMEM or ENOSPC) occurred.
346            Err(e) => panic!("unexpected error while polling: {e}"),
347        }
348    }
349
350    /// Returns whether there exists any thread that is blocked on host I/O.
351    fn any_thread_blocked_on_host(&self) -> bool {
352        let this = self.eval_context_ref();
353        this.machine.blocking_io.sources.iter().any(|(&fd_id, source)| {
354            // There's two ways something could be blocked on this: directly,
355            // or indirectly via a readiness watcher.
356            source.blocked_threads.len() > 0 || this.has_watcher_with_blocked_thread(fd_id)
357        })
358    }
359}