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;