Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

token-privilege is a safe Rust crate that wraps the Windows Win32 APIs for querying process token privileges and elevation status. It provides a fully safe public API so that downstream consumers can maintain #![forbid(unsafe_code)] in their own crates while still accessing low-level Windows security information.

Why This Crate Exists

Querying Windows process tokens requires multiple Win32 FFI calls involving unsafe handle management, raw pointer casting, and variable-length buffer allocation. Getting any of these steps wrong can lead to undefined behavior, resource leaks, or incorrect security decisions.

token-privilege encapsulates all of that complexity behind four simple functions and a set of well-known privilege name constants.

Key Features

  • Safe public API – all unsafe code is confined to a single internal module (ffi.rs).
  • RAII handle management – Win32 HANDLE values are wrapped in a drop guard that calls CloseHandle automatically.
  • Cross-platform friendly – on non-Windows platforms, every public function returns Err(TokenPrivilegeError::UnsupportedPlatform), allowing unconditional dependency without #[cfg] at the call site.
  • Read-only – the crate never modifies token privileges; it only queries them.
  • Strict lintingclippy::unwrap_used and clippy::panic are denied; every unsafe block requires a // SAFETY: comment.

Quick Example

use token_privilege::{is_elevated, is_privilege_enabled, privileges};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    if is_elevated()? {
        println!("Running as Administrator");
    }

    if is_privilege_enabled(privileges::SE_DEBUG)? {
        println!("SeDebugPrivilege is enabled");
    }

    Ok(())
}

License

Dual-licensed under MIT or Apache-2.0 at your option.

Repository

Source code and issue tracker: https://github.com/EvilBit-Labs/token-privilege

Getting Started

Requirements

  • Rust 1.91 or later (the crate uses the 2024 edition)
  • Windows for actual privilege queries (Linux and macOS are supported but all functions return Err(TokenPrivilegeError::UnsupportedPlatform))

Installation

Add token-privilege to your project:

cargo add token-privilege

Or add it manually to your Cargo.toml:

[dependencies]
token-privilege = "0.1"

Basic Usage

Check Elevation Status

Determine whether the current process is running with Administrator privileges (elevated via UAC):

use token_privilege::is_elevated;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    match is_elevated()? {
        true  => println!("Process is elevated (Administrator)"),
        false => println!("Process is NOT elevated"),
    }
    Ok(())
}

Check a Specific Privilege

Use is_privilege_enabled with a constant from the privileges module to check whether a specific privilege is currently enabled on the process token:

use token_privilege::{is_privilege_enabled, privileges};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    if is_privilege_enabled(privileges::SE_DEBUG)? {
        println!("SeDebugPrivilege is enabled -- process can debug other processes");
    } else {
        println!("SeDebugPrivilege is NOT enabled");
    }
    Ok(())
}

Check Privilege Presence

has_privilege checks whether a privilege exists on the token regardless of whether it is currently enabled:

use token_privilege::{has_privilege, privileges};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    if has_privilege(privileges::SE_BACKUP)? {
        println!("SeBackupPrivilege is present on the token (may or may not be enabled)");
    }
    Ok(())
}

Enumerate All Privileges

List every privilege on the current process token along with its status:

use token_privilege::enumerate_privileges;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let privileges = enumerate_privileges()?;
    for info in &privileges {
        println!(
            "{}: enabled={}, enabled_by_default={}, removed={}",
            info.name, info.enabled, info.enabled_by_default, info.removed
        );
    }
    Ok(())
}

Error Handling

All functions return Result<T, TokenPrivilegeError>. The error type is #[non_exhaustive], so always include a wildcard arm when matching:

#![allow(unused)]
fn main() {
use token_privilege::{is_elevated, TokenPrivilegeError};

fn check() {
    match is_elevated() {
        Ok(true)  => println!("Elevated"),
        Ok(false) => println!("Not elevated"),
        Err(TokenPrivilegeError::UnsupportedPlatform) => {
            println!("Not running on Windows");
        }
        Err(e) => eprintln!("Unexpected error: {e}"),
    }
}
}

API Reference

This page documents every public item exported by token-privilege. For the auto-generated rustdoc, see https://docs.rs/token-privilege.

Functions

is_elevated

pub fn is_elevated() -> Result<bool, TokenPrivilegeError>

Check if the current process is running with elevated (Administrator) privileges.

Returns true if the process token has TokenElevationTypeFull (elevated via UAC) or TokenElevationTypeDefault (UAC disabled and user is an admin).

Errors:

  • TokenPrivilegeError::UnsupportedPlatform on non-Windows.
  • TokenPrivilegeError::OpenTokenFailed if the process token cannot be opened.
  • TokenPrivilegeError::QueryFailed if the elevation query fails.

Example:

#![allow(unused)]
fn main() {
use token_privilege::is_elevated;

let elevated = is_elevated()?;
println!("Elevated: {elevated}");
Ok::<(), Box<dyn std::error::Error>>(())
}

is_privilege_enabled

pub fn is_privilege_enabled(privilege_name: &str) -> Result<bool, TokenPrivilegeError>

Check if a specific named privilege is present and enabled on the current process token.

