windows_file_enumeration_sys/admission.rs
1// Copyright (c) 2026 Mike Grier
2//! Admission: the point at which an enumeration becomes the session's problem.
3//!
4//! # Everything that can fail, fails here
5//!
6//! Admission is where a request stops being the caller's and starts being the
7//! session's, so it is also the last place a failure can be reported to the
8//! caller's own thread. Three things are therefore secured *before* the begin
9//! message becomes visible to the servicer:
10//!
11//! 1. the submitter's security context, captured synchronously;
12//! 2. a completion-ring slot for the outcome the enumeration will owe; and
13//! 3. a submission-ring slot for the cancellation it may later need.
14//!
15//! Once all three are in hand the begin cannot be half-accepted: either it
16//! enters the ring and the session owes exactly one terminal, or it is refused
17//! and the caller gets its request -- and its captured token -- back.
18//!
19//! # Why the token is captured by the submitter
20//!
21//! The directory is opened on a thread-pool worker, whose own identity is
22//! whatever the pool last left there. Capturing at submission and carrying the
23//! context in the message is what makes the open happen under the identity of
24//! whoever asked for it. Capturing later, on the worker, would silently
25//! enumerate under the process or worker identity -- the exact defect this layer
26//! exists to prevent.
27//!
28//! A second form takes an already-captured context, so a traversal layer can
29//! capture once for a whole tree instead of once per directory.
30//!
31//! # Why the handle is affine
32//!
33//! An [`EnumerationHandle`] owns its enumeration's cancellation reservation, so
34//! cancelling never has to find room in a ring that ordinary traffic may have
35//! filled -- and `Drop` never has to report a failure it has nowhere to report.
36//! Dropping the handle therefore cancels. A caller that wants an enumeration to
37//! outlive its handle says so with [`EnumerationHandle::detach`], which gives
38//! the reservation back rather than spending it.
39
40use windows_impersonation_token_sys::{CaptureError, ImpersonationToken};
41
42use crate::buffer::NativeBuffer;
43use crate::completion::EnumerationId;
44use crate::engine::EngineState;
45use crate::error::{BeginError, BeginFailure};
46use crate::request::EnumerationRequest;
47use crate::session::SessionShared;
48use crate::submission_ring::{
49 BeginMessage, CancelSlot, ControlMessage, SubmitRejection, release_cancel_slot,
50 release_retire_slot,
51};
52use std::sync::Arc;
53
54/// A live enumeration's affine handle.
55///
56/// Holding one is what keeps an enumeration running: dropping it asks the
57/// session to stop that enumeration. The handle is not clonable and not
58/// copyable, so exactly one owner decides when the enumeration ends.
59///
60/// Cancellation is asynchronous. It enters the submission ring like every other
61/// control operation and takes effect when the servicer reaches it, so entries
62/// already queued still arrive and exactly one terminal outcome still follows
63/// them.
64#[must_use = "dropping the handle cancels the enumeration; use `detach` to let it run"]
65pub struct EnumerationHandle {
66 enumeration: EnumerationId,
67 shared: Arc<SessionShared>,
68 /// Taken at admission, spent by exactly one of cancel, detach, or drop.
69 cancel: Option<CancelSlot>,
70}
71
72impl EnumerationHandle {
73 pub(crate) fn new(
74 enumeration: EnumerationId,
75 shared: Arc<SessionShared>,
76 cancel: CancelSlot,
77 ) -> Self {
78 Self {
79 enumeration,
80 shared,
81 cancel: Some(cancel),
82 }
83 }
84
85 /// Which enumeration this handle controls.
86 ///
87 /// Completion records carry the same identifier, which is how a caller
88 /// attributes them when several enumerations share one session.
89 #[must_use]
90 pub fn id(&self) -> EnumerationId {
91 self.enumeration
92 }
93
94 /// Ask the session to stop this enumeration.
95 ///
96 /// Returns immediately. Cancellation cannot preempt a directory query that
97 /// is already executing, so entries produced before it is observed are still
98 /// delivered, followed by one
99 /// [`Cancelled`](crate::TerminalOutcome::Cancelled) terminal.
100 pub fn cancel(mut self) {
101 self.enqueue_cancel();
102 }
103
104 /// Give up control of this enumeration and let it run to completion.
105 ///
106 /// The cancellation reservation returns to the submission ring, so a
107 /// detached enumeration costs the session nothing beyond its terminal slot.
108 /// It still reports its outcome; a caller simply loses the ability to stop
109 /// it early.
110 pub fn detach(mut self) {
111 if let Some(slot) = self.cancel.take() {
112 release_cancel_slot(&self.shared.submissions, slot);
113 }
114 }
115
116 /// Spend the reservation on a cancellation, if it has not been spent.
117 fn enqueue_cancel(&mut self) {
118 let Some(slot) = self.cancel.take() else {
119 return;
120 };
121 let pushed = self.shared.submissions.push_cancel(slot, self.enumeration);
122 self.shared.ring_servicer(pushed);
123 }
124}
125
126impl Drop for EnumerationHandle {
127 fn drop(&mut self) {
128 // Infallible by construction: the reservation was taken at admission
129 // precisely so that this path has nowhere to fail.
130 self.enqueue_cancel();
131 }
132}
133
134impl std::fmt::Debug for EnumerationHandle {
135 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136 f.debug_struct("EnumerationHandle")
137 .field("enumeration", &self.enumeration)
138 .field("cancellable", &self.cancel.is_some())
139 .finish_non_exhaustive()
140 }
141}
142
143/// Admit one request, capturing the caller's current security context.
144pub(crate) fn try_begin(
145 shared: &Arc<SessionShared>,
146 request: EnumerationRequest,
147) -> Result<EnumerationHandle, BeginError> {
148 // Captured before anything else is claimed, so a capture failure costs no
149 // reservations and leaves the rings exactly as they were.
150 let token = match ImpersonationToken::capture() {
151 Ok(token) => token,
152 Err(error) => return Err(BeginError::capture(request, error)),
153 };
154 try_begin_with_token(shared, request, token)
155}
156
157/// Admit one request under an already-captured security context.
158pub(crate) fn try_begin_with_token(
159 shared: &Arc<SessionShared>,
160 request: EnumerationRequest,
161 token: ImpersonationToken,
162) -> Result<EnumerationHandle, BeginError> {
163 if shared.submissions.is_abandoned() {
164 return Err(BeginError::rejected(
165 BeginFailure::Abandoned,
166 request,
167 Some(token),
168 ));
169 }
170
171 let Some(cancel) = shared.submissions.reserve_cancel() else {
172 return Err(BeginError::rejected(
173 BeginFailure::SubmissionRingFull,
174 request,
175 Some(token),
176 ));
177 };
178 // Claimed here too, so a worker can always report itself finished. Without
179 // it a completed enumeration would strand its registry entry, and with it
180 // the token, handle, and buffer that entry holds.
181 let Some(retire) = shared.submissions.reserve_retire() else {
182 release_cancel_slot(&shared.submissions, cancel);
183 return Err(BeginError::rejected(
184 BeginFailure::SubmissionRingFull,
185 request,
186 Some(token),
187 ));
188 };
189 let enumeration = shared.next_enumeration_id();
190 let Some(terminal) = shared.completions.reserve_terminal(enumeration) else {
191 release_cancel_slot(&shared.submissions, cancel);
192 release_retire_slot(&shared.submissions, retire);
193 return Err(BeginError::rejected(
194 BeginFailure::CompletionRingFull,
195 request,
196 Some(token),
197 ));
198 };
199 // The last thing that can fail. Allocated here rather than in the request
200 // because the buffer belongs to the enumeration it serves: a request is a
201 // cheap, clonable, comparable description that may be submitted more than
202 // once and is handed straight back when a begin is refused.
203 let Some(buffer) = NativeBuffer::try_new(request.buffer_capacity()) else {
204 release_cancel_slot(&shared.submissions, cancel);
205 release_retire_slot(&shared.submissions, retire);
206 drop(terminal);
207 return Err(BeginError::rejected(
208 BeginFailure::BufferAllocation,
209 request,
210 Some(token),
211 ));
212 };
213
214 let message = ControlMessage::Begin(Box::new(BeginMessage {
215 enumeration,
216 engine: EngineState::new(request, token, buffer),
217 terminal,
218 retire,
219 }));
220 match shared.submissions.try_push(message) {
221 Ok(pushed) => {
222 shared.ring_servicer(pushed);
223 Ok(EnumerationHandle::new(
224 enumeration,
225 Arc::clone(shared),
226 cancel,
227 ))
228 }
229 Err((message, rejection)) => {
230 // Nothing was accepted, so every claim made above is given back and
231 // the caller's request and token are returned intact. Dropping the
232 // message's terminal slot releases its completion-ring reservation.
233 release_cancel_slot(&shared.submissions, cancel);
234 let (request, token) = match message {
235 ControlMessage::Begin(begin) => {
236 let begin = *begin;
237 release_retire_slot(&shared.submissions, begin.retire);
238 // The buffer goes with the engine state, which is dropped
239 // here: nothing was accepted, so nothing keeps it.
240 begin.engine.into_parts()
241 }
242 _ => unreachable!("the message pushed above is always a begin"),
243 };
244 let failure = match rejection {
245 SubmitRejection::Full => BeginFailure::SubmissionRingFull,
246 SubmitRejection::Abandoned => BeginFailure::Abandoned,
247 };
248 Err(BeginError::rejected(failure, request, Some(token)))
249 }
250 }
251}
252
253/// The capture error behind a [`BeginFailure::TokenCapture`], re-exported so a
254/// caller can inspect it without depending on the sibling crate by name.
255pub type TokenCaptureError = CaptureError;
256
257#[cfg(test)]
258mod tests;