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;