Arguments:

  • privilege_name – the Windows privilege name (e.g., "SeDebugPrivilege"). Use constants from the privileges module.

Errors:

  • TokenPrivilegeError::UnsupportedPlatform on non-Windows.
  • TokenPrivilegeError::InvalidPrivilegeName if the name is not recognized.
  • TokenPrivilegeError::LookupFailed if the OS-level lookup fails.
  • TokenPrivilegeError::CheckFailed if PrivilegeCheck fails.

Example:

#![allow(unused)]
fn main() {
use token_privilege::{is_privilege_enabled, privileges};

if is_privilege_enabled(privileges::SE_CHANGE_NOTIFY)? {
    println!("SeChangeNotifyPrivilege is enabled");
}
Ok::<(), Box<dyn std::error::Error>>(())
}

has_privilege

pub fn has_privilege(privilege_name: &str) -> Result<bool, TokenPrivilegeError>

Check if a specific named privilege is present on the current process token, regardless of whether it is currently enabled.

This function enumerates all token privileges and checks whether the named privilege appears in the list.

Arguments:

  • privilege_name – the Windows privilege name.

Errors:

  • TokenPrivilegeError::UnsupportedPlatform on non-Windows.
  • TokenPrivilegeError::InvalidPrivilegeName if the name is not recognized.
  • TokenPrivilegeError::QueryFailed if token enumeration fails.

Example:

#![allow(unused)]
fn main() {
use token_privilege::{has_privilege, privileges};

if has_privilege(privileges::SE_BACKUP)? {
    println!("SeBackupPrivilege is on the token");
}
Ok::<(), Box<dyn std::error::Error>>(())
}

enumerate_privileges

pub fn enumerate_privileges() -> Result<Vec<PrivilegeInfo>, TokenPrivilegeError>

Enumerate all privileges on the current process token.

Returns a Vec<PrivilegeInfo> describing each privilege, its name, and its current status flags.

Errors:

  • TokenPrivilegeError::UnsupportedPlatform on non-Windows.
  • TokenPrivilegeError::OpenTokenFailed if the process token cannot be opened.
  • TokenPrivilegeError::QueryFailed if privilege enumeration fails.

Example:

#![allow(unused)]
fn main() {
use token_privilege::enumerate_privileges;

for info in enumerate_privileges()? {
    println!("{}: enabled={}", info.name, info.enabled);
}
Ok::<(), Box<dyn std::error::Error>>(())
}

Types

PrivilegeInfo

#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct PrivilegeInfo {
    pub name: String,
    pub enabled: bool,
    pub enabled_by_default: bool,
    pub removed: bool,
}

Represents the status of a single Windows privilege on the process token.

FieldTypeDescription
nameStringThe privilege name (e.g., "SeDebugPrivilege").
enabledboolWhether the privilege is currently enabled.
enabled_by_defaultboolWhether the privilege is enabled by default.
removedboolWhether the privilege has been removed from the token.

TokenPrivilegeError

#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum TokenPrivilegeError {
    OpenTokenFailed(std::io::Error),
    QueryFailed(std::io::Error),
    InvalidPrivilegeName { name: String },
    LookupFailed { name: String, source: std::io::Error },
    CheckFailed(std::io::Error),
    UnsupportedPlatform,
}

All functions in this crate return this error type. The enum is #[non_exhaustive] – always include a wildcard arm when matching.

VariantWhen It Occurs
OpenTokenFailedOpenProcessToken fails.
QueryFailedGetTokenInformation fails.
InvalidPrivilegeNameThe privilege name is not recognized by Windows.
LookupFailedLookupPrivilegeValueW fails for OS-level reasons.
CheckFailedPrivilegeCheck fails.
UnsupportedPlatformCalled on a non-Windows platform.

privileges Module

The privileges module provides well-known Windows privilege name constants. Use these instead of hard-coding string literals.

ConstantValueDescription
SE_DEBUG"SeDebugPrivilege"Debug programs.
SE_BACKUP"SeBackupPrivilege"Back up files and directories.
SE_RESTORE"SeRestorePrivilege"Restore files and directories.
SE_SHUTDOWN"SeShutdownPrivilege"Shut down the system.
SE_SECURITY"SeSecurityPrivilege"Manage auditing and security log.
SE_TAKE_OWNERSHIP"SeTakeOwnershipPrivilege"Take ownership of files or other objects.
SE_LOAD_DRIVER"SeLoadDriverPrivilege"Load and unload device drivers.
SE_SYSTEM_PROFILE"SeSystemProfilePrivilege"Profile system performance.
SE_SYSTEMTIME"SeSystemtimePrivilege"Change the system time.
SE_CHANGE_NOTIFY"SeChangeNotifyPrivilege"Bypass traverse checking.
SE_IMPERSONATE"SeImpersonatePrivilege"Impersonate a client after authentication.
SE_CREATE_GLOBAL"SeCreateGlobalPrivilege"Create global objects.
SE_INCREASE_QUOTA"SeIncreaseQuotaPrivilege"Adjust memory quotas for a process.
SE_UNDOCK"SeUndockPrivilege"Remove computer from docking station.
SE_MANAGE_VOLUME"SeManageVolumePrivilege"Perform volume maintenance tasks.
SE_ASSIGN_PRIMARY_TOKEN"SeAssignPrimaryTokenPrivilege"Replace a process-level token.
SE_INCREASE_BASE_PRIORITY"SeIncreaseBasePriorityPrivilege"Increase scheduling priority.
SE_CREATE_PAGEFILE"SeCreatePagefilePrivilege"Create a pagefile.
SE_TCB"SeTcbPrivilege"Act as part of the operating system.
SE_REMOTE_SHUTDOWN"SeRemoteShutdownPrivilege"Force shutdown from a remote system.

