Skip to main content

windows_ioring_sys/sys/
call_faults.rs

1// Copyright (c) Mike Grier
2//! A safe responder that fails a ring's calls, a completion's result, or its submits on demand
3//! (M37.1, M38.1).
4//!
5//! # Why this is here and not in the consumer
6//!
7//! [`Responses`]' methods are `unsafe fn`, so implementing it needs `unsafe`
8//! -- which a consumer such as durable-ioring forbids
9//! ([DI-D-4](../../../durable-ioring/DESIGN-NOTES.md#di-d-4)) and which that
10//! decision says to answer by adding the safe wrapper to the lower crate. What
11//! a consumer's failure paths need is small enough to provide whole: fail a
12//! call for as long as, or as many times as, a test says; fail the result of
13//! the next completions; and stall submits so an operation stays pending. Every
14//! other call reaches the kernel.
15//!
16//! # Use
17//!
18//! ```
19//! use windows_ioring_sys::sys::{Call, call_faults, register};
20//! use windows_ioring_sys::{InjectedFailure, IoRing};
21//!
22//! let mut ring = IoRing::new(8, 16)?;
23//! let (faults, control) = call_faults();
24//! // Ended by the ring's drop; nothing here needs it sooner.
25//! let _registration = register(&ring, Box::new(faults));
26//!
27//! // `ERROR_IO_DEVICE`, once: the first attach of the completion event fails.
28//! control.fail_next(Call::SetCompletionEvent, 1, InjectedFailure::Win32(1117));
29//! assert!(ring.completion_event().is_err());
30//! assert!(ring.completion_event().is_ok(), "and the next reaches the kernel");
31//! # Ok::<(), std::io::Error>(())
32//! ```
33//!
34//! [`submit_faults`] is the narrow form for the one call a consumer most
35//! often wants failed:
36//!
37//! ```
38//! use windows_ioring_sys::sys::{register, submit_faults};
39//! use windows_ioring_sys::{Batch, InjectedFailure, IoRing};
40//!
41//! let mut ring = IoRing::new(8, 16)?;
42//! let (faults, control) = submit_faults();
43//! let _registration = register(&ring, Box::new(faults));
44//!
45//! // `ERROR_IO_DEVICE`: the ring's submits, on any thread, now fail.
46//! control.fail_until_cleared(InjectedFailure::Win32(1117));
47//! assert!(Batch::new(&mut ring).submit().is_err());
48//!
49//! control.clear();
50//! Batch::new(&mut ring).submit()?;
51//! # Ok::<(), std::io::Error>(())
52//! ```
53//!
54//! # What a failed call is
55//!
56//! It returns the failure **without reaching the kernel**. For a submit that
57//! means every entry it was asked to submit stays in the submission queue,
58//! which is what `SubmitIoRing` documents for a failure (RS-P-7): a later
59//! submit is what runs them. A call's out-parameters are left untouched,
60//! because what the real call writes there on failure is undocumented and
61//! writing anything would invent a platform.
62//!
63//! **A submit that waits fails too.** A ring dropped with the fault still
64//! armed and operations outstanding cannot run down -- its rundown is a
65//! submit -- so clear the fault first.
66//!
67//! # What a failed completion is
68//!
69//! [`fail_next_completions`](CallFaultControl::fail_next_completions) pops the
70//! real completion and rewrites only its result code, the identity and
71//! everything else untouched -- what
72//! [`Completion::with_injected_failure`](crate::Completion::with_injected_failure)
73//! does to a popped one, here on the way out of the seam, so a consumer that
74//! never sees the raw completion can still be failed.
75//!
76//! # What a stalled submit is
77//!
78//! [`stall_submits`](CallFaultControl::stall_submits) leaves an operation
79//! pending, which the platform permits (RS-P-1). A stalled submit returns
80//! without reaching the kernel: success with nothing submitted if it does not
81//! wait, and `IORING_E_WAIT_TIMEOUT` after its wait -- or what is left of the
82//! stall, if less -- if it does, which is the documented answer to a wait that
83//! expired. So a bounded wait for the operation's completion expires, and a
84//! ring dropped meanwhile still runs down once the stall ends. The stall runs
85//! from the **first submit it stalled**, not from when it was armed, so
86//! nothing a test does between arming and submitting shortens it.
87
88use std::collections::HashMap;
89use std::ffi::c_void;
90use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
91use std::thread::ThreadId;
92use std::time::{Duration, Instant};
93
94use windows_sys::Win32::Foundation::S_OK;
95use windows_sys::Win32::Storage::FileSystem::{
96    FILE_FLUSH_MODE, IORING_BUFFER_INFO, IORING_BUFFER_REF, IORING_CQE, IORING_HANDLE_REF,
97};
98use windows_sys::core::HRESULT;
99
100use super::Responses;
101use crate::InjectedFailure;
102use crate::ring::IORING_E_WAIT_TIMEOUT;
103
104/// A call the seam carries, which [`CallFaults`] can fail.
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
106#[non_exhaustive]
107pub enum Call {
108    /// `SubmitIoRing`.
109    Submit,
110    /// `PopIoRingCompletion`, the call itself failing. A completion's result
111    /// failing is [`CallFaultControl::fail_next_completions`].
112    Pop,
113    /// `BuildIoRingReadFile`.
114    BuildRead,
115    /// `BuildIoRingWriteFile`.
116    BuildWrite,
117    /// `BuildIoRingFlushFile`.
118    BuildFlush,
119    /// `BuildIoRingCancelRequest`.
120    BuildCancel,
121    /// `BuildIoRingRegisterFileHandles`.
122    BuildRegisterFiles,
123    /// `BuildIoRingRegisterBuffers`.
124    BuildRegisterBuffers,
125    /// `SetIoRingCompletionEvent`.
126    SetCompletionEvent,
127}
128
129/// A failure armed: the code, and how many more times, or until cleared when `None`.
130#[derive(Debug, Clone, Copy)]
131struct Fail {
132    code: HRESULT,
133    remaining: Option<u32>,
134}
135
136impl Fail {
137    /// Spend one use of the failure in `slot`: its code, and what is left of it.
138    fn spend(slot: &mut Option<Self>) -> Option<HRESULT> {
139        let fail = (*slot)?;
140        *slot = match fail.remaining {
141            Some(left) if left > 1 => Some(Self {
142                remaining: Some(left - 1),
143                ..fail
144            }),
145            Some(_) => None,
146            None => Some(fail),
147        };
148        Some(fail.code)
149    }
150}
151
152/// Whether submits are stalled.
153#[derive(Debug, Clone, Copy)]
154enum Stall {
155    Off,
156    /// Armed for this long, not yet begun: it begins at the first submit it stalls.
157    For(Duration),
158    /// Begun at this instant, for this long.
159    Since(Instant, Duration),
160}
161
162#[derive(Debug)]
163struct State {
164    calls: HashMap<Call, Fail>,
165    completions: Option<Fail>,
166    stall: Stall,
167    /// The call and thread of each failed call, in order.
168    failed_from: Vec<(Call, ThreadId)>,
169}
170
171/// The state shared by a responder and its control, under one lock so a
172/// failure is recorded in the same step that decides it.
173type Shared = Arc<Mutex<State>>;
174
175fn lock(shared: &Shared) -> MutexGuard<'_, State> {
176    shared.lock().unwrap_or_else(PoisonError::into_inner)
177}
178
179/// Reaches the kernel for every call: the trait's defaults.
180struct Forward;
181
182impl Responses for Forward {}
183
184/// A responder that fails its ring's calls, completions and submits when armed through its
185/// [`CallFaultControl`], and forwards everything else to the kernel.
186///
187/// Register it with [`register`](super::register).
188#[derive(Debug)]
189pub struct CallFaults {
190    shared: Shared,
191}
192
193/// Arms and reads a [`CallFaults`] from any thread.
194#[derive(Debug, Clone)]
195pub struct CallFaultControl {
196    shared: Shared,
197}
198
199/// A responder that fails a ring's calls on demand, and the handle that arms it. Nothing fails
200/// until armed.
201#[must_use]
202pub fn call_faults() -> (CallFaults, CallFaultControl) {
203    let shared = Arc::new(Mutex::new(State {
204        calls: HashMap::new(),
205        completions: None,
206        stall: Stall::Off,
207        failed_from: Vec::new(),
208    }));
209    (
210        CallFaults {
211            shared: Arc::clone(&shared),
212        },
213        CallFaultControl { shared },
214    )
215}
216
217impl CallFaultControl {
218    /// Fail the next `count` calls of `call` with `failure`, then reach the kernel again. A
219    /// `count` of zero clears `call`.
220    ///
221    /// # Panics
222    ///
223    /// If `failure` is not a failing `HRESULT`, which only [`InjectedFailure::Hresult`] can
224    /// express: this seam injects failure, never success.
225    pub fn fail_next(&self, call: Call, count: u32, failure: InjectedFailure) {
226        let code = failure.as_hresult();
227        let mut state = lock(&self.shared);
228        if count == 0 {
229            state.calls.remove(&call);
230        } else {
231            state.calls.insert(
232                call,
233                Fail {
234                    code,
235                    remaining: Some(count),
236                },
237            );
238        }
239    }
240
241    /// Fail every call of `call` with `failure` until [`clear`](Self::clear).
242    ///
243    /// # Panics
244    ///
245    /// As [`fail_next`](Self::fail_next).
246    pub fn fail_until_cleared(&self, call: Call, failure: InjectedFailure) {
247        let code = failure.as_hresult();
248        lock(&self.shared).calls.insert(
249            call,
250            Fail {
251                code,
252                remaining: None,
253            },
254        );
255    }
256
257    /// Rewrite the result code of the next `count` completions popped to `failure`, the identity
258    /// and everything else in them untouched. A `count` of zero clears.
259    ///
260    /// # Panics
261    ///
262    /// As [`fail_next`](Self::fail_next).
263    pub fn fail_next_completions(&self, count: u32, failure: InjectedFailure) {
264        let code = failure.as_hresult();
265        lock(&self.shared).completions = (count != 0).then_some(Fail {
266            code,
267            remaining: Some(count),
268        });
269    }
270
271    /// Stall submits for `duration`, from the first submit stalled: each returns without reaching
272    /// the kernel, so what is queued stays pending, until `duration` has passed and submits reach
273    /// the kernel and run it. A `duration` of zero clears.
274    pub fn stall_submits(&self, duration: Duration) {
275        lock(&self.shared).stall = if duration.is_zero() {
276            Stall::Off
277        } else {
278            Stall::For(duration)
279        };
280    }
281
282    /// Stop failing and stalling: every call reaches the kernel again. What was recorded is kept.
283    pub fn clear(&self) {
284        let mut state = lock(&self.shared);
285        state.calls.clear();
286        state.completions = None;
287        state.stall = Stall::Off;
288    }
289
290    /// The thread that made each failed call of `call` so far, in order.
291    ///
292    /// A test that arms a failure on one thread and wants to show it was answered on another -- a
293    /// pool thread draining the ring -- compares these with its own.
294    #[must_use]
295    pub fn failures(&self, call: Call) -> Vec<ThreadId> {
296        lock(&self.shared)
297            .failed_from
298            .iter()
299            .filter(|(failed, _)| *failed == call)
300            .map(|(_, thread)| *thread)
301            .collect()
302    }
303}
304
305impl CallFaults {
306    /// The failure armed for `call`, spent, if one is.
307    fn fails(&self, call: Call) -> Option<HRESULT> {
308        let mut state = lock(&self.shared);
309        let mut slot = state.calls.get(&call).copied();
310        let code = Fail::spend(&mut slot)?;
311        match slot {
312            Some(armed) => state.calls.insert(call, armed),
313            None => state.calls.remove(&call),
314        };
315        state.failed_from.push((call, std::thread::current().id()));
316        Some(code)
317    }
318
319    /// How a stalled submit answers, if submits are stalled and the stall has not ended: the
320    /// answer, and how long to wait before giving it.
321    fn stalled(&self, wait_operations: u32, milliseconds: u32) -> Option<(HRESULT, Duration)> {
322        let mut state = lock(&self.shared);
323        let (since, length) = match state.stall {
324            Stall::Off => return None,
325            Stall::For(length) => {
326                let now = Instant::now();
327                state.stall = Stall::Since(now, length);
328                (now, length)
329            }
330            Stall::Since(since, length) => (since, length),
331        };
332        let left = length.saturating_sub(since.elapsed());
333        if left.is_zero() {
334            state.stall = Stall::Off;
335            return None;
336        }
337        Some(if wait_operations == 0 {
338            (S_OK, Duration::ZERO)
339        } else {
340            (
341                IORING_E_WAIT_TIMEOUT,
342                left.min(Duration::from_millis(u64::from(milliseconds))),
343            )
344        })
345    }
346}
347
348impl Responses for CallFaults {
349    unsafe fn submit(
350        &mut self,
351        ring: *mut c_void,
352        wait_operations: u32,
353        milliseconds: u32,
354        submitted: *mut u32,
355    ) -> HRESULT {
356        if let Some(code) = self.fails(Call::Submit) {
357            return code;
358        }
359        if let Some((answer, wait)) = self.stalled(wait_operations, milliseconds) {
360            std::thread::sleep(wait);
361            if answer == S_OK && !submitted.is_null() {
362                // SAFETY: `submitted` is the caller's valid out-pointer, as `SubmitIoRing`
363                // requires; nothing was submitted, which is what is written.
364                unsafe { submitted.write(0) };
365            }
366            return answer;
367        }
368        // SAFETY: forwarded unchanged from the seam's caller, who holds the Win32 call's own
369        // contract.
370        unsafe { Forward.submit(ring, wait_operations, milliseconds, submitted) }
371    }
372
373    unsafe fn pop(&mut self, ring: *mut c_void, cqe: *mut IORING_CQE) -> HRESULT {
374        if let Some(code) = self.fails(Call::Pop) {
375            return code;
376        }
377        // SAFETY: as `submit`.
378        let answer = unsafe { Forward.pop(ring, cqe) };
379        // `S_OK` is a completion popped; an empty queue answers `S_FALSE`, with nothing to fail.
380        if answer == S_OK
381            && let Some(code) = Fail::spend(&mut lock(&self.shared).completions)
382        {
383            // SAFETY: `cqe` is the caller's valid out-pointer, and the real pop just wrote it.
384            unsafe { (*cqe).ResultCode = code };
385        }
386        answer
387    }
388
389    #[allow(clippy::too_many_arguments)]
390    unsafe fn build_read(
391        &mut self,
392        ring: *mut c_void,
393        file: IORING_HANDLE_REF,
394        buffer: IORING_BUFFER_REF,
395        bytes: u32,
396        offset: u64,
397        user_data: usize,
398        flags: i32,
399    ) -> HRESULT {
400        if let Some(code) = self.fails(Call::BuildRead) {
401            return code;
402        }
403        // SAFETY: as `submit`.
404        unsafe { Forward.build_read(ring, file, buffer, bytes, offset, user_data, flags) }
405    }
406
407    #[allow(clippy::too_many_arguments)]
408    unsafe fn build_write(
409        &mut self,
410        ring: *mut c_void,
411        file: IORING_HANDLE_REF,
412        buffer: IORING_BUFFER_REF,
413        bytes: u32,
414        offset: u64,
415        caching: i32,
416        user_data: usize,
417        flags: i32,
418    ) -> HRESULT {
419        if let Some(code) = self.fails(Call::BuildWrite) {
420            return code;
421        }
422        // SAFETY: as `submit`.
423        unsafe { Forward.build_write(ring, file, buffer, bytes, offset, caching, user_data, flags) }
424    }
425
426    unsafe fn build_flush(
427        &mut self,
428        ring: *mut c_void,
429        file: IORING_HANDLE_REF,
430        mode: FILE_FLUSH_MODE,
431        user_data: usize,
432        flags: i32,
433    ) -> HRESULT {
434        if let Some(code) = self.fails(Call::BuildFlush) {
435            return code;
436        }
437        // SAFETY: as `submit`.
438        unsafe { Forward.build_flush(ring, file, mode, user_data, flags) }
439    }
440
441    unsafe fn build_cancel(
442        &mut self,
443        ring: *mut c_void,
444        file: IORING_HANDLE_REF,
445        cancel_user_data: usize,
446        user_data: usize,
447    ) -> HRESULT {
448        if let Some(code) = self.fails(Call::BuildCancel) {
449            return code;
450        }
451        // SAFETY: as `submit`.
452        unsafe { Forward.build_cancel(ring, file, cancel_user_data, user_data) }
453    }
454
455    unsafe fn build_register_files(
456        &mut self,
457        ring: *mut c_void,
458        count: u32,
459        handles: *const *mut c_void,
460        user_data: usize,
461    ) -> HRESULT {
462        if let Some(code) = self.fails(Call::BuildRegisterFiles) {
463            return code;
464        }
465        // SAFETY: as `submit`.
466        unsafe { Forward.build_register_files(ring, count, handles, user_data) }
467    }
468
469    unsafe fn build_register_buffers(
470        &mut self,
471        ring: *mut c_void,
472        count: u32,
473        buffers: *const IORING_BUFFER_INFO,
474        user_data: usize,
475    ) -> HRESULT {
476        if let Some(code) = self.fails(Call::BuildRegisterBuffers) {
477            return code;
478        }
479        // SAFETY: as `submit`.
480        unsafe { Forward.build_register_buffers(ring, count, buffers, user_data) }
481    }
482
483    unsafe fn set_completion_event(&mut self, ring: *mut c_void, event: *mut c_void) -> HRESULT {
484        if let Some(code) = self.fails(Call::SetCompletionEvent) {
485            return code;
486        }
487        // SAFETY: as `submit`.
488        unsafe { Forward.set_completion_event(ring, event) }
489    }
490}
491
492/// A responder that fails a ring's submits on demand, and the narrow handle that arms it:
493/// [`call_faults`], with the control reduced to [`Call::Submit`]. Nothing fails until armed.
494#[must_use]
495pub fn submit_faults() -> (CallFaults, SubmitFaultControl) {
496    let (faults, control) = call_faults();
497    (faults, SubmitFaultControl { control })
498}
499
500/// Arms and reads a ring's submit failures from any thread: a [`CallFaultControl`] that can only
501/// fail [`Call::Submit`].
502#[derive(Debug, Clone)]
503pub struct SubmitFaultControl {
504    control: CallFaultControl,
505}
506
507impl SubmitFaultControl {
508    /// Fail the next `count` submits with `failure`, then reach the kernel again. A `count` of
509    /// zero clears.
510    ///
511    /// # Panics
512    ///
513    /// If `failure` is not a failing `HRESULT`, which only [`InjectedFailure::Hresult`] can
514    /// express: this seam injects failure, never success.
515    pub fn fail_next(&self, count: u32, failure: InjectedFailure) {
516        self.control.fail_next(Call::Submit, count, failure);
517    }
518
519    /// Fail every submit with `failure` until [`clear`](Self::clear).
520    ///
521    /// # Panics
522    ///
523    /// As [`fail_next`](Self::fail_next).
524    pub fn fail_until_cleared(&self, failure: InjectedFailure) {
525        self.control.fail_until_cleared(Call::Submit, failure);
526    }
527
528    /// Stop failing: submits reach the kernel again.
529    pub fn clear(&self) {
530        self.control.clear();
531    }
532
533    /// The thread that made each failed submit so far, in order.
534    #[must_use]
535    pub fn failures(&self) -> Vec<ThreadId> {
536        self.control.failures(Call::Submit)
537    }
538}
539
540#[cfg(test)]
541mod tests;