Skip to main content

windows_overlapped_io_sys/
device.rs

1// Copyright (c) 2026 Mike Grier
2//! Buffer-owning device-control operation adapters, gated behind the `device`
3//! feature.
4//!
5//! These wrappers own the input and output buffers and issue the single native
6//! `DeviceIoControl` internally, so a caller performs an overlapped device
7//! control without touching `OVERLAPPED` or the submission seam. A device is a
8//! `HANDLE`, so the adapters extend the existing handle endpoints rather than
9//! introducing a device-specific type.
10//!
11//! The `ioctl` methods are `unsafe` because they take an arbitrary control code:
12//! a code whose input structure embeds raw pointers to separate buffers (such as
13//! `SCSI_PASS_THROUGH_DIRECT`) reaches storage these adapters do not own, so only
14//! the caller can guarantee that storage outlives the operation. A self-contained
15//! code -- an `FSCTL` query, say -- needs nothing beyond the owned buffers, but
16//! the seam cannot tell the two apart, so the obligation lives in the contract.
17
18use std::ffi::c_void;
19use std::io;
20use std::os::windows::io::AsRawHandle;
21
22use windows_sys::Win32::Foundation::ERROR_IO_PENDING;
23use windows_sys::Win32::System::IO::DeviceIoControl;
24
25use crate::operation::sync_bytes_ptr_from_overlapped;
26use crate::{
27    AssociatedEndpoint, BlockingEndpoint, Completion, IoBuf, IoBufMut, Issued, Operation,
28    OperationId, Started, Submitted,
29};
30
31impl BlockingEndpoint {
32    /// Issue an overlapped `DeviceIoControl` with control code `code`, blocking
33    /// until it completes.
34    ///
35    /// `input` is the input buffer (empty for control codes that take none) and
36    /// `output` is the buffer the device writes into; the return value is how
37    /// many bytes it wrote. Takes plain slices and allocates nothing: this call
38    /// does not return until the operation is over, so an ordinary borrow
39    /// provably covers the whole time the driver is using them.
40    ///
41    /// # Errors
42    ///
43    /// Returns [`io::ErrorKind::InvalidInput`] if either buffer is longer than
44    /// `u32::MAX` bytes, which the control code's byte counts cannot express, or
45    /// any error from issuing or completing the control operation.
46    ///
47    /// # Safety
48    ///
49    /// `code`'s input layout must be *self-contained*: the driver may read or
50    /// write only the bytes inside `input` and the output buffer, never memory
51    /// reached through a pointer embedded in `input`. Some control codes take an
52    /// input structure that carries raw pointers to separate buffers --
53    /// `SCSI_PASS_THROUGH_DIRECT::DataBuffer` is one -- which this adapter neither
54    /// owns nor keeps alive. For such a code the caller must keep every referenced
55    /// buffer valid for the whole call; the adapter cannot, because it does not
56    /// know the code's layout. This is why the generic raw-code seam is `unsafe`
57    /// even though a self-contained code (an `FSCTL` query, say) needs nothing
58    /// more than owned buffers.
59    pub unsafe fn ioctl(
60        &mut self,
61        code: u32,
62        input: &[u8],
63        output: &mut [u8],
64    ) -> io::Result<usize> {
65        let in_len = checked_len(input.len(), "input")?;
66        let out_len = checked_len(output.len(), "output")?;
67
68        let in_ptr = in_ptr(input.as_ptr(), in_len);
69        let out_ptr = out_ptr(output.as_mut_ptr(), out_len);
70
71        let mut operation = Operation::new(());
72        // SAFETY: issues exactly one DeviceIoControl reading `input` and writing
73        // `output`, both valid for the whole blocking call; no other operation is
74        // outstanding.
75        unsafe {
76            self.run(&mut operation, |handle, overlapped| {
77                let ok = DeviceIoControl(
78                    handle.as_raw_handle(),
79                    code,
80                    in_ptr,
81                    in_len,
82                    out_ptr,
83                    out_len,
84                    std::ptr::null_mut(),
85                    overlapped,
86                );
87                classify(ok)
88            })
89        }
90    }
91}
92
93/// The input buffer pointer, `NULL` for an empty buffer.
94///
95/// A control code that takes no input must be given `NULL` rather than a
96/// dangling-but-nonnull pointer, which is what an empty buffer's `stable_ptr`
97/// legitimately is.
98fn in_ptr(ptr: *const u8, len: u32) -> *const c_void {
99    if len == 0 {
100        std::ptr::null()
101    } else {
102        ptr.cast()
103    }
104}
105
106/// The output buffer pointer, `NULL` for an empty buffer.
107fn out_ptr(ptr: *mut u8, len: u32) -> *mut c_void {
108    if len == 0 {
109        std::ptr::null_mut()
110    } else {
111        ptr.cast()
112    }
113}
114
115/// Map a native `BOOL` into the submission-seam contract: native success or
116/// `ERROR_IO_PENDING` is accepted, any other error is an immediate failure.
117fn classify(ok: i32) -> io::Result<()> {
118    if ok != 0 {
119        return Ok(());
120    }
121    let error = io::Error::last_os_error();
122    if error.raw_os_error() == Some(ERROR_IO_PENDING as i32) {
123        Ok(())
124    } else {
125        Err(error)
126    }
127}
128
129/// Convert a buffer length to the `u32` byte count `DeviceIoControl` takes.
130///
131/// Rejects rather than caps. Capping would submit a prefix of the caller's
132/// input, or tell the device an output buffer is smaller than it is, and report
133/// success for an operation that did something other than what was asked.
134fn checked_len(len: usize, which: &str) -> io::Result<u32> {
135    u32::try_from(len).map_err(|_| {
136        io::Error::new(
137            io::ErrorKind::InvalidInput,
138            format!(
139                "a DeviceIoControl {which} buffer is limited to u32::MAX bytes; {len} does not fit"
140            ),
141        )
142    })
143}
144
145/// The pinned payload for an in-flight device-control operation: the input and
146/// output buffers, both of which must outlive the async call.
147struct DeviceIoPayload<I, O> {
148    /// Kept alive (pinned, via this struct) for as long as the operation is
149    /// outstanding -- the driver reads it for the whole call -- but never
150    /// read back through this field: `ioctl` captures its stable pointer
151    /// before submission (PR #20 review response) and `claim`/`finish_device`
152    /// drop it unread once the operation completes (the common case only
153    /// wants `output` back).
154    #[allow(dead_code)]
155    input: I,
156    output: O,
157}
158
159impl AssociatedEndpoint<'_> {
160    /// Submit an overlapped `DeviceIoControl` with control code `code`.
161    ///
162    /// Returns [`Started::Pending`] with a [`DeviceIoControlIo`] token that
163    /// recovers the output buffer and byte count from the operation's
164    /// completion, or -- only on an endpoint in
165    /// `FILE_SKIP_COMPLETION_PORT_ON_SUCCESS` mode, where a synchronous success
166    /// queues no packet -- [`Started::Completed`] with the output buffer already
167    /// in hand.
168    ///
169    /// `input` is the input buffer (empty for control codes that take none) and
170    /// `output` is the buffer the device writes its result into. Both are owned
171    /// buffers of the caller's choosing, handed over for the operation's life
172    /// and returned when it completes: nothing is copied and nothing is
173    /// allocated here.
174    ///
175    /// # Errors
176    ///
177    /// Returns [`io::ErrorKind::InvalidInput`] if either buffer is longer than
178    /// `u32::MAX` bytes, which the control code's byte counts cannot express, or
179    /// any immediate failure from issuing the control operation.
180    ///
181    /// # Safety
182    ///
183    /// `code`'s input layout must be *self-contained*: the driver may read or
184    /// write only the bytes inside `input` and the output buffer, never memory
185    /// reached through a pointer embedded in `input`. Some control codes take an
186    /// input structure that carries raw pointers to separate buffers --
187    /// `SCSI_PASS_THROUGH_DIRECT::DataBuffer` is one -- which this adapter neither
188    /// owns nor keeps alive. Because the operation outlives this call, such a
189    /// pointee could be freed while the driver is still using it. For such a code
190    /// the caller must keep every referenced buffer alive until the operation
191    /// completes; the adapter cannot, because it does not know the code's layout.
192    /// This is why the generic raw-code seam is `unsafe` even though a
193    /// self-contained code (an `FSCTL` query, say) needs nothing more than owned
194    /// buffers.
195    #[track_caller]
196    pub unsafe fn ioctl<I: IoBuf, O: IoBufMut>(
197        &self,
198        code: u32,
199        input: I,
200        mut output: O,
201    ) -> io::Result<Started<DeviceIoControlIo<I, O>, O>> {
202        // The lengths are captured here rather than measured inside the
203        // submission closure, which runs at the FFI boundary and cannot report
204        // an error.
205        let in_len = checked_len(input.bytes_len(), "input")?;
206        let out_len = checked_len(output.bytes_len(), "output")?;
207        let skip = self.notification_modes().skip_completion_port_on_success;
208        // Captured before submission too, alongside the lengths above (PR #20
209        // review response): `IoBuf`/`IoBufMut` are safe trait methods a
210        // caller's own buffer type implements, and `submit`'s safety contract
211        // forbids the closure unwinding -- a panic here, before the operation
212        // is registered, is merely an ordinary panic, where one from inside
213        // the closure would leave the operation permanently outstanding.
214        let in_ptr = in_ptr(input.stable_ptr(), in_len);
215        let out_ptr = out_ptr(output.stable_mut_ptr(), out_len);
216
217        let operation = Operation::new(DeviceIoPayload { input, output });
218        // SAFETY: issues exactly one DeviceIoControl reading from `in_ptr` and
219        // writing into `out_ptr` (captured above; identical to what the
220        // pinned payload would report, per `IoBuf`/`IoBufMut`'s
221        // address-stability contract); they and the byte-count cell live
222        // until the completion is claimed.
223        let submitted = unsafe {
224            self.submit(operation, |handle, overlapped| {
225                let bytes = sync_bytes_ptr_from_overlapped(overlapped);
226                let ok = DeviceIoControl(
227                    handle.as_raw_handle(),
228                    code,
229                    in_ptr,
230                    in_len,
231                    out_ptr,
232                    out_len,
233                    bytes,
234                    overlapped,
235                );
236                classify_issued(ok, skip, bytes)
237            })
238        };
239        finish_device(submitted)
240    }
241}
242
243/// Map a native `BOOL` into the IOCP submission contract.
244///
245/// # Why an immediate `TRUE` is usually `Pending`
246///
247/// [`Issued`] does not record whether `DeviceIoControl` finished synchronously.
248/// It records whether a **completion packet will arrive on the port**, and for
249/// an overlapped handle bound to an IOCP those are different facts: the I/O
250/// Manager queues a packet for every request it completes, *including* one that
251/// succeeds immediately without returning `ERROR_IO_PENDING`. See
252/// [`Issued::Pending`] for the full statement of that rule.
253///
254/// The single exception is `skip_on_success`, which is why this needs to know
255/// it: on an endpoint in `FILE_SKIP_COMPLETION_PORT_ON_SUCCESS` mode no packet
256/// is queued for an immediate success, so that -- and only that -- is an
257/// [`Issued::Completed`]. Both directions of getting this wrong are serious.
258/// Answering `Completed` when a packet is coming tells the port to reclaim the
259/// operation's storage inline, and the packet then arrives carrying a dangling
260/// `OVERLAPPED` -- a use-after-free on claim. Answering `Pending` when none is
261/// coming leaves the operation counted as outstanding forever, so
262/// [`crate::CompletionPort::run_down`] spins waiting for a packet that will
263/// never be queued.
264///
265/// # Safety
266///
267/// `sync_bytes` must be the byte-count cell of the operation being submitted,
268/// which is live for the whole call.
269unsafe fn classify_issued(
270    ok: i32,
271    skip_on_success: bool,
272    sync_bytes: *mut u32,
273) -> io::Result<Issued> {
274    if ok != 0 {
275        if skip_on_success {
276            // SAFETY: the call reported immediate success, so the kernel has
277            // already written the count and will not write it again.
278            let bytes_transferred = unsafe { *sync_bytes };
279            return Ok(Issued::Completed { bytes_transferred });
280        }
281        return Ok(Issued::Pending);
282    }
283    let error = io::Error::last_os_error();
284    if error.raw_os_error() == Some(ERROR_IO_PENDING as i32) {
285        Ok(Issued::Pending)
286    } else {
287        Err(error)
288    }
289}
290
291/// Turn a device submission outcome into the adapter's two-state outcome.
292fn finish_device<I: IoBuf, O: IoBufMut>(
293    submitted: Submitted<DeviceIoPayload<I, O>>,
294) -> io::Result<Started<DeviceIoControlIo<I, O>, O>> {
295    match submitted {
296        Submitted::Pending(id) => Ok(Started::Pending(DeviceIoControlIo {
297            id,
298            buffers: std::marker::PhantomData,
299        })),
300        Submitted::Completed {
301            operation,
302            bytes_transferred,
303        } => Ok(Started::Completed {
304            payload: operation.into_payload().output,
305            bytes_transferred: bytes_transferred as usize,
306        }),
307        Submitted::Failed { error, .. } => Err(error),
308    }
309}
310
311/// A pending device-control operation submitted through
312/// [`AssociatedEndpoint::ioctl`].
313///
314/// The token carries the operation's identity and remembers both buffer types it
315/// was submitted with, so [`DeviceIoControlIo::claim`] hands back the caller's
316/// own output buffer -- the same value, not a copy -- once the matching
317/// completion is dequeued. The input type is carried too, because it is part of
318/// the payload type the claim must name.
319#[derive(Debug)]
320pub struct DeviceIoControlIo<I, O> {
321    id: OperationId,
322    /// The buffers live in the pinned operation, not here; this only keeps the
323    /// token's type tied to them so `claim` cannot be handed the wrong payload.
324    buffers: std::marker::PhantomData<fn() -> (I, O)>,
325}
326
327impl<I: IoBuf, O: IoBufMut> DeviceIoControlIo<I, O> {
328    /// The identity of the in-flight operation, for cancellation or matching.
329    #[must_use]
330    pub fn id(&self) -> OperationId {
331        self.id
332    }
333
334    /// Claim this operation's result from `completion`.
335    ///
336    /// On a match returns `Ok((output, result))`: `output` is the buffer the
337    /// caller handed over (valid up to the byte count) and `result` is the byte
338    /// count or the operation's error. Returns `Err(self)` when `completion`
339    /// belongs to a different operation.
340    ///
341    /// The input buffer is dropped here: the driver is done reading it, and
342    /// returning both would make the common case pay for the rare one.
343    pub fn claim(self, completion: &Completion) -> Result<(O, io::Result<usize>), Self> {
344        if completion.id() != Some(self.id) {
345            return Err(self);
346        }
347        // SAFETY: the full identity -- address *and* generation -- matches, which
348        // an address alone would not: a recycled address can belong to a later
349        // operation of a different payload type. The match therefore proves this
350        // completion is the Operation<DeviceIoPayload<I, O>> this token
351        // submitted, and the token's own type parameters name that payload;
352        // claim it exactly once.
353        let operation = unsafe { completion.claim::<DeviceIoPayload<I, O>>() };
354        let output = operation.into_payload().output;
355        let result = match completion.error() {
356            Some(error) => Err(io::Error::from_raw_os_error(
357                error.raw_os_error().unwrap_or_default(),
358            )),
359            None => Ok(completion.bytes_transferred() as usize),
360        };
361        Ok((output, result))
362    }
363}
364
365#[cfg(test)]
366mod tests;