Skip to main content

windows_overlapped_io_sys/
identity.rs

1// Copyright (c) 2026 Mike Grier
2//! Operation identity and the live-identity registry shared by the backends.
3//!
4//! An operation's `OVERLAPPED` address alone is not a durable name for it.
5//! Reclaiming an operation returns that address to the allocator, which may hand
6//! it to a later operation, so an address retained past its operation's
7//! completion can silently name a different, live operation. Cancellation acts
8//! purely on that address, so without more information a stale name would cancel
9//! the wrong operation through an entirely safe API.
10//!
11//! [`OperationId`] therefore pairs the address with a process-wide monotonic
12//! generation taken at submission, and each backend records its live identities
13//! in an [`OperationRegistry`]. Cancellation consults the registry first: an
14//! identity whose address is not live, or is live under a different generation,
15//! is rejected instead of being passed to the kernel.
16
17use std::collections::HashMap;
18use std::collections::hash_map::Entry;
19use std::io;
20use std::sync::atomic::{AtomicU64, Ordering};
21use std::sync::{Condvar, Mutex, MutexGuard};
22
23use windows_sys::Win32::System::IO::OVERLAPPED;
24
25/// Source of the process-wide generation sequence.
26///
27/// Generations start at 1, so 0 is never a generation any real submission was
28/// given. The sequence is process-wide rather than per-backend so that an
29/// identity minted by one backend object can never collide with one minted by
30/// another.
31static NEXT_GENERATION: AtomicU64 = AtomicU64::new(1);
32
33/// Take the next generation from `sequence`, or `None` once it is exhausted.
34///
35/// This is the whole mechanism; [`next_generation`] only adds the panic. Having
36/// a non-panicking form is not merely tidy: the concurrent exhaustion test hits
37/// the boundary tens of thousands of times across several threads, and doing
38/// that through the panicking form would mean either a flood of panic output or
39/// worker threads swapping the *process-global* panic hook, which would race
40/// each other and silence diagnostics for every other test in the binary.
41///
42/// The counter is a parameter so the boundary can be tested; production code
43/// always passes [`NEXT_GENERATION`].
44fn try_next_generation(sequence: &AtomicU64) -> Option<u64> {
45    // A single atomic update, not an increment followed by a repair. `fetch_add`
46    // would wrap the stored value to zero and leave it there until a separate
47    // `store` could pin it -- and a thread arriving in that window would take 0,
48    // then 1, 2, ... and mint successfully, which is exactly the recycled
49    // generation this refuses to produce. Saturating inside the update means the
50    // counter never transiently holds a wrapped value, so there is no window.
51    sequence
52        .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
53            // `then`, not `then_some`: the latter is eager, so `current + 1`
54            // would overflow at the boundary before the guard could apply.
55            (current != u64::MAX).then(|| current + 1)
56        })
57        .ok()
58}
59
60/// Take the next generation from `sequence`, refusing to wrap.
61///
62/// Wrapping would restart the sequence and hand out generations already in use,
63/// which is exactly the stale-identity aliasing generations exist to prevent --
64/// so the invariant is enforced here rather than only asserted in prose.
65/// Exhaustion is not reachable in practice: at one submission per nanosecond a
66/// `u64` still takes centuries.
67///
68/// # Panics
69///
70/// Panics once the sequence is exhausted, and every later call panics too: the
71/// counter saturates at `u64::MAX` rather than passing it, so a caught panic
72/// cannot resume minting recycled generations.
73fn next_generation(sequence: &AtomicU64) -> u64 {
74    try_next_generation(sequence).unwrap_or_else(|| {
75        panic!(
76            "the operation-generation sequence is exhausted; continuing would reissue \
77             generations already in use and reintroduce stale-identity aliasing"
78        )
79    })
80}
81
82/// An identity for an in-flight operation: the address of its `OVERLAPPED`
83/// together with the generation stamped on it at submission.
84///
85/// The address must not be dereferenced or freed; the kernel owns the storage
86/// until the completion is claimed. The generation is what makes the identity
87/// durable: addresses are recycled when operations are reclaimed, but a given
88/// (address, generation) pair names exactly one submission for the life of the
89/// process. Retaining an identity past its operation's completion is therefore
90/// harmless -- the backend will reject it rather than act on whatever operation
91/// currently occupies that address.
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
93pub struct OperationId {
94    overlapped: *mut OVERLAPPED,
95    generation: u64,
96}
97
98// SAFETY: an identity is inert data -- an address and a number. It owns nothing,
99// and neither this type nor any backend ever dereferences the address: the
100// registry compares it, and cancellation passes it to `CancelIoEx` as an opaque
101// token. Moving one between threads is therefore no different from moving the
102// address as an integer.
103//
104// This matters because cancelling from a thread other than the submitting one is
105// the central use of an identity -- a timeout elsewhere aborting an in-flight
106// operation -- and the raw pointer would otherwise make the type `!Send` and put
107// that pattern out of reach.
108unsafe impl Send for OperationId {}
109unsafe impl Sync for OperationId {}
110
111impl OperationId {
112    /// Mint a new identity for an operation being submitted.
113    ///
114    /// Takes the next generation from the process-wide sequence, so every call
115    /// yields a distinct identity even when `overlapped` repeats an address used
116    /// by an earlier, already-reclaimed operation. A backend calls this exactly
117    /// once per submission, at the moment it hands the storage to the kernel.
118    ///
119    /// # Panics
120    ///
121    /// Panics if the process-wide generation sequence is exhausted, rather than
122    /// wrapping and reissuing generations already in use. A `u64` takes
123    /// centuries to exhaust at one submission per nanosecond, so this is a
124    /// guard on the type's uniqueness invariant rather than a reachable case.
125    #[must_use]
126    pub fn mint(overlapped: *mut OVERLAPPED) -> Self {
127        Self {
128            overlapped,
129            generation: next_generation(&NEXT_GENERATION),
130        }
131    }
132
133    /// Assemble an identity from an address and a generation chosen by the
134    /// caller, without checking that they belong together.
135    ///
136    /// Backends do not need this: [`OperationRegistry::remove`] and
137    /// [`OperationRegistry::identify`] hand back a whole `OperationId`,
138    /// assembled from the pair the registry itself recorded, so the normal path
139    /// from a completion to its identity never supplies a generation.
140    ///
141    /// This exists for tests that must synthesize an identity the registry never
142    /// issued -- a stale one, or one from a generation ahead of the current --
143    /// in order to prove such an identity is rejected.
144    ///
145    /// # Safety
146    ///
147    /// The caller must have observed `overlapped` and `generation` together as
148    /// one operation's identity, or must be deliberately forging an identity in
149    /// order to assert that it is refused.
150    ///
151    /// Forging is not memory-unsafe -- cancelling a live operation is
152    /// well-defined and no storage can be reclaimed twice by it -- but it defeats
153    /// the isolation the generation exists to provide. A caller holding `(p, g)`
154    /// could otherwise construct `(p, g + 1)` and, if the next submission reusing
155    /// `p` were stamped with that generation, cancel an operation it never
156    /// submitted. That is why this is not a safe constructor:
157    ///
158    /// ```compile_fail
159    /// # use windows_overlapped_io_sys::OperationId;
160    /// fn forge_the_next_one(observed: OperationId) -> OperationId {
161    ///     OperationId::forge(observed.as_ptr(), observed.generation() + 1)
162    /// }
163    /// ```
164    ///
165    /// The same call compiles once the caller takes on the obligation, so what
166    /// the example above rejects is the missing `unsafe` rather than anything
167    /// else about the code:
168    ///
169    /// ```
170    /// # use windows_overlapped_io_sys::OperationId;
171    /// fn rebuild(observed: OperationId) -> OperationId {
172    ///     // SAFETY: both halves came from one identity, so they were observed
173    ///     // together by construction.
174    ///     unsafe { OperationId::forge(observed.as_ptr(), observed.generation()) }
175    /// }
176    /// ```
177    #[must_use]
178    pub unsafe fn forge(overlapped: *mut OVERLAPPED, generation: u64) -> Self {
179        Self {
180            overlapped,
181            generation,
182        }
183    }
184
185    /// Assemble an identity the registry has just looked up, in-crate.
186    pub(crate) fn from_recorded_parts(overlapped: *mut OVERLAPPED, generation: u64) -> Self {
187        Self {
188            overlapped,
189            generation,
190        }
191    }
192
193    /// The `OVERLAPPED` pointer this identity refers to.
194    #[must_use]
195    pub fn as_ptr(self) -> *mut OVERLAPPED {
196        self.overlapped
197    }
198
199    /// The generation stamped on this identity at submission.
200    #[must_use]
201    pub fn generation(self) -> u64 {
202        self.generation
203    }
204}
205
206/// Lock a mutex, recovering the guard even if a previous holder panicked.
207///
208/// A poisoned lock here only means some callback panicked. The registry it
209/// protects is a plain map that the backends keep exact through guards that run
210/// while unwinding, so refusing to proceed would strand outstanding operations
211/// rather than protect anything.
212fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
213    mutex.lock().unwrap_or_else(|poison| poison.into_inner())
214}
215
216/// The set of identities a backend currently considers live.
217///
218/// An identity is registered while a backend still treats its operation as
219/// outstanding, and removed the moment the backend stops -- at *dequeue* for the
220/// IOCP backend and on *callback entry* for TP_IO, both of which precede the
221/// completion's or callback's own reclamation of the operation's storage. An
222/// empty registry therefore means only that no operation is still considered
223/// live; it is **not** proof that every operation's storage has been freed or
224/// that every callback has finished.
225///
226/// The registry is the single source of truth for both questions a backend must
227/// answer: how many operations are still live (rundown), and whether a given
228/// identity still names a live operation (cancellation). Keeping them in one
229/// structure means they cannot disagree.
230///
231/// At most one live operation exists per address at any moment -- the address is
232/// only reusable once the previous operation's storage has been freed -- so the
233/// map is keyed by address and holds the generation of whichever submission owns
234/// it now.
235///
236/// # Invariant enforced by this type
237///
238/// **An address must never be registered while it is available for reuse.**
239/// Equivalently: a backend must deregister an operation *before* anything can
240/// free its storage, never after. [`OperationRegistry::insert`] panics when this
241/// is violated, because the alternative is silent corruption -- two live
242/// operations sharing one map entry would make [`OperationRegistry::is_live`]
243/// answer for the wrong one and let a cancellation reach an unrelated operation,
244/// which is the exact class of bug generations exist to prevent.
245///
246/// The invariant is easy to break in a completion callback, where the natural
247/// place to deregister (after the callback returns) is *later* than the point at
248/// which the callback may free the storage -- for example by taking ownership of
249/// the operation and dropping it. Deregister on callback entry instead.
250#[derive(Debug)]
251pub struct OperationRegistry {
252    live: Mutex<HashMap<usize, u64>>,
253    drained: Condvar,
254}
255
256impl OperationRegistry {
257    /// Create an empty registry.
258    #[must_use]
259    pub fn new() -> Self {
260        Self {
261            live: Mutex::new(HashMap::new()),
262            drained: Condvar::new(),
263        }
264    }
265
266    /// Record a newly submitted operation.
267    ///
268    /// Call this once per submission, before the operation can complete.
269    ///
270    /// # Panics
271    ///
272    /// Panics if `id`'s address is **already registered** -- that is, if a
273    /// previous operation at the same storage address has not been removed with
274    /// [`OperationRegistry::remove`] yet.
275    ///
276    /// This is always a defect in the completion backend, never in the code
277    /// calling that backend, and it is deliberately a panic rather than a
278    /// silently-ignored duplicate: two live operations sharing one entry would
279    /// corrupt exactly the guarantee this registry provides, letting a
280    /// cancellation reach an operation the caller never named.
281    ///
282    /// The two ways a backend causes it:
283    ///
284    /// - **Deregistering too late.** If a completed operation is removed only
285    ///   *after* its storage is freed, the allocator can hand that address to a
286    ///   concurrent submission while the stale entry is still present. Remove the
287    ///   operation before anything can free it -- on completion-callback entry
288    ///   rather than on exit, since a callback may take ownership of the
289    ///   operation and drop it part-way through.
290    /// - **Submitting one operation's storage twice**, so a second submission
291    ///   reuses an `OVERLAPPED` that is still in flight.
292    pub fn insert(&self, id: OperationId) {
293        let mut live = lock(&self.live);
294        match live.entry(id.as_ptr() as usize) {
295            Entry::Occupied(occupied) => panic!(
296                "windows-overlapped-io-sys: operation storage {address:p} was registered for \
297                 generation {new} while generation {existing} was still registered at the same \
298                 address. An address must never be registered while it is available for reuse. \
299                 This is a defect in the completion backend: it either deregistered a completed \
300                 operation after its storage was freed rather than before (leaving a window in \
301                 which a concurrent submission can be handed the same address), or submitted one \
302                 operation's storage twice while it was still in flight.",
303                address = id.as_ptr(),
304                new = id.generation(),
305                existing = occupied.get(),
306            ),
307            Entry::Vacant(slot) => {
308                slot.insert(id.generation());
309            }
310        }
311    }
312
313    /// Deregister an operation the backend no longer considers live, waking any
314    /// rundown once the last one clears.
315    ///
316    /// Called when the backend stops treating the operation as outstanding
317    /// (dequeue for IOCP, callback entry for TP_IO), which precedes reclamation
318    /// of the storage. Returns the identity that was registered for the address,
319    /// if any -- assembled here from the pair this registry recorded, so a
320    /// backend never has to supply a generation and cannot get the pairing wrong.
321    pub fn remove(&self, overlapped: *mut OVERLAPPED) -> Option<OperationId> {
322        let mut live = lock(&self.live);
323        let generation = live.remove(&(overlapped as usize));
324        if live.is_empty() {
325            self.drained.notify_all();
326        }
327        generation.map(|generation| OperationId::from_recorded_parts(overlapped, generation))
328    }
329
330    /// Whether this exact identity -- address *and* generation -- is still live.
331    ///
332    /// A retained identity whose operation has completed returns `false` even if
333    /// its address has since been reissued to another operation.
334    ///
335    /// This is a snapshot, so it must not be used to guard a native cancellation:
336    /// the answer can be stale by the time the caller acts on it. Use
337    /// [`OperationRegistry::cancel_if_live`] for that.
338    #[must_use]
339    pub fn is_live(&self, id: OperationId) -> bool {
340        lock(&self.live).get(&(id.as_ptr() as usize)) == Some(&id.generation())
341    }
342
343    /// Run `cancel` only if `id` still names a live operation, holding the
344    /// registry guard across both the check and the call.
345    ///
346    /// Checking liveness and then cancelling as two steps is a race, not merely
347    /// an imprecision: between the two, the operation can complete and be
348    /// reclaimed, and a concurrent submission can be handed the same storage
349    /// address. The native cancel would then reach an unrelated live operation --
350    /// exactly what the generation is meant to prevent. Holding the guard closes
351    /// that window, because a submission cannot register a reused address until
352    /// it is released.
353    ///
354    /// `cancel` should perform only the native cancellation. It must not call
355    /// back into this registry, which would deadlock on the same non-reentrant
356    /// lock.
357    ///
358    /// # Errors
359    ///
360    /// Returns [`io::ErrorKind::NotFound`] without invoking `cancel` if `id` no
361    /// longer names a live operation, or whatever `cancel` returns.
362    pub fn cancel_if_live<F>(&self, id: OperationId, cancel: F) -> io::Result<()>
363    where
364        F: FnOnce() -> io::Result<()>,
365    {
366        let live = lock(&self.live);
367        if live.get(&(id.as_ptr() as usize)) != Some(&id.generation()) {
368            return Err(io::Error::new(
369                io::ErrorKind::NotFound,
370                "the operation named by this identity is no longer outstanding",
371            ));
372        }
373        // The guard is deliberately still held: releasing it here would reopen
374        // the window this function exists to close.
375        let result = cancel();
376        drop(live);
377        result
378    }
379
380    /// The identity currently registered for an address, if it is live.
381    ///
382    /// A backend uses this to recover the full identity of an operation it knows
383    /// only by address, as when a completion arrives carrying its `OVERLAPPED`.
384    /// The generation comes from this registry rather than from the caller, so
385    /// the returned identity always names the operation that is actually live at
386    /// that address -- there is no pairing for a caller to get wrong, or to
387    /// choose.
388    #[must_use]
389    pub fn identify(&self, overlapped: *mut OVERLAPPED) -> Option<OperationId> {
390        lock(&self.live)
391            .get(&(overlapped as usize))
392            .copied()
393            .map(|generation| OperationId::from_recorded_parts(overlapped, generation))
394    }
395
396    /// The number of identities the backend currently considers live.
397    #[must_use]
398    pub fn len(&self) -> usize {
399        lock(&self.live).len()
400    }
401
402    /// Whether no operation is still considered live.
403    #[must_use]
404    pub fn is_empty(&self) -> bool {
405        self.len() == 0
406    }
407
408    /// Block until the registry is empty -- until every operation the backend
409    /// still considers live has been removed.
410    ///
411    /// Removal happens when a backend stops considering an operation live
412    /// (dequeue for IOCP, callback entry for TP_IO), which precedes the
413    /// completion's reclamation of the storage, so this waits for liveness to
414    /// clear, not for every allocation to be freed. The caller must have arranged
415    /// for the outstanding operations to complete -- by cancelling them, or
416    /// because they are destined to finish -- or this waits indefinitely. It is
417    /// used by backends whose completions arrive on threads the owner does not
418    /// drive.
419    pub fn wait_until_empty(&self) {
420        let mut live = lock(&self.live);
421        while !live.is_empty() {
422            live = self
423                .drained
424                .wait(live)
425                .unwrap_or_else(|poison| poison.into_inner());
426        }
427    }
428}
429
430impl Default for OperationRegistry {
431    fn default() -> Self {
432        Self::new()
433    }
434}
435
436#[cfg(test)]
437mod tests;