Example:

#![allow(unused)]
fn main() {
use token_privilege::{is_privilege_enabled, privileges};

let debug_enabled = is_privilege_enabled(privileges::SE_DEBUG)?;
let backup_enabled = is_privilege_enabled(privileges::SE_BACKUP)?;
Ok::<(), Box<dyn std::error::Error>>(())
}

Integration Examples

This page shows common patterns for integrating token-privilege into your Rust projects.

Using from a #![forbid(unsafe_code)] Crate

The primary design goal of token-privilege is to let downstream consumers remain fully safe. Your crate can forbid unsafe code while still querying Windows process tokens:

// your_crate/src/lib.rs
#![forbid(unsafe_code)]

use token_privilege::{is_elevated, TokenPrivilegeError};

pub fn require_admin() -> Result<(), String> {
    match is_elevated() {
        Ok(true) => Ok(()),
        Ok(false) => Err("This operation requires Administrator privileges.".into()),
        Err(TokenPrivilegeError::UnsupportedPlatform) => {
            // Not on Windows -- skip the check or use a platform-specific alternative
            Ok(())
        }
        Err(e) => Err(format!("Failed to check elevation: {e}")),
    }
}

No unsafe block is needed in your code. All FFI interactions happen inside token-privilege’s internal ffi.rs module.

Cross-Platform Applications

token-privilege compiles on all platforms. On non-Windows targets, every public function returns Err(TokenPrivilegeError::UnsupportedPlatform). This means you can depend on the crate unconditionally and handle the error at runtime:

#![allow(unused)]
fn main() {
use token_privilege::{is_elevated, TokenPrivilegeError};

fn check_elevation() -> bool {
    match is_elevated() {
        Ok(elevated) => elevated,
        Err(TokenPrivilegeError::UnsupportedPlatform) => {
            // On Linux/macOS, fall back to a different check.
            // For example, check if running as root via std::process::Command:
            //   std::process::Command::new("id").arg("-u").output()
            // Or use a platform-specific crate like `nix` (which provides safe wrappers).
            false
        }
        Err(e) => {
            eprintln!("Elevation check failed: {e}");
            false
        }
    }
}
}

Alternatively, use compile-time #[cfg] gating if you want to avoid the runtime error path entirely:

#![allow(unused)]
fn main() {
fn is_admin() -> bool {
    #[cfg(target_os = "windows")]
    {
        token_privilege::is_elevated().unwrap_or(false)
    }
    #[cfg(not(target_os = "windows"))]
    {
        false
    }
}
}

Privilege Guard Pattern

Create a guard that checks for required privileges before performing a sensitive operation:

use token_privilege::{is_privilege_enabled, has_privilege, privileges, TokenPrivilegeError};

#[derive(Debug)]
pub enum PrivilegeStatus {
    Enabled,
    PresentButDisabled,
    Missing,
}

pub fn check_privilege_status(name: &str) -> Result<PrivilegeStatus, TokenPrivilegeError> {
    if is_privilege_enabled(name)? {
        Ok(PrivilegeStatus::Enabled)
    } else if has_privilege(name)? {
        Ok(PrivilegeStatus::PresentButDisabled)
    } else {
        Ok(PrivilegeStatus::Missing)
    }
}

pub fn require_privilege(name: &str) -> Result<(), String> {
    match check_privilege_status(name) {
        Ok(PrivilegeStatus::Enabled) => Ok(()),
        Ok(PrivilegeStatus::PresentButDisabled) => {
            Err(format!("{name} is present but not enabled. Run as Administrator."))
        }
        Ok(PrivilegeStatus::Missing) => {
            Err(format!("{name} is not available on this token."))
        }
        Err(e) => Err(format!("Privilege check failed: {e}")),
    }
}

Diagnostic Reporting

Enumerate all privileges and produce a diagnostic report, useful for troubleshooting or security audits:

use token_privilege::{enumerate_privileges, is_elevated};

fn print_token_report() -> Result<(), Box<dyn std::error::Error>> {
    println!("=== Process Token Report ===");
    println!("Elevated: {}", is_elevated()?);
    println!();

    let privileges = enumerate_privileges()?;
    println!("{:<40} {:>8} {:>10} {:>8}", "Privilege", "Enabled", "Default", "Removed");
    println!("{}", "-".repeat(70));

    for info in &privileges {
        println!(
            "{:<40} {:>8} {:>10} {:>8}",
            info.name, info.enabled, info.enabled_by_default, info.removed
        );
    }

    println!();
    println!("Total privileges: {}", privileges.len());
    let enabled_count = privileges.iter().filter(|p| p.enabled).count();
    println!("Enabled: {enabled_count}");

    Ok(())
}
fn main() { let _ = print_token_report(); }

Conditional Feature Gating

Use privilege checks to enable or disable features in your application:

use token_privilege::{is_elevated, is_privilege_enabled, privileges};

pub struct AppCapabilities {
    pub can_debug_processes: bool,
    pub can_backup_files: bool,
    pub is_admin: bool,
}

pub fn detect_capabilities() -> AppCapabilities {
    AppCapabilities {
        can_debug_processes: is_privilege_enabled(privileges::SE_DEBUG).unwrap_or(false),
        can_backup_files: is_privilege_enabled(privileges::SE_BACKUP).unwrap_or(false),
        is_admin: is_elevated().unwrap_or(false),
    }
}

Architecture Overview

token-privilege uses a layered architecture that isolates all unsafe FFI code behind a safe public API. This page describes how the layers fit together.

Layer Diagram

flowchart TD
    A[Consumer Crate - forbid unsafe_code] --> B[token-privilege Public API]
    B --> C[elevation.rs]
    B --> D[privilege.rs]
    C --> E[ffi.rs - all unsafe lives here]
    D --> E
    E --> F[windows crate - Win32 FFI bindings]
    F --> G[Windows Kernel - Process Token]

Module Responsibilities

lib.rs – Public API

The crate root declares modules, re-exports public types, and provides the top-level function signatures. On Windows, each function delegates to the corresponding domain module. On non-Windows, each function is a const fn stub returning Err(TokenPrivilegeError::UnsupportedPlatform).

Public exports:

  • is_elevated()
  • is_privilege_enabled(privilege_name)
  • has_privilege(privilege_name)
  • enumerate_privileges()
  • PrivilegeInfo struct
  • TokenPrivilegeError enum
  • privileges module (well-known constants)

elevation.rs – Elevation Detection

Contains the Windows-specific implementation of is_elevated(). Calls into ffi::open_current_process_token() and ffi::query_elevation() to determine whether the process has Administrator elevation via UAC.

privilege.rs – Privilege Queries

Contains the Windows-specific implementations of:

  • is_privilege_enabled() – opens the token, looks up the privilege LUID, and checks if it is enabled via PrivilegeCheck.
  • has_privilege() – enumerates all token privileges and checks whether the named privilege appears in the list.
  • enumerate_privileges() – opens the token and enumerates all privileges with their status flags.

ffi.rs – FFI Boundary

The only module containing unsafe code. All Win32 API calls are wrapped in safe pub(crate) functions:

FunctionWin32 API CalledPurpose
open_current_process_token()OpenProcessTokenOpen the current process token.
query_elevation()GetTokenInformationQuery token elevation status.
lookup_privilege_value()LookupPrivilegeValueWResolve a privilege name to LUID.
check_privilege_enabled()PrivilegeCheckCheck if a privilege is enabled.
enumerate_token_privileges()GetTokenInformationList all token privileges.
lookup_privilege_name()LookupPrivilegeNameWResolve a LUID to a name.

error.rs – Error Types

Defines TokenPrivilegeError, a #[non_exhaustive] enum built with thiserror. Each variant maps to a specific failure mode in the Win32 call chain.

Data Flow

A typical call to is_privilege_enabled("SeDebugPrivilege") follows this path:

  1. lib.rs dispatches to privilege::is_privilege_enabled().
  2. privilege.rs calls ffi::open_current_process_token() to get an OwnedHandle.
  3. privilege.rs calls ffi::lookup_privilege_value("SeDebugPrivilege") to resolve the name to a LUID.
  4. privilege.rs calls ffi::check_privilege_enabled(&token, luid) to perform the actual privilege check.
  5. The OwnedHandle is dropped, calling CloseHandle automatically.
  6. The bool result propagates back to the consumer.

Design Constraints

  • unsafe is NOT forbidden at crate level. This crate is the unsafe boundary – it exists to contain the unsafe code so consumers do not need it.
  • All unsafe blocks require // SAFETY: comments. The lint undocumented_unsafe_blocks = "deny" enforces this.
  • No panics, no unwraps. clippy::panic and clippy::unwrap_used are denied. All fallible operations return Result.
  • Read-only. The crate never calls AdjustTokenPrivileges or any other API that modifies the process token.
  • RAII for handles. The OwnedHandle wrapper ensures CloseHandle is called on all code paths, including panics and early returns.

Platform Compilation

The crate uses #[cfg(target_os = "windows")] to conditionally compile modules:

  • Windows: elevation.rs, privilege.rs, and ffi.rs are compiled. The windows crate dependency is pulled in.
  • Non-Windows: Only lib.rs, error.rs, and the stub functions are compiled. The windows crate is not linked.

This is controlled via [target.'cfg(windows)'.dependencies] in Cargo.toml.

Safety Contract

All unsafe code in token-privilege is confined to a single module: src/ffi.rs. This page documents every unsafe block, its safety invariants, and the mechanisms that enforce correctness.

Enforcement

The Clippy lint undocumented_unsafe_blocks = "deny" is set in Cargo.toml. Any unsafe block without a // SAFETY: comment is a compile-time error. This ensures every unsafe operation has an explicit justification in the source code.

Unsafe Blocks

OwnedHandle::dropCloseHandle

// SAFETY: `CloseHandle` is safe to call on a valid, open handle that
// we own. The RAII pattern ensures this is called exactly once, when
// the `OwnedHandle` is dropped. The `is_invalid()` guard skips the
// call for default-initialized or explicitly invalidated handles.
unsafe {
    let close_result = CloseHandle(self.0);
    debug_assert!(close_result.is_ok(), "CloseHandle failed: {close_result:?}");
}

Invariant: The HANDLE stored in OwnedHandle is either valid (opened by OpenProcessToken) or INVALID_HANDLE_VALUE. The is_invalid() check before the call prevents closing an invalid handle. The handle is never cloned or aliased, so double-close is impossible under normal usage. A debug_assert! verifies the close succeeds in debug builds.


open_current_process_tokenOpenProcessToken

// SAFETY: `GetCurrentProcess()` returns a pseudo-handle that is always valid
// and does not need to be closed. `OpenProcessToken` writes to `handle` only
// on success; on failure we return the IO error.
unsafe {
    OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut handle)
        .map_err(|e| TokenPrivilegeError::OpenTokenFailed(io::Error::from(e)))?;
}

Invariant: GetCurrentProcess() always returns a valid pseudo-handle (-1). The handle output parameter is a local variable with exclusive access. On success, OpenProcessToken writes a valid token handle. On failure, the error is propagated and no handle is stored.


query_elevationGetTokenInformation

// SAFETY: We pass a valid token handle and a correctly-sized buffer.
// `GetTokenInformation` writes at most `elevation_size` bytes into
// `elevation` and sets `return_length` to the actual bytes written.
unsafe {
    GetTokenInformation(
        token.0,
        TokenElevation,
        Some(std::ptr::from_mut(&mut elevation).cast()),
        elevation_size,
        &mut return_length,
    )
    .map_err(|e| TokenPrivilegeError::QueryFailed(io::Error::from(e)))?;
}

Invariant: The token handle is valid (obtained from open_current_process_token and wrapped in OwnedHandle). The buffer is a stack-allocated TOKEN_ELEVATION of known, fixed size. The elevation_size parameter matches size_of::<TOKEN_ELEVATION>(). The cast from *mut TOKEN_ELEVATION to *mut c_void is layout-compatible.


lookup_privilege_valueLookupPrivilegeValueW

// SAFETY: We pass a null-terminated wide string and a valid LUID pointer.
// `LookupPrivilegeValueW` writes the LUID on success.
unsafe {
    LookupPrivilegeValueW(None, PCWSTR(wide_name.as_ptr()), &mut luid)
        .map_err(|e| { /* error mapping */ })?;
}

Invariant: wide_name is constructed by encoding the input &str as UTF-16 and appending a null terminator. The PCWSTR wrapper points to a live Vec<u16> that outlives the call. The luid output is a local variable with exclusive access.


check_privilege_enabledPrivilegeCheck

// SAFETY: We pass a valid token handle and a correctly initialized
// PRIVILEGE_SET with count=1. `PrivilegeCheck` writes the result.
unsafe {
    PrivilegeCheck(token.0, &mut privilege_set, &mut result)
        .map_err(|e| TokenPrivilegeError::CheckFailed(io::Error::from(e)))?;
}

Invariant: The token handle is valid. The PRIVILEGE_SET is stack-allocated with PrivilegeCount = 1 and a single correctly-initialized LUID_AND_ATTRIBUTES entry. The result output is a local i32.


enumerate_token_privilegesGetTokenInformation (size query)

// SAFETY: First call with null buffer to query the required size.
// Expected to fail with ERROR_INSUFFICIENT_BUFFER, which we handle.
let size_result = unsafe {
    GetTokenInformation(token.0, TokenPrivileges, None, 0, &mut return_length)
};

Invariant: Passing None and size 0 is the documented pattern for querying the required buffer size. No data is written. The function is expected to fail with ERROR_INSUFFICIENT_BUFFER.


enumerate_token_privilegesGetTokenInformation (data query)

// SAFETY: We pass a buffer of exactly `return_length` bytes as reported
// by the previous call. `GetTokenInformation` will write TOKEN_PRIVILEGES
// data into this buffer.
unsafe {
    GetTokenInformation(
        token.0, TokenPrivileges,
        Some(buffer.as_mut_ptr().cast()),
        return_length, &mut return_length,
    )
    .map_err(|e| TokenPrivilegeError::QueryFailed(io::Error::from(e)))?;
}

Invariant: The buffer is a Vec<u64> heap-allocated with at least return_length bytes (using div_ceil to round up to whole u64 elements). The Vec<u64> guarantees 8-byte alignment, which satisfies TOKEN_PRIVILEGES alignment requirements on all Windows platforms. The token handle has not been invalidated between calls.


enumerate_token_privilegesTOKEN_PRIVILEGES pointer cast

// SAFETY: The buffer was successfully filled with a TOKEN_PRIVILEGES struct.
// We read PrivilegeCount and then iterate over that many LUID_AND_ATTRIBUTES.
let token_privileges = unsafe { &*(buffer.as_ptr().cast::<TOKEN_PRIVILEGES>()) };

Invariant: GetTokenInformation succeeded, guaranteeing the buffer contains a valid TOKEN_PRIVILEGES structure. The buffer is a Vec<u64>, providing 8-byte alignment which exceeds the alignment requirement of TOKEN_PRIVILEGES (align 4 on 32-bit, align 8 on 64-bit Windows).


enumerate_token_privilegesslice::from_raw_parts

// SAFETY: The privileges array in TOKEN_PRIVILEGES is a variable-length
// array. We access `count` elements, which is what Windows wrote.
let privileges_slice = unsafe {
    std::slice::from_raw_parts(token_privileges.Privileges.as_ptr(), count)
};

Invariant: count is read from PrivilegeCount, which was written by Windows. The pointer comes from the Privileges field of the successfully populated TOKEN_PRIVILEGES. The buffer is large enough because its size was determined by the Windows API itself.


lookup_privilege_nameLookupPrivilegeNameW (size query and data query)

// SAFETY: First call with null buffer to get the required name length.
let _ = unsafe {
    LookupPrivilegeNameW(None, &luid, PWSTR::null(), &mut name_len)
};
// SAFETY: We pass a buffer of the size reported by the first call.
// `LookupPrivilegeNameW` writes the privilege name as a wide string.
unsafe {
    LookupPrivilegeNameW(None, &luid, PWSTR(name_buf.as_mut_ptr()), &mut name_len)
        .map_err(|e| TokenPrivilegeError::QueryFailed(io::Error::from(e)))?;
}

Invariant: The first call uses a null buffer to query the required size. The second call allocates a Vec<u16> of exactly that size. The LUID is valid because it was obtained from the same token enumeration. name_len is updated to reflect the actual characters written (excluding the null terminator).

RAII Handle Wrapper

OwnedHandle is a pub(crate) newtype around HANDLE that implements Drop to call CloseHandle. This ensures handles are closed on all code paths:

  • Normal return
  • Early ? propagation
  • Panic unwinding

The handle is never Copy or Clone, preventing aliased access. It is never exposed outside ffi.rs.

Security Assurance

token-privilege is a security-sensitive crate. This page documents the threat model, security properties, and the measures in place to maintain them.

Threat Model

In Scope

  • Incorrect privilege reporting. A bug that reports a privilege as enabled when it is not (or vice versa) could lead to incorrect authorization decisions by consuming applications.
  • Resource leaks. Failing to close a token handle could leak kernel resources.
  • Memory safety violations. Incorrect FFI usage could lead to buffer overflows, use-after-free, or other undefined behavior.
  • Denial of service. Pathological inputs (e.g., extremely long privilege names) should not cause panics or unbounded allocations.

Out of Scope

  • Privilege escalation. This crate is read-only; it never calls AdjustTokenPrivileges or any API that modifies the process token. It cannot elevate, enable, disable, or remove privileges.
  • Malicious OS behavior. If the Windows kernel itself returns incorrect data, that is outside the crate’s control.
  • Side-channel attacks. Timing differences between privilege checks are not considered a threat for this use case.

Security Properties

Read-Only Access

The crate opens process tokens with TOKEN_QUERY access only. No write operations are performed. The token state is never modified.

Unsafe Code Isolation

All unsafe blocks are confined to src/ffi.rs. The public API is entirely safe. Consumers can use #![forbid(unsafe_code)] in their own crates.

See the Safety Contract for a detailed audit of every unsafe block.

No Panics, No Unwraps

The following Clippy lints are set to deny in Cargo.toml:

  • clippy::panic – no panic!(), todo!(), or unimplemented!() in non-test code.
  • clippy::unwrap_used – no .unwrap() calls; all fallible operations use Result propagation.

This ensures the crate returns structured errors instead of aborting the process.

Documented Unsafe Blocks

The lint clippy::undocumented_unsafe_blocks = "deny" requires every unsafe block to have a // SAFETY: comment explaining why the operation is sound.

Non-Exhaustive Error Type

TokenPrivilegeError is #[non_exhaustive], allowing new error variants to be added in future releases without breaking downstream match arms. This prevents consumers from assuming they handle all possible errors.

Static Analysis

Clippy Configuration

The crate enables aggressive Clippy lint groups:

GroupLevelPurpose
correctnessdenyCatch definite bugs.
pedanticwarnCatch subtle issues.
nurserywarnCatch emerging patterns.
suspiciouswarnCatch code that looks like a bug.
cargowarnCatch packaging issues.

Additional security-focused lints include as_conversions, cast_ptr_alignment, indexing_slicing, and arithmetic_side_effects.

Formatting

rustfmt is configured for the 2024 edition and style. Formatting is checked in CI via just fmt-check.

Dependency Auditing

cargo-audit

cargo audit checks for known vulnerabilities in the dependency tree. Run locally with:

just audit

This is also enforced in CI via the audit.yml workflow.

cargo-deny

cargo deny check validates licenses, bans problematic crates, and checks for security advisories. Run locally with:

just deny

Minimal Dependencies

The crate has a single runtime dependency: thiserror for error derivation. The windows crate is only pulled in on Windows targets via [target.'cfg(windows)'.dependencies].

Dev dependencies (proptest, tempfile) are not included in production builds.

Test Coverage

The project targets 85% line coverage, enforced by:

just coverage-check

Coverage is generated with cargo-llvm-cov and reported to Codecov in CI. See the Testing page for details on the testing strategy.

CI Pipeline

Every push to main and every pull request runs through:

  1. Quality gaterustfmt check and Clippy with -D warnings.
  2. Test suitecargo nextest run on Ubuntu.
  3. Cross-platform tests – Linux, macOS, and Windows runners.
  4. Coveragecargo-llvm-cov with Codecov upload.
  5. Auditcargo audit for known vulnerabilities.
  6. Scorecard – OpenSSF Scorecard for supply chain security.
  7. Security scanning – Additional security workflow checks.

Reporting Security Issues

If you discover a security vulnerability, please report it responsibly via GitHub’s private vulnerability reporting feature on the repository: https://github.com/EvilBit-Labs/token-privilege/security

Development Setup

This page covers how to set up a local development environment for working on token-privilege.

Prerequisites

  • Rust 1.91+ – the crate uses the 2024 edition
  • Git
  • mise – manages all development tool versions (Rust toolchain, cargo extensions, formatters, linters)
  • just – command runner (installed automatically by mise)

Initial Setup

Clone the repository and install all tools:

git clone https://github.com/EvilBit-Labs/token-privilege.git
cd token-privilege
just setup

just setup runs mise install, which reads mise.toml and installs all required tool versions. This includes cargo-nextest, cargo-llvm-cov, cargo-audit, cargo-deny, mdbook, and other development tools.

Common Commands

All development tasks are orchestrated through the justfile. Run just (with no arguments) to see all available recipes.

Building

CommandDescription
just buildBuild the workspace (debug).
just build-releaseBuild the workspace (release).

Testing

CommandDescription
just testRun all tests with cargo nextest.
just test-allInclude ignored and slow tests.

Run a single test by name:

cargo nextest run -E 'test(test_name)'

Formatting and Linting

CommandDescription
just fmtFormat all Rust code.
just fmt-checkCheck formatting without modifying files.
just lintRun all linters (Rust, actions, docs, justfile).
just lint-rustFormat check + Clippy (pedantic, all features).
just fixAuto-fix Clippy warnings.

Quality Checks

CommandDescription
just checkPre-commit hooks + all linters (local quality gate).
just ci-checkFull CI parity: pre-commit, fmt, clippy, test, build-release, audit, coverage, docs.

Coverage

CommandDescription
just coverageGenerate LCOV coverage report.
just coverage-checkCoverage with --fail-under-lines 85.
just coverage-reportHTML coverage report, opens in browser.
just coverage-summaryPrint coverage summary by file.

Security

CommandDescription
just auditRun cargo audit for known vulnerabilities.
just denyRun cargo deny check for license and advisory checks.

Documentation

CommandDescription
just docs-buildBuild mdBook and rustdoc.
just docs-serveServe docs locally with live reload.
just docs-checkValidate rustdoc links and mdBook build.
just docs-cleanRemove generated documentation artifacts.

Editor Configuration

The repository includes an .editorconfig file. Ensure your editor respects it or configure the following manually:

  • Indent with 4 spaces (Rust files)
  • UTF-8 encoding
  • LF line endings
  • Trailing whitespace trimmed

Pre-Commit Hooks

Pre-commit hooks are configured via .pre-commit-config.yaml. They run automatically on git commit and can be run manually with:

just check

Windows Development

The justfile supports Windows via PowerShell. All just commands work on both Windows and Unix. Windows is the primary platform for actually running the FFI tests – Linux and macOS only exercise the non-Windows stub paths.

Testing

This page describes the testing strategy, tools, and conventions used in token-privilege.

Test Runner

Tests are run with cargo-nextest instead of the built-in cargo test. nextest provides better output, parallel execution, and per-test timeouts.

just test                # Run all tests
just test-all            # Include ignored/slow tests

Run a single test by name:

cargo nextest run -E 'test(test_name)'

Test Organization

Unit Tests Per Module

Each source module has #[cfg(test)] mod tests at the bottom with unit tests for its internal logic:

  • error.rs – display formatting for each error variant, Send + Sync bounds.
  • elevation.rs – (Windows only) is_elevated() returns Ok, result is consistent across calls.
  • privilege.rs – (Windows only) SeChangeNotifyPrivilege is enabled, invalid names return errors, enumeration is non-empty.
  • ffi.rs – (Windows only) handle open/close cycle, elevation query, privilege lookup (valid and invalid), privilege check, enumeration.

Stub Tests

lib.rs contains stub tests gated with #[cfg(not(target_os = "windows"))] that verify all public functions return Err(TokenPrivilegeError::UnsupportedPlatform) on non-Windows platforms:

#[cfg(not(target_os = "windows"))]
#[cfg(test)]
mod stub_tests {
    #[test]
    fn is_elevated_returns_unsupported() { /* ... */ }
    #[test]
    fn is_privilege_enabled_returns_unsupported() { /* ... */ }
    #[test]
    fn has_privilege_returns_unsupported() { /* ... */ }
    #[test]
    fn enumerate_privileges_returns_unsupported() { /* ... */ }
}

Platform-Gated Tests

Windows-specific tests in elevation.rs, privilege.rs, and ffi.rs are compiled only when target_os = "windows" because those modules are conditionally compiled. This means:

  • Linux/macOS CI runners execute the stub tests and error.rs tests.
  • Windows CI runners execute the full FFI test suite.

Reliable Test Privilege

SeChangeNotifyPrivilege is used as the canonical test privilege because it is enabled by default on all Windows process tokens. Tests relying on this privilege do not require Administrator elevation to pass.

Property-Based Testing

The crate includes proptest as a dev dependency for property-based testing. Proptest generates random inputs and verifies that invariants hold across many cases. This is particularly useful for testing error handling with arbitrary privilege name strings.

Coverage

Coverage is measured with cargo-llvm-cov and reported in LCOV format.

Running Coverage Locally

just coverage            # Generate lcov.info
just coverage-check      # Fail if line coverage < 85%
just coverage-report     # HTML report, opens in browser
just coverage-summary    # Print per-file summary

Coverage Target

The project enforces a minimum of 85% line coverage, checked by:

just coverage-check

This threshold is also enforced in the ci-check recipe and the CI pipeline.

Coverage in CI

The CI pipeline generates coverage on Ubuntu, uploads the LCOV report to Codecov, and reports results on pull requests.

CI Test Matrix

The CI workflow runs tests across multiple platforms:

RunnerPlatformWhat Runs
windows-latestWindowsCode quality (fmt + clippy), full FFI test suite.
ubuntu-latestLinuxStub tests, error tests.

The pipeline structure is:

  1. quality – formatting and Clippy checks (Windows).
  2. test-windows – full test suite + release build (depends on quality).
  3. test-linux-stubs – stub validation on Linux (depends on quality).
  4. coverage – generate coverage and upload to Codecov (Windows, depends on test-windows).

Writing New Tests

When adding a new test:

  1. Place it in the #[cfg(test)] mod tests block of the relevant module.
  2. Gate Windows-specific tests with #[cfg(target_os = "windows")] (handled automatically since the module is already conditionally compiled).
  3. Use SeChangeNotifyPrivilege when you need a privilege that is reliably present and enabled.
  4. Return Result from tests when possible to avoid .unwrap() (which is denied by Clippy in non-test code but allowed in test modules).
  5. Run just coverage-check to verify the 85% threshold is maintained.

Release Process

token-privilege uses automated release tooling to minimize manual steps and ensure consistency.

Overview

Releases are managed by release-plz, which automates version bumping, changelog generation, and crate publishing. The process is driven by conventional commits on the main branch.

Conventional Commits

All commits must follow the conventional commits format:

<type>(<scope>): <description>

Types

TypePurposeVersion Bump
featNew featureMinor
fixBug fixPatch
refactorCode restructuringPatch
docsDocumentation changesNone
testTest additions or changesNone
choreMaintenance tasksNone
perfPerformance improvementsPatch
ciCI configuration changesNone

Scopes

ScopeWhen to Use
libPublic API in lib.rs
apiAPI surface changes
errorError type changes
elevationElevation detection logic
privilegePrivilege query logic
privilegesPrivilege constants module
ffiFFI boundary (unsafe code)
safetySafety invariant changes
securitySecurity-related changes
docsDocumentation
bookmdBook documentation
testsTest changes
ciCI workflows
depsDependency updates
releaseRelease-related changes

Special Rules

  • Changes touching unsafe code must use the ffi or safety scope and include the safety invariant in the commit body.
  • Cross-platform behavior changes must note the impact on Windows vs. non-Windows in the commit body.

Automated Release Flow

1. Push to main

When commits land on the main branch (via merged PR or direct push), release-plz runs two jobs:

2. Release PR

The release-plz-pr job analyzes commits since the last release, determines the appropriate version bump based on conventional commit types, and opens (or updates) a release PR. The PR contains:

  • Updated version in Cargo.toml
  • Generated changelog entries from commit messages

3. Release

The release-plz-release job detects when the version in Cargo.toml has been bumped and:

  • Creates a GitHub release with the changelog
  • Publishes the crate to crates.io

CI Pipeline

Every change goes through the following CI stages before it can be merged:

  1. Qualityrustfmt check and Clippy with -D warnings.
  2. Testcargo nextest run on Ubuntu.
  3. Cross-platform – tests on Linux, macOS, and Windows.
  4. Coveragecargo-llvm-cov with Codecov upload.
  5. Auditcargo audit for known vulnerabilities.

All checks must pass before a PR can be merged.

Local Release Commands

For local testing and verification:

CommandDescription
just release-dry-runSimulate a release without publishing.
just releasePerform the release (requires credentials).
just release-patchRelease a patch version.
just release-minorRelease a minor version.
just release-majorRelease a major version.
just changelogGenerate the full changelog.
just changelog-unreleasedGenerate changelog for unreleased changes.

Changelog

The changelog is generated by git-cliff from conventional commit messages. Configuration is in cliff.toml.

To regenerate the changelog locally:

just changelog

To generate a changelog for a specific version:

just changelog-version v0.2.0