Skip to main content

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;