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;