Skip to main content

windows_impersonation_token_sys/
lib.rs

1// Copyright (c) 2026 Mike Grier
2//! Memory-safe capture, transport, and scoped application of Windows
3//! impersonation tokens.
4//!
5//! [`ImpersonationToken`] captures the calling thread's effective security
6//! context into an owned impersonation token that can be carried across
7//! threads without exposing its raw handle or mutation rights.
8//!
9//! # Capture contract
10//!
11//! Capture is synchronous. If the calling thread is impersonating, the
12//! captured token preserves its identification, impersonation, or delegation
13//! level. If the thread has no token, capture snapshots the process identity as
14//! a `SecurityImpersonation` token. Windows does not permit anonymous
15//! impersonation contexts to be opened, so they are rejected as
16//! [`CaptureFailure::AnonymousContext`].
17//!
18//! # Application and restoration
19//!
20//! [`ImpersonationToken::with_impersonation`] applies the captured token only
21//! for the dynamic extent of a closure. Before applying it, the method opens a
22//! handle to the exact thread-token object present on entry, or records that
23//! the thread had no token. That exact state is restored on ordinary return and
24//! during unwinding; restoration never substitutes a duplicate token and never
25//! uses `RevertToSelf`.
26//!
27//! A failure to save the entry state or apply the captured token is returned as
28//! [`ApplyError`] before the closure runs. A failure to restore the entry state
29//! panics because continuing to use a shared worker thread under an unknown
30//! identity would be unsafe. If the closure is already unwinding, the resulting
31//! double panic aborts the process.
32//!
33//! # Security invariants
34//!
35//! - The captured handle is owned, non-inheritable, and grants only
36//!   `TOKEN_IMPERSONATE`.
37//! - Clones share the same immutable token object; no safe API exposes its
38//!   handle or permits token mutation or rights expansion.
39//! - The private restoration guard cannot move to or be shared with another
40//!   thread.
41//! - The public API is closure-only, so safe code cannot forget the restoration
42//!   guard.
43//!
44//! # Example
45//!
46//! Capture once, move the owned token to a worker, and scope access-checked work
47//! to that context:
48//!
49//! ```no_run
50//! use std::thread;
51//! use windows_impersonation_token_sys::ImpersonationToken;
52//!
53//! let token = ImpersonationToken::capture()?;
54//! let worker = thread::spawn(move || {
55//!     token.with_impersonation(|| {
56//!         // Perform access-checked Windows work here.
57//!     })
58//! });
59//!
60//! worker.join().expect("worker panicked")?;
61//! # Ok::<(), Box<dyn std::error::Error>>(())
62//! ```
63
64#![cfg(windows)]
65#![forbid(unsafe_op_in_unsafe_fn)]
66#![warn(missing_docs)]
67
68mod restore;
69
70use std::fmt;
71use std::io;
72use std::marker::PhantomData;
73use std::mem::size_of;
74use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle};
75use std::ptr;
76use std::rc::Rc;
77use std::sync::Arc;
78
79use windows_sys::Win32::Foundation::{ERROR_CANT_OPEN_ANONYMOUS, ERROR_NO_TOKEN, FALSE, TRUE};
80use windows_sys::Win32::Security::{
81    DuplicateTokenEx, GetTokenInformation, SECURITY_IMPERSONATION_LEVEL, SecurityAnonymous,
82    SecurityImpersonation, TOKEN_ACCESS_MASK, TOKEN_DUPLICATE, TOKEN_IMPERSONATE, TOKEN_QUERY,
83    TokenImpersonation, TokenImpersonationLevel,
84};
85use windows_sys::Win32::System::Threading::{
86    GetCurrentProcess, GetCurrentThread, OpenProcessToken, OpenThreadToken, SetThreadToken,
87};
88
89const THREAD_TOKEN_CAPTURE_ACCESS: TOKEN_ACCESS_MASK = TOKEN_DUPLICATE | TOKEN_QUERY;
90const PROCESS_TOKEN_CAPTURE_ACCESS: TOKEN_ACCESS_MASK = TOKEN_DUPLICATE;
91const CAPTURED_TOKEN_ACCESS: TOKEN_ACCESS_MASK = TOKEN_IMPERSONATE;
92
93/// The stage at which an impersonation context could not be captured.
94#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
95#[non_exhaustive]
96pub enum CaptureFailure {
97    /// The current thread is impersonating at `SecurityAnonymous`, whose token
98    /// Windows does not permit callers to open or transport.
99    AnonymousContext,
100    /// Windows could not open the current thread token.
101    OpenThreadToken,
102    /// The thread had no token and Windows could not open the process token.
103    OpenProcessToken,
104    /// Windows could not report the thread token's impersonation level.
105    QueryImpersonationLevel,
106    /// Windows could not duplicate the effective token into the captured form.
107    DuplicateToken,
108}
109
110/// A synchronous failure while capturing the calling thread's effective
111/// impersonation context.
112#[derive(Debug)]
113pub struct CaptureError {
114    failure: CaptureFailure,
115    source: io::Error,
116}
117
118impl CaptureError {
119    fn new(failure: CaptureFailure, source: io::Error) -> Self {
120        Self { failure, source }
121    }
122
123    fn anonymous() -> Self {
124        Self::new(
125            CaptureFailure::AnonymousContext,
126            io::Error::from_raw_os_error(
127                i32::try_from(ERROR_CANT_OPEN_ANONYMOUS)
128                    .expect("ERROR_CANT_OPEN_ANONYMOUS fits in i32"),
129            ),
130        )
131    }
132
133    /// The capture stage that failed.
134    #[must_use]
135    pub fn failure(&self) -> CaptureFailure {
136        self.failure
137    }
138
139    /// The underlying Win32 error code.
140    #[must_use]
141    pub fn raw_os_error(&self) -> Option<i32> {
142        self.source.raw_os_error()
143    }
144}
145
146impl fmt::Display for CaptureError {
147    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148        let stage = match self.failure {
149            CaptureFailure::AnonymousContext => "anonymous impersonation context",
150            CaptureFailure::OpenThreadToken => "OpenThreadToken",
151            CaptureFailure::OpenProcessToken => "OpenProcessToken",
152            CaptureFailure::QueryImpersonationLevel => {
153                "GetTokenInformation(TokenImpersonationLevel)"
154            }
155            CaptureFailure::DuplicateToken => "DuplicateTokenEx",
156        };
157
158        write!(f, "{stage}: {}", self.source)
159    }
160}
161
162impl std::error::Error for CaptureError {
163    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
164        Some(&self.source)
165    }
166}
167
168/// The stage at which a captured token could not be applied.
169#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
170#[non_exhaustive]
171pub enum ApplyFailure {
172    /// Windows could not open the current thread token for exact restoration.
173    SavePreviousToken,
174    /// Windows could not assign the captured token to the current thread.
175    ApplyToken,
176}
177
178/// A synchronous failure while applying a captured impersonation context.
179#[derive(Debug)]
180pub struct ApplyError {
181    failure: ApplyFailure,
182    source: io::Error,
183}
184
185impl ApplyError {
186    fn new(failure: ApplyFailure, source: io::Error) -> Self {
187        Self { failure, source }
188    }
189
190    /// The application stage that failed.
191    #[must_use]
192    pub fn failure(&self) -> ApplyFailure {
193        self.failure
194    }
195
196    /// The underlying Win32 error code.
197    #[must_use]
198    pub fn raw_os_error(&self) -> Option<i32> {
199        self.source.raw_os_error()
200    }
201}
202
203impl fmt::Display for ApplyError {
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        let stage = match self.failure {
206            ApplyFailure::SavePreviousToken => "OpenThreadToken for exact restoration",
207            ApplyFailure::ApplyToken => "SetThreadToken",
208        };
209
210        write!(f, "{stage}: {}", self.source)
211    }
212}
213
214impl std::error::Error for ApplyError {
215    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
216        Some(&self.source)
217    }
218}
219
220/// An owned, immutable snapshot of a Windows impersonation context.
221///
222/// Cloning this value shares ownership of the same captured token. The native
223/// handle is real rather than pseudo or borrowed, is not inheritable, and has
224/// only `TOKEN_IMPERSONATE` access. No safe API exposes that handle or permits
225/// callers to mutate the captured token. The value is safe to send to and share
226/// between threads; each call to [`Self::with_impersonation`] affects only the
227/// calling thread for the duration of its closure.
228#[must_use = "dropping the token discards the captured impersonation context"]
229pub struct ImpersonationToken {
230    handle: Arc<OwnedHandle>,
231}
232
233impl Clone for ImpersonationToken {
234    fn clone(&self) -> Self {
235        Self {
236            handle: Arc::clone(&self.handle),
237        }
238    }
239}
240
241impl ImpersonationToken {
242    /// Captures the calling thread's effective Windows security context.
243    ///
244    /// If the thread is impersonating, capture preserves its identification,
245    /// impersonation, or delegation level. If the thread has no token, capture
246    /// snapshots the process token as a `SecurityImpersonation` token. The
247    /// captured handle is non-inheritable and grants only `TOKEN_IMPERSONATE`.
248    ///
249    /// # Errors
250    ///
251    /// Returns a [`CaptureError`] synchronously when the effective token cannot
252    /// be opened, inspected, or duplicated. Anonymous impersonation is reported
253    /// as [`CaptureFailure::AnonymousContext`].
254    pub fn capture() -> Result<Self, CaptureError> {
255        let source = SourceToken::open()?;
256        let mut captured = ptr::null_mut();
257
258        // SAFETY: source.handle is a live token handle with TOKEN_DUPLICATE;
259        // captured points to writable storage. Null security attributes make
260        // the new TOKEN_IMPERSONATE-only handle non-inheritable.
261        let duplicated = unsafe {
262            DuplicateTokenEx(
263                source.handle.as_raw_handle(),
264                CAPTURED_TOKEN_ACCESS,
265                ptr::null(),
266                source.level,
267                TokenImpersonation,
268                &raw mut captured,
269            )
270        };
271        if duplicated == FALSE {
272            return Err(CaptureError::new(
273                CaptureFailure::DuplicateToken,
274                io::Error::last_os_error(),
275            ));
276        }
277
278        // SAFETY: successful DuplicateTokenEx returns a new, owned token handle
279        // that must be released with CloseHandle, which OwnedHandle does.
280        let handle = unsafe { OwnedHandle::from_raw_handle(captured) };
281        Ok(Self {
282            handle: Arc::new(handle),
283        })
284    }
285
286    /// Runs `operation` with this token applied to the current thread.
287    ///
288    /// The exact prior thread-token state is restored before this method
289    /// returns, whether `operation` returns an ordinary value or a `Result`.
290    /// Stack unwinding also restores the prior state. The closure's return value
291    /// is not interpreted, so a fallible closure produces
292    /// `Result<Result<T, E>, ApplyError>`.
293    ///
294    /// This method changes only the calling thread's impersonation state. The
295    /// token may be reused sequentially or concurrently on other threads.
296    ///
297    /// # Errors
298    ///
299    /// Returns an [`ApplyError`] before calling `operation` when the exact prior
300    /// context cannot be saved or the captured token cannot be applied.
301    ///
302    /// # Panics
303    ///
304    /// Panics if restoring the exact prior thread-token state fails. Returning
305    /// a shared worker thread under an unknown identity would permit unrelated
306    /// later work to run with the wrong security context. If restoration fails
307    /// while `operation` is already unwinding, Rust's double-panic behavior
308    /// aborts the process.
309    pub fn with_impersonation<F, T>(&self, operation: F) -> Result<T, ApplyError>
310    where
311        F: FnOnce() -> T,
312    {
313        run_in_scope(ApplicationGuard::apply(self), operation)
314    }
315}
316
317impl fmt::Debug for ImpersonationToken {
318    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
319        f.debug_struct("ImpersonationToken").finish_non_exhaustive()
320    }
321}
322
323fn run_in_scope<G, F, T>(guard: Result<G, ApplyError>, operation: F) -> Result<T, ApplyError>
324where
325    F: FnOnce() -> T,
326{
327    let guard = guard?;
328    let result = operation();
329    drop(guard);
330    Ok(result)
331}
332
333struct ApplicationGuard {
334    previous: Option<OwnedHandle>,
335    _thread_bound: PhantomData<Rc<()>>,
336}
337
338impl ApplicationGuard {
339    fn apply(token: &ImpersonationToken) -> Result<Self, ApplyError> {
340        let previous = open_previous_token()?;
341
342        // SAFETY: a null thread pointer selects the current thread. The
343        // captured handle remains live and has TOKEN_IMPERSONATE access.
344        let applied = unsafe { SetThreadToken(ptr::null(), token.handle.as_raw_handle()) };
345        check_application_result(applied, io::Error::last_os_error)?;
346
347        Ok(Self {
348            previous,
349            _thread_bound: PhantomData,
350        })
351    }
352}
353
354impl Drop for ApplicationGuard {
355    fn drop(&mut self) {
356        let previous = self
357            .previous
358            .as_ref()
359            .map_or(ptr::null_mut(), AsRawHandle::as_raw_handle);
360
361        // SAFETY: a null thread pointer selects the current thread. previous is
362        // either null (restore no-token process context) or a live token handle
363        // opened with TOKEN_IMPERSONATE. The Rc marker prevents this guard from
364        // moving to or being shared with another thread.
365        let restored = unsafe { SetThreadToken(ptr::null(), previous) };
366        if restored == FALSE {
367            restore::panic_failure(io::Error::last_os_error());
368        }
369    }
370}
371
372fn check_application_result<F>(applied: i32, last_error: F) -> Result<(), ApplyError>
373where
374    F: FnOnce() -> io::Error,
375{
376    if applied == FALSE {
377        Err(ApplyError::new(ApplyFailure::ApplyToken, last_error()))
378    } else {
379        Ok(())
380    }
381}
382
383fn open_previous_token() -> Result<Option<OwnedHandle>, ApplyError> {
384    let mut raw = ptr::null_mut();
385
386    // SAFETY: GetCurrentThread is a valid pseudo-handle for this call and raw
387    // points to writable handle storage. OpenAsSelf permits saving an
388    // identification-level token using the process context for the access check.
389    let opened =
390        unsafe { OpenThreadToken(GetCurrentThread(), TOKEN_IMPERSONATE, TRUE, &raw mut raw) };
391    if opened != FALSE {
392        // SAFETY: successful OpenThreadToken returns a new, owned handle closed
393        // by OwnedHandle.
394        return Ok(Some(unsafe { OwnedHandle::from_raw_handle(raw) }));
395    }
396
397    let error = io::Error::last_os_error();
398    if error.raw_os_error()
399        == Some(i32::try_from(ERROR_NO_TOKEN).expect("ERROR_NO_TOKEN fits in i32"))
400    {
401        Ok(None)
402    } else {
403        Err(ApplyError::new(ApplyFailure::SavePreviousToken, error))
404    }
405}
406
407struct SourceToken {
408    handle: OwnedHandle,
409    level: SECURITY_IMPERSONATION_LEVEL,
410}
411
412impl SourceToken {
413    fn open() -> Result<Self, CaptureError> {
414        let mut raw = ptr::null_mut();
415
416        // SAFETY: GetCurrentThread is a valid pseudo-handle for this call and
417        // raw points to writable handle storage. OpenAsSelf uses the process
418        // context for the access check, which is required for identification
419        // level impersonation.
420        let opened = unsafe {
421            OpenThreadToken(
422                GetCurrentThread(),
423                THREAD_TOKEN_CAPTURE_ACCESS,
424                TRUE,
425                &raw mut raw,
426            )
427        };
428        if opened != FALSE {
429            // SAFETY: successful OpenThreadToken returns a new, owned handle
430            // closed by OwnedHandle.
431            let handle = unsafe { OwnedHandle::from_raw_handle(raw) };
432            let level = query_impersonation_level(&handle)?;
433            return Ok(Self { handle, level });
434        }
435
436        let error = io::Error::last_os_error();
437        match classify_thread_token_open_error(error) {
438            ThreadTokenOpenError::NoToken => Self::open_process(),
439            ThreadTokenOpenError::Capture(error) => Err(error),
440        }
441    }
442
443    fn open_process() -> Result<Self, CaptureError> {
444        let mut raw = ptr::null_mut();
445
446        // SAFETY: GetCurrentProcess is a valid pseudo-handle for this call and
447        // raw points to writable handle storage.
448        let opened = unsafe {
449            OpenProcessToken(
450                GetCurrentProcess(),
451                PROCESS_TOKEN_CAPTURE_ACCESS,
452                &raw mut raw,
453            )
454        };
455        if opened == FALSE {
456            return Err(CaptureError::new(
457                CaptureFailure::OpenProcessToken,
458                io::Error::last_os_error(),
459            ));
460        }
461
462        // SAFETY: successful OpenProcessToken returns a new, owned handle
463        // closed by OwnedHandle.
464        let handle = unsafe { OwnedHandle::from_raw_handle(raw) };
465        Ok(Self {
466            handle,
467            level: SecurityImpersonation,
468        })
469    }
470}
471
472fn query_impersonation_level(
473    handle: &OwnedHandle,
474) -> Result<SECURITY_IMPERSONATION_LEVEL, CaptureError> {
475    let mut level = SecurityAnonymous;
476    let mut returned = 0;
477    let level_size =
478        u32::try_from(size_of::<SECURITY_IMPERSONATION_LEVEL>()).expect("token level fits in u32");
479
480    // SAFETY: handle has TOKEN_QUERY and remains live for the call. level is a
481    // correctly sized writable SECURITY_IMPERSONATION_LEVEL buffer and returned
482    // points to writable length storage.
483    let queried = unsafe {
484        GetTokenInformation(
485            handle.as_raw_handle(),
486            TokenImpersonationLevel,
487            (&raw mut level).cast(),
488            level_size,
489            &raw mut returned,
490        )
491    };
492    if queried == FALSE {
493        return Err(CaptureError::new(
494            CaptureFailure::QueryImpersonationLevel,
495            io::Error::last_os_error(),
496        ));
497    }
498    if level == SecurityAnonymous {
499        return Err(CaptureError::anonymous());
500    }
501
502    Ok(level)
503}
504
505enum ThreadTokenOpenError {
506    NoToken,
507    Capture(CaptureError),
508}
509
510fn classify_thread_token_open_error(error: io::Error) -> ThreadTokenOpenError {
511    match error.raw_os_error() {
512        Some(code)
513            if code
514                == i32::try_from(ERROR_CANT_OPEN_ANONYMOUS)
515                    .expect("ERROR_CANT_OPEN_ANONYMOUS fits in i32") =>
516        {
517            ThreadTokenOpenError::Capture(CaptureError::new(
518                CaptureFailure::AnonymousContext,
519                error,
520            ))
521        }
522        Some(code)
523            if code == i32::try_from(ERROR_NO_TOKEN).expect("ERROR_NO_TOKEN fits in i32") =>
524        {
525            ThreadTokenOpenError::NoToken
526        }
527        _ => {
528            ThreadTokenOpenError::Capture(CaptureError::new(CaptureFailure::OpenThreadToken, error))
529        }
530    }
531}
532
533#[cfg(test)]
534mod tests;