Files
MercuryToolbox/crates/windowsupport/src/sudo.rs
T

2092 lines
76 KiB
Rust

//! Windows-native helpers for privileged process launch and token inspection.
use serde::Serialize;
/// Target identity used for a privileged process launch.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum LaunchIdentity {
/// Run as the current process token without UAC elevation.
CurrentProcess,
/// Run as the active interactive user.
CurrentUser,
/// Run as an elevated administrator.
Admin,
/// Run as `NT AUTHORITY\SYSTEM`.
System,
/// Run as `NT SERVICE\TrustedInstaller`.
TrustedInstaller,
}
/// Privilege policy applied before launching a process.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PrivilegeMode {
/// Keep the duplicated token privileges unchanged.
Default,
/// Attempt to enable all privileges present on the token.
EnableAll,
/// Attempt to disable all privileges present on the duplicated token.
DisableAll,
}
/// Token integrity level used for status reports and optional launch shaping.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum TokenIntegrity {
/// Untrusted integrity.
Untrusted,
/// Low integrity.
Low,
/// Medium integrity.
Medium,
/// Medium-plus integrity.
MediumPlus,
/// High integrity.
High,
/// System integrity.
System,
/// Unknown or unmapped integrity.
Unknown,
}
/// Process priority class used during launch.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ProcessPriority {
/// Idle priority.
Idle,
/// Below normal priority.
BelowNormal,
/// Normal priority.
Normal,
/// Above normal priority.
AboveNormal,
/// High priority.
High,
/// Realtime priority.
Realtime,
}
/// Window presentation mode used for a launch request.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ShowWindowMode {
/// Let Windows decide.
Default,
/// Hide the new window.
Hidden,
/// Show a normal window.
Normal,
/// Show a minimized window.
Minimized,
/// Show a maximized window.
Maximized,
}
/// Stable status report for the current process token and privileged-launch capabilities.
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct TokenStatus {
/// Whether the current process is elevated.
pub is_elevated: bool,
/// Whether the token belongs to an administrators group member.
pub is_admin_member: bool,
/// Current integrity level.
pub integrity: TokenIntegrity,
/// Current Windows user name when available.
pub current_user: Option<String>,
/// Current process session identifier when available.
pub session_id: Option<u32>,
/// Active console session identifier when available.
pub active_session_id: Option<u32>,
/// Whether the current process can directly launch as administrator.
pub can_admin: bool,
/// Whether an active interactive user token can be used.
pub can_current_user: bool,
/// Whether the current process can directly launch as `SYSTEM`.
pub can_system: bool,
/// Whether the current process can directly launch as `TrustedInstaller`.
pub can_trustedinstaller: bool,
/// Whether the `TrustedInstaller` service exists.
pub trustedinstaller_installed: bool,
/// Whether the `TrustedInstaller` service is already running.
pub trustedinstaller_running: bool,
}
/// High-level launch request consumed by `msudo`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LaunchRequest {
/// Program path or command name.
pub program: String,
/// Command arguments excluding the program itself.
pub args: Vec<String>,
/// Current working directory for the launched process.
pub current_directory: Option<String>,
/// Optional stdout redirection path used by short-lived relays.
pub stdout_path: Option<String>,
/// Optional stderr redirection path used by short-lived relays.
pub stderr_path: Option<String>,
/// Target identity.
pub identity: LaunchIdentity,
/// Privilege policy.
pub privileges: PrivilegeMode,
/// Optional integrity shaping request.
pub integrity: Option<TokenIntegrity>,
/// Process priority class.
pub priority: ProcessPriority,
/// Window presentation mode.
pub show_window: ShowWindowMode,
/// Optional target session id.
pub session: Option<u32>,
/// Whether the caller explicitly wants a new window.
pub new_window: bool,
/// Whether the launched process should take over the caller's current console.
pub same_console: bool,
/// Whether to wait for completion and capture the exit code.
pub wait: bool,
}
/// Result returned after a privileged launch attempt.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct LaunchResult {
/// Spawned process identifier when available.
pub pid: Option<u32>,
/// Child exit code when `wait` was requested and the process exited normally.
pub exit_code: Option<i32>,
/// Identity used for the launched process.
pub identity: LaunchIdentity,
}
#[cfg(not(windows))]
/// Reports the current process token status.
pub fn current_token_status() -> Result<TokenStatus, WindowsSupportError> {
Err(WindowsSupportError::Unsupported(
"msudo is only supported on Windows".to_string(),
))
}
#[cfg(not(windows))]
/// Relaunches the current executable with UAC elevation.
pub fn elevate_current_process(
_arguments: &[String],
_wait: bool,
_show_window: ShowWindowMode,
) -> Result<LaunchResult, WindowsSupportError> {
Err(WindowsSupportError::Unsupported(
"msudo is only supported on Windows".to_string(),
))
}
#[cfg(not(windows))]
/// Reattaches the current process to the parent's console so foreground launches can share it.
pub fn attach_parent_console(_parent_pid: u32) -> Result<(), WindowsSupportError> {
Err(WindowsSupportError::Unsupported(
"msudo is only supported on Windows".to_string(),
))
}
#[cfg(not(windows))]
/// Launches a process with the requested identity.
pub fn launch_request(_request: &LaunchRequest) -> Result<LaunchResult, WindowsSupportError> {
Err(WindowsSupportError::Unsupported(
"msudo is only supported on Windows".to_string(),
))
}
#[cfg(windows)]
#[allow(unsafe_code)]
mod imp {
use std::ffi::{OsStr, c_void};
use std::fs::{File, OpenOptions};
use std::mem::size_of;
use std::os::windows::ffi::OsStrExt;
use std::os::windows::fs::OpenOptionsExt;
use std::os::windows::io::AsRawHandle;
use std::ptr::{null, null_mut};
use std::thread;
use std::time::{Duration, Instant};
use windows_sys::Win32::Foundation::{
CloseHandle, DUPLICATE_SAME_ACCESS, DuplicateHandle, ERROR_ACCESS_DENIED,
ERROR_NOT_ALL_ASSIGNED, FALSE, GetLastError, HANDLE, INVALID_HANDLE_VALUE, TRUE,
WAIT_FAILED, WAIT_OBJECT_0,
};
use windows_sys::Win32::Security::{
AdjustTokenPrivileges, CheckTokenMembership, CreateWellKnownSid, DuplicateTokenEx,
GetTokenInformation, ImpersonateLoggedOnUser, LookupAccountSidW, LookupPrivilegeValueW,
RevertToSelf, SE_PRIVILEGE_ENABLED, SecurityImpersonation, SetTokenInformation,
TOKEN_ACCESS_MASK, TOKEN_ADJUST_DEFAULT, TOKEN_ADJUST_PRIVILEGES, TOKEN_ADJUST_SESSIONID,
TOKEN_ASSIGN_PRIMARY, TOKEN_DUPLICATE, TOKEN_ELEVATION, TOKEN_IMPERSONATE,
TOKEN_MANDATORY_LABEL, TOKEN_PRIVILEGES, TOKEN_QUERY, TokenElevation, TokenIntegrityLevel,
TokenSessionId, WinBuiltinAdministratorsSid, WinLocalSystemSid,
};
use windows_sys::Win32::System::Console::{
AttachConsole, FreeConsole, GetStdHandle, STD_ERROR_HANDLE, STD_HANDLE, STD_INPUT_HANDLE,
STD_OUTPUT_HANDLE, SetConsoleCtrlHandler,
};
use windows_sys::Win32::System::Environment::{
CreateEnvironmentBlock, DestroyEnvironmentBlock,
};
use windows_sys::Win32::System::RemoteDesktop::{
ProcessIdToSessionId, WTSGetActiveConsoleSessionId, WTSQueryUserToken,
};
use windows_sys::Win32::System::Services::{
CloseServiceHandle, OpenSCManagerW, OpenServiceW, QueryServiceStatusEx, SC_HANDLE,
SC_MANAGER_CONNECT, SC_STATUS_PROCESS_INFO, SERVICE_QUERY_STATUS, SERVICE_RUNNING,
SERVICE_START, SERVICE_START_PENDING, SERVICE_STATUS_PROCESS, StartServiceW,
};
use windows_sys::Win32::System::SystemServices::SE_GROUP_INTEGRITY;
use windows_sys::Win32::System::Threading::{
ABOVE_NORMAL_PRIORITY_CLASS, BELOW_NORMAL_PRIORITY_CLASS, CREATE_NEW_CONSOLE,
CREATE_NO_WINDOW, CREATE_UNICODE_ENVIRONMENT, CreateProcessAsUserW, CreateProcessW,
GetCurrentProcess, GetCurrentProcessId, GetExitCodeProcess, HIGH_PRIORITY_CLASS,
IDLE_PRIORITY_CLASS, NORMAL_PRIORITY_CLASS, OpenProcess, OpenProcessToken,
PROCESS_INFORMATION, PROCESS_QUERY_LIMITED_INFORMATION, REALTIME_PRIORITY_CLASS,
STARTF_USESHOWWINDOW, STARTF_USESTDHANDLES, STARTUPINFOW, WaitForSingleObject,
};
use windows_sys::Win32::UI::Shell::{
SEE_MASK_NOCLOSEPROCESS, SHELLEXECUTEINFOW, ShellExecuteExW,
};
use crate::WindowsSupportError;
use crate::sudo::{
LaunchIdentity, LaunchRequest, LaunchResult, PrivilegeMode, ProcessPriority,
ShowWindowMode, TokenIntegrity, TokenStatus,
};
const SECURITY_MAX_SID_SIZE: usize = 68;
const START_TIMEOUT: Duration = Duration::from_secs(10);
const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
const SW_HIDE_VALUE: u16 = 0;
const SW_NORMAL_VALUE: u16 = 1;
const SW_SHOWMINIMIZED_VALUE: u16 = 2;
const SW_SHOWMAXIMIZED_VALUE: u16 = 3;
const SW_SHOWDEFAULT_VALUE: u16 = 10;
const SECURITY_MANDATORY_UNTRUSTED_RID: u32 = 0x0000_0000;
const SECURITY_MANDATORY_LOW_RID: u32 = 0x0000_1000;
const SECURITY_MANDATORY_MEDIUM_RID: u32 = 0x0000_2000;
const SECURITY_MANDATORY_MEDIUM_PLUS_RID: u32 = SECURITY_MANDATORY_MEDIUM_RID + 0x100;
const SECURITY_MANDATORY_HIGH_RID: u32 = 0x0000_3000;
const SECURITY_MANDATORY_SYSTEM_RID: u32 = 0x0000_4000;
#[derive(Debug)]
struct OwnedHandle(HANDLE);
impl OwnedHandle {
fn new(raw: HANDLE) -> Result<Self, WindowsSupportError> {
if raw.is_null() || std::ptr::eq(raw, INVALID_HANDLE_VALUE) {
Err(last_error("acquire handle"))
} else {
Ok(Self(raw))
}
}
const fn raw(&self) -> HANDLE {
self.0
}
}
impl Drop for OwnedHandle {
fn drop(&mut self) {
let _ = unsafe { CloseHandle(self.0) };
}
}
#[derive(Debug)]
struct ImpersonationGuard {
_token: OwnedHandle,
}
impl ImpersonationGuard {
fn as_system() -> Result<Self, WindowsSupportError> {
Self::as_system_with_privileges(&[])
}
fn as_system_with_privileges(privileges: &[&str]) -> Result<Self, WindowsSupportError> {
let token = open_system_impersonation_token()?;
for privilege in privileges {
enable_named_privilege(token.raw(), privilege)?;
}
#[allow(unsafe_code)]
let impersonated = unsafe { ImpersonateLoggedOnUser(token.raw()) };
if impersonated == FALSE {
return Err(last_error("ImpersonateLoggedOnUser"));
}
Ok(Self { _token: token })
}
}
impl Drop for ImpersonationGuard {
fn drop(&mut self) {
let _ = unsafe { RevertToSelf() };
}
}
#[derive(Debug)]
struct OwnedServiceHandle(SC_HANDLE);
impl OwnedServiceHandle {
fn new(raw: SC_HANDLE, action: &'static str) -> Result<Self, WindowsSupportError> {
if raw.is_null() {
Err(last_error(action))
} else {
Ok(Self(raw))
}
}
const fn raw(&self) -> SC_HANDLE {
self.0
}
}
impl Drop for OwnedServiceHandle {
fn drop(&mut self) {
let _ = unsafe { CloseServiceHandle(self.0) };
}
}
#[derive(Debug)]
struct OwnedEnvironmentBlock(*mut c_void);
impl OwnedEnvironmentBlock {
fn for_token(token: HANDLE, inherit: bool) -> Result<Self, WindowsSupportError> {
let mut block = null_mut();
#[allow(unsafe_code)]
let created =
unsafe { CreateEnvironmentBlock(&raw mut block, token, i32::from(inherit)) };
if created == FALSE {
return Err(last_error("CreateEnvironmentBlock"));
}
Ok(Self(block))
}
const fn raw(&self) -> *const c_void {
self.0.cast::<c_void>()
}
}
impl Drop for OwnedEnvironmentBlock {
fn drop(&mut self) {
#[allow(unsafe_code)]
let _ = unsafe { DestroyEnvironmentBlock(self.0.cast::<c_void>()) };
}
}
#[derive(Debug)]
struct InheritedStdHandles {
input: OwnedHandle,
output: OwnedHandle,
error: OwnedHandle,
_stdout_file: Option<File>,
_stderr_file: Option<File>,
}
#[derive(Debug)]
struct ConsoleCtrlGuard {
ignore_ctrl_c: bool,
}
impl ConsoleCtrlGuard {
fn ignore(ignore_ctrl_c: bool) -> Result<Self, WindowsSupportError> {
if !ignore_ctrl_c {
return Ok(Self { ignore_ctrl_c });
}
#[allow(unsafe_code)]
let installed = unsafe { SetConsoleCtrlHandler(None, 1) };
if installed == FALSE {
return Err(last_error("SetConsoleCtrlHandler"));
}
Ok(Self { ignore_ctrl_c })
}
}
impl Drop for ConsoleCtrlGuard {
fn drop(&mut self) {
if self.ignore_ctrl_c {
#[allow(unsafe_code)]
let _ = unsafe { SetConsoleCtrlHandler(None, 0) };
}
}
}
/// Reports the current process token status and privileged-launch capabilities.
/// Query the current process token state and launch capabilities.
///
/// # Errors
///
/// Returns an error when the current process token cannot be inspected.
pub fn current_token_status() -> Result<TokenStatus, WindowsSupportError> {
let token = open_current_token(TOKEN_QUERY)?;
let is_elevated = token_elevation(token.raw())?;
let is_admin_member = current_token_is_admin_member()?;
let integrity = token_integrity(token.raw())?;
let current_user = token_user_name(token.raw())?;
let session_id = current_process_session_id();
let active_session_id = active_console_session_id();
let (trustedinstaller_installed, trustedinstaller_running) = trustedinstaller_presence()?;
Ok(TokenStatus {
is_elevated,
is_admin_member,
integrity,
current_user,
session_id,
active_session_id,
can_admin: true,
can_current_user: active_session_id.is_some(),
can_system: is_elevated,
can_trustedinstaller: is_elevated && trustedinstaller_installed,
trustedinstaller_installed,
trustedinstaller_running,
})
}
/// Relaunches the current executable through the Windows `runas` verb.
/// Relaunch the current executable with `runas` so it can continue privileged work.
///
/// # Errors
///
/// Returns an error when the elevated helper cannot be started or waited on.
///
/// # Panics
///
/// Panics only if a Win32 structure size unexpectedly stops fitting in `u32`.
pub fn elevate_current_process(
arguments: &[String],
wait: bool,
show_window: ShowWindowMode,
) -> Result<LaunchResult, WindowsSupportError> {
let executable = std::env::current_exe().map_err(|error| {
WindowsSupportError::Process(format!("failed to locate current executable: {error}"))
})?;
let executable_wide = wide_from_os(executable.as_os_str());
let verb = wide("runas");
let parameters = quote_command_line(arguments.iter().map(String::as_str));
let parameters_wide = wide(&parameters);
let mut execute = SHELLEXECUTEINFOW {
cbSize: u32::try_from(size_of::<SHELLEXECUTEINFOW>())
.expect("shell execute info size always fits in u32"),
fMask: SEE_MASK_NOCLOSEPROCESS,
lpVerb: verb.as_ptr(),
lpFile: executable_wide.as_ptr(),
lpParameters: parameters_wide.as_ptr(),
nShow: i32::from(show_window_value(show_window)),
..SHELLEXECUTEINFOW::default()
};
#[allow(unsafe_code)]
if unsafe { ShellExecuteExW(&raw mut execute) } == FALSE {
return Err(last_error("ShellExecuteExW"));
}
let process = if execute.hProcess.is_null() {
None
} else {
Some(OwnedHandle::new(execute.hProcess)?)
};
let (pid, exit_code) = if let Some(process) = process.as_ref() {
let pid = process_id_from_handle(process.raw())?;
let exit_code = if wait {
Some(wait_for_exit_code(process.raw())?)
} else {
None
};
(Some(pid), exit_code)
} else {
(None, None)
};
Ok(LaunchResult {
pid,
exit_code,
identity: LaunchIdentity::Admin,
})
}
/// Reattach the current process to a caller-owned console before starting a foreground child.
///
/// # Errors
///
/// Returns an error when the console cannot be rebound to the requested parent process.
pub fn attach_parent_console(parent_pid: u32) -> Result<(), WindowsSupportError> {
if parent_pid == 0 {
return Err(WindowsSupportError::Process(
"same-console relay requires a non-zero parent console pid".to_string(),
));
}
#[allow(unsafe_code)]
let _ = unsafe { FreeConsole() };
#[allow(unsafe_code)]
let attached = unsafe { AttachConsole(parent_pid) };
if attached == FALSE {
return Err(last_error("AttachConsole"));
}
Ok(())
}
/// Launches a process with the requested identity in the current elevated context.
/// Launch a process using the requested identity and token shaping options.
///
/// # Errors
///
/// Returns an error when token acquisition, mutation, or process creation fails.
pub fn launch_request(request: &LaunchRequest) -> Result<LaunchResult, WindowsSupportError> {
match request.identity {
LaunchIdentity::CurrentUser => launch_with_active_user(request),
LaunchIdentity::Admin if admin_launch_requires_duplicate_token(request) => {
launch_with_duplicate_identity(request, LaunchIdentity::Admin)
}
LaunchIdentity::CurrentProcess | LaunchIdentity::Admin => {
launch_with_current_identity(request)
}
LaunchIdentity::System => {
launch_with_duplicate_identity(request, LaunchIdentity::System)
}
LaunchIdentity::TrustedInstaller => {
launch_with_duplicate_identity(request, LaunchIdentity::TrustedInstaller)
}
}
}
const fn admin_launch_requires_duplicate_token(request: &LaunchRequest) -> bool {
request.session.is_some()
|| request.integrity.is_some()
|| !matches!(request.privileges, PrivilegeMode::Default)
}
fn launch_with_current_identity(
request: &LaunchRequest,
) -> Result<LaunchResult, WindowsSupportError> {
let desktop = interactive_desktop(request);
let mut startup = build_startup_info(request.show_window, desktop.as_deref());
let std_handles = inherited_standard_handles(request)?;
apply_standard_handles(&mut startup, std_handles.as_ref());
let mut process_info = PROCESS_INFORMATION::default();
let application = application_name_wide(&request.program);
let command_line_text = build_process_command_line(&request.program, &request.args);
let mut command_line = wide(&command_line_text);
let mut environment = build_environment_block();
let current_directory = request.current_directory.as_deref().map(wide);
let creation_flags = creation_flags(request);
#[allow(unsafe_code)]
let created = unsafe {
CreateProcessW(
application.as_ref().map_or(null(), Vec::as_ptr),
command_line.as_mut_ptr(),
null(),
null(),
inherit_handles_flag(std_handles.as_ref()),
creation_flags,
environment.as_mut_ptr().cast::<c_void>(),
current_directory.as_ref().map_or(null(), Vec::as_ptr),
&raw mut startup,
&raw mut process_info,
)
};
if created == FALSE {
return Err(last_error("CreateProcessW"));
}
let thread = OwnedHandle::new(process_info.hThread)?;
drop(thread);
let process = OwnedHandle::new(process_info.hProcess)?;
let exit_code = if request.wait {
Some(wait_for_exit_code(process.raw())?)
} else {
None
};
Ok(LaunchResult {
pid: Some(process_info.dwProcessId),
exit_code,
identity: request.identity,
})
}
fn launch_with_duplicate_identity(
request: &LaunchRequest,
identity: LaunchIdentity,
) -> Result<LaunchResult, WindowsSupportError> {
let status = current_token_status()?;
if !status.is_elevated {
return Err(WindowsSupportError::Unsupported(format!(
"{identity:?} launch requires an elevated administrator context"
)));
}
enable_token_discovery_privileges()?;
let token = duplicate_primary_token(identity, None)?;
let _impersonation = ImpersonationGuard::as_system_with_privileges(&[
"SeTcbPrivilege",
"SeAssignPrimaryTokenPrivilege",
"SeIncreaseQuotaPrivilege",
"SeImpersonatePrivilege",
"SeDebugPrivilege",
])?;
if let Some(session_id) = resolve_target_session_id(request.session) {
set_token_session_id(token.raw(), session_id)?;
}
apply_token_options(&token, request.privileges, request.integrity)?;
launch_with_token_as_user(request, identity, &token)
}
fn duplicate_primary_token(
identity: LaunchIdentity,
session: Option<u32>,
) -> Result<OwnedHandle, WindowsSupportError> {
let source = match identity {
LaunchIdentity::Admin | LaunchIdentity::CurrentProcess => open_current_token(
TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY | TOKEN_ADJUST_DEFAULT,
)?,
LaunchIdentity::CurrentUser => open_active_user_process_token(session)?,
LaunchIdentity::System => open_system_process_token()?,
LaunchIdentity::TrustedInstaller => open_trustedinstaller_process_token()?,
};
let desired_access: TOKEN_ACCESS_MASK = TOKEN_QUERY
| TOKEN_DUPLICATE
| TOKEN_ASSIGN_PRIMARY
| TOKEN_ADJUST_DEFAULT
| TOKEN_ADJUST_SESSIONID
| TOKEN_ADJUST_PRIVILEGES;
let mut duplicate: HANDLE = null_mut();
#[allow(unsafe_code)]
let duplicated = unsafe {
DuplicateTokenEx(
source.raw(),
desired_access,
null(),
SecurityImpersonation,
windows_sys::Win32::Security::TokenPrimary,
&raw mut duplicate,
)
};
if duplicated == FALSE {
return Err(last_error("DuplicateTokenEx"));
}
OwnedHandle::new(duplicate)
}
fn apply_token_options(
token: &OwnedHandle,
privileges: PrivilegeMode,
integrity: Option<TokenIntegrity>,
) -> Result<(), WindowsSupportError> {
match privileges {
PrivilegeMode::Default => {}
PrivilegeMode::EnableAll => enable_token_privileges(token.raw())?,
PrivilegeMode::DisableAll => disable_token_privileges(token.raw())?,
}
if let Some(integrity) = integrity {
set_token_integrity(token.raw(), integrity)?;
}
Ok(())
}
fn launch_with_token_as_user(
request: &LaunchRequest,
identity: LaunchIdentity,
token: &OwnedHandle,
) -> Result<LaunchResult, WindowsSupportError> {
let desktop = interactive_desktop(request);
let mut startup = build_startup_info(request.show_window, desktop.as_deref());
let std_handles = inherited_standard_handles(request)?;
apply_standard_handles(&mut startup, std_handles.as_ref());
let mut process_info = PROCESS_INFORMATION::default();
let application = application_name_wide(&request.program);
let command_line_text = build_process_command_line(&request.program, &request.args);
let mut command_line = wide(&command_line_text);
let environment = OwnedEnvironmentBlock::for_token(token.raw(), false)?;
let current_directory = request.current_directory.as_deref().map(wide);
let creation_flags = creation_flags(request);
#[allow(unsafe_code)]
let created = unsafe {
CreateProcessAsUserW(
token.raw(),
application.as_ref().map_or(null(), Vec::as_ptr),
command_line.as_mut_ptr(),
null(),
null(),
inherit_handles_flag(std_handles.as_ref()),
creation_flags,
environment.raw().cast_mut(),
current_directory.as_ref().map_or(null(), Vec::as_ptr),
&raw mut startup,
&raw mut process_info,
)
};
if created == FALSE {
return Err(last_error("CreateProcessAsUserW"));
}
finalize_launch(request, identity, process_info)
}
fn launch_with_active_user(
request: &LaunchRequest,
) -> Result<LaunchResult, WindowsSupportError> {
let status = current_token_status()?;
if !request_requires_token_shaping(request) {
return launch_with_current_identity(request);
}
if !status.is_elevated {
return Err(WindowsSupportError::Unsupported(
"current-user launch requires an elevated administrator context".to_string(),
));
}
enable_token_discovery_privileges()?;
let _impersonation = ImpersonationGuard::as_system_with_privileges(&[
"SeTcbPrivilege",
"SeAssignPrimaryTokenPrivilege",
"SeIncreaseQuotaPrivilege",
"SeImpersonatePrivilege",
])?;
let token = duplicate_primary_token(LaunchIdentity::CurrentUser, request.session)?;
apply_token_options(&token, request.privileges, request.integrity)?;
launch_with_token_as_user(request, LaunchIdentity::CurrentUser, &token)
}
const fn request_requires_token_shaping(request: &LaunchRequest) -> bool {
request.session.is_some()
|| request.integrity.is_some()
|| !matches!(request.privileges, PrivilegeMode::Default)
}
fn finalize_launch(
request: &LaunchRequest,
identity: LaunchIdentity,
process_info: PROCESS_INFORMATION,
) -> Result<LaunchResult, WindowsSupportError> {
let thread = OwnedHandle::new(process_info.hThread)?;
drop(thread);
let process = OwnedHandle::new(process_info.hProcess)?;
let exit_code = if request.wait {
let _console_ctrl = ConsoleCtrlGuard::ignore(request.same_console)?;
Some(wait_for_exit_code(process.raw())?)
} else {
None
};
Ok(LaunchResult {
pid: Some(process_info.dwProcessId),
exit_code,
identity,
})
}
fn open_system_process_token() -> Result<OwnedHandle, WindowsSupportError> {
open_preferred_system_process_token(TOKEN_QUERY | TOKEN_DUPLICATE)
}
fn open_system_impersonation_token() -> Result<OwnedHandle, WindowsSupportError> {
let source = open_system_process_token()?;
duplicate_token(
source.raw(),
TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_IMPERSONATE | TOKEN_ADJUST_PRIVILEGES,
windows_sys::Win32::Security::TokenImpersonation,
)
}
fn open_active_user_process_token(
explicit_session: Option<u32>,
) -> Result<OwnedHandle, WindowsSupportError> {
let session = explicit_session
.or_else(active_console_session_id)
.or_else(current_process_session_id)
.ok_or_else(|| {
WindowsSupportError::Process("no active user session found".to_string())
})?;
let mut token: HANDLE = null_mut();
#[allow(unsafe_code)]
let queried = unsafe { WTSQueryUserToken(session, &raw mut token) };
if queried == FALSE {
return Err(last_error("WTSQueryUserToken"));
}
OwnedHandle::new(token)
}
fn open_preferred_system_process_token(
access: TOKEN_ACCESS_MASK,
) -> Result<OwnedHandle, WindowsSupportError> {
let expected = well_known_sid(WinLocalSystemSid)?;
let processes = crate::snapshot_processes();
let current_session = current_process_session_id();
for process in processes
.iter()
.filter(|process| {
process.image_name.eq_ignore_ascii_case("winlogon.exe")
&& current_session == process_session_id(process.pid)
})
.chain(
processes
.iter()
.filter(|process| process.image_name.eq_ignore_ascii_case("lsass.exe")),
)
.chain(processes.iter())
{
let opened = open_process_token_for_pid(process.pid, access);
let Ok(token) = opened else {
continue;
};
if token_user_matches_sid(token.raw(), &expected)? {
return Ok(token);
}
}
Err(WindowsSupportError::Process(
"failed to locate a usable SYSTEM process token".to_string(),
))
}
fn open_trustedinstaller_process_token() -> Result<OwnedHandle, WindowsSupportError> {
let started = Instant::now();
let _impersonation = ImpersonationGuard::as_system()?;
loop {
let pid = ensure_trustedinstaller_running()?;
match open_process_token_for_pid(pid, TOKEN_QUERY | TOKEN_DUPLICATE) {
Ok(token) => return Ok(token),
Err(error) => {
if !should_retry_trustedinstaller_token_open(&error)
|| started.elapsed() >= START_TIMEOUT
{
return Err(error);
}
thread::sleep(Duration::from_millis(100));
}
}
}
}
fn duplicate_token(
source: HANDLE,
desired_access: TOKEN_ACCESS_MASK,
token_type: windows_sys::Win32::Security::TOKEN_TYPE,
) -> Result<OwnedHandle, WindowsSupportError> {
let mut duplicate: HANDLE = null_mut();
#[allow(unsafe_code)]
let duplicated = unsafe {
DuplicateTokenEx(
source,
desired_access,
null(),
SecurityImpersonation,
token_type,
&raw mut duplicate,
)
};
if duplicated == FALSE {
return Err(last_error("DuplicateTokenEx"));
}
OwnedHandle::new(duplicate)
}
fn open_process_token_for_pid(
pid: u32,
access: TOKEN_ACCESS_MASK,
) -> Result<OwnedHandle, WindowsSupportError> {
#[allow(unsafe_code)]
let process = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid) };
let process = OwnedHandle::new(process)?;
let mut token: HANDLE = null_mut();
#[allow(unsafe_code)]
let opened = unsafe { OpenProcessToken(process.raw(), access, &raw mut token) };
if opened == FALSE {
return Err(last_error("OpenProcessToken"));
}
OwnedHandle::new(token)
}
fn process_session_id(pid: u32) -> Option<u32> {
let mut session = 0_u32;
#[allow(unsafe_code)]
let ok = unsafe { ProcessIdToSessionId(pid, &raw mut session) };
(ok != FALSE).then_some(session)
}
fn should_retry_trustedinstaller_token_open(error: &WindowsSupportError) -> bool {
matches!(
error,
WindowsSupportError::WindowsApi {
action: "OpenProcessToken" | "OpenProcess",
code: ERROR_ACCESS_DENIED,
}
)
}
fn trustedinstaller_presence() -> Result<(bool, bool), WindowsSupportError> {
let manager = unsafe { OpenSCManagerW(null(), null(), SC_MANAGER_CONNECT) };
let manager = match OwnedServiceHandle::new(manager, "OpenSCManagerW") {
Ok(value) => value,
Err(error) => {
return match error {
WindowsSupportError::WindowsApi { .. } => Ok((false, false)),
other => Err(other),
};
}
};
let service_name = wide("TrustedInstaller");
let service =
unsafe { OpenServiceW(manager.raw(), service_name.as_ptr(), SERVICE_QUERY_STATUS) };
let service = match OwnedServiceHandle::new(service, "OpenServiceW") {
Ok(value) => value,
Err(error) => {
return match error {
WindowsSupportError::WindowsApi { .. } => Ok((false, false)),
other => Err(other),
};
}
};
let running = service_running(service.raw())?;
Ok((true, running))
}
fn ensure_trustedinstaller_running() -> Result<u32, WindowsSupportError> {
let manager = OwnedServiceHandle::new(
unsafe { OpenSCManagerW(null(), null(), SC_MANAGER_CONNECT) },
"OpenSCManagerW",
)?;
let service_name = wide("TrustedInstaller");
let service = OwnedServiceHandle::new(
unsafe {
OpenServiceW(
manager.raw(),
service_name.as_ptr(),
SERVICE_QUERY_STATUS | SERVICE_START,
)
},
"OpenServiceW",
)?;
let mut deadline = Instant::now() + START_TIMEOUT;
let mut last_checkpoint = 0_u32;
let mut start_requested = false;
loop {
let status = query_service_status(service.raw())?;
if status.dwCurrentState == SERVICE_RUNNING {
return Ok(status.dwProcessId);
}
if status.dwCurrentState == SERVICE_START_PENDING {
if status.dwCheckPoint != 0 && status.dwCheckPoint != last_checkpoint {
last_checkpoint = status.dwCheckPoint;
deadline = Instant::now() + START_TIMEOUT;
}
} else if !start_requested {
#[allow(unsafe_code)]
let _ = unsafe { StartServiceW(service.raw(), 0, null()) };
start_requested = true;
}
if Instant::now() >= deadline {
return Err(WindowsSupportError::Process(
"timed out waiting for TrustedInstaller service".to_string(),
));
}
let wait_hint = if status.dwWaitHint == 0 {
100
} else {
status.dwWaitHint.saturating_div(10).clamp(100, 1_000)
};
thread::sleep(Duration::from_millis(u64::from(wait_hint)));
}
}
fn service_running(service: SC_HANDLE) -> Result<bool, WindowsSupportError> {
let status = query_service_status(service)?;
Ok(status.dwCurrentState == SERVICE_RUNNING)
}
fn query_service_status(
service: SC_HANDLE,
) -> Result<SERVICE_STATUS_PROCESS, WindowsSupportError> {
let mut status = SERVICE_STATUS_PROCESS::default();
let mut needed = 0_u32;
#[allow(unsafe_code)]
let queried = unsafe {
QueryServiceStatusEx(
service,
SC_STATUS_PROCESS_INFO,
(&raw mut status).cast::<u8>(),
u32::try_from(size_of::<SERVICE_STATUS_PROCESS>())
.expect("service status size always fits in u32"),
&raw mut needed,
)
};
if queried == FALSE {
return Err(last_error("QueryServiceStatusEx"));
}
Ok(status)
}
fn enable_token_discovery_privileges() -> Result<(), WindowsSupportError> {
let token = open_current_token(TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY)?;
for privilege in ["SeDebugPrivilege", "SeImpersonatePrivilege"] {
enable_named_privilege(token.raw(), privilege)?;
}
Ok(())
}
fn enable_token_privileges(token: HANDLE) -> Result<(), WindowsSupportError> {
let bytes = get_token_info_bytes(token, windows_sys::Win32::Security::TokenPrivileges)?;
#[allow(unsafe_code)]
let privilege_count = unsafe { std::ptr::read_unaligned(bytes.as_ptr().cast::<u32>()) };
let privileges_offset = std::mem::offset_of!(TOKEN_PRIVILEGES, Privileges);
for index in 0..usize::try_from(privilege_count).expect("privilege count fits usize") {
#[allow(unsafe_code)]
let privilege = unsafe {
std::ptr::read_unaligned(
bytes
.as_ptr()
.add(
privileges_offset
+ index
* size_of::<windows_sys::Win32::Security::LUID_AND_ATTRIBUTES>(
),
)
.cast::<windows_sys::Win32::Security::LUID_AND_ATTRIBUTES>(),
)
};
adjust_single_token_privilege(token, privilege.Luid, SE_PRIVILEGE_ENABLED)?;
}
Ok(())
}
fn disable_token_privileges(token: HANDLE) -> Result<(), WindowsSupportError> {
#[allow(unsafe_code)]
let adjusted =
unsafe { AdjustTokenPrivileges(token, 1, null_mut(), 0, null_mut(), null_mut()) };
if adjusted == FALSE {
return Err(last_error("AdjustTokenPrivileges"));
}
Ok(())
}
fn enable_named_privilege(token: HANDLE, name: &str) -> Result<(), WindowsSupportError> {
let name_wide = wide(name);
let mut luid = windows_sys::Win32::Foundation::LUID::default();
#[allow(unsafe_code)]
let looked_up = unsafe { LookupPrivilegeValueW(null(), name_wide.as_ptr(), &raw mut luid) };
if looked_up == FALSE {
return Err(last_error("LookupPrivilegeValueW"));
}
adjust_single_token_privilege(token, luid, SE_PRIVILEGE_ENABLED)
}
fn adjust_single_token_privilege(
token: HANDLE,
luid: windows_sys::Win32::Foundation::LUID,
attributes: u32,
) -> Result<(), WindowsSupportError> {
let mut privileges = TOKEN_PRIVILEGES {
PrivilegeCount: 1,
Privileges: [windows_sys::Win32::Security::LUID_AND_ATTRIBUTES {
Luid: luid,
Attributes: attributes,
}],
};
#[allow(unsafe_code)]
let adjusted = unsafe {
AdjustTokenPrivileges(
token,
FALSE,
&raw mut privileges,
u32::try_from(size_of::<TOKEN_PRIVILEGES>())
.expect("token privileges size always fits in u32"),
null_mut(),
null_mut(),
)
};
if adjusted == FALSE {
return Err(last_error("AdjustTokenPrivileges"));
}
#[allow(unsafe_code)]
let status = unsafe { GetLastError() };
if status == ERROR_NOT_ALL_ASSIGNED {
return Err(WindowsSupportError::Process(
"token did not contain every requested privilege".to_string(),
));
}
Ok(())
}
fn open_current_token(access: TOKEN_ACCESS_MASK) -> Result<OwnedHandle, WindowsSupportError> {
let mut token: HANDLE = null_mut();
#[allow(unsafe_code)]
let opened = unsafe { OpenProcessToken(GetCurrentProcess(), access, &raw mut token) };
if opened == FALSE {
return Err(last_error("OpenProcessToken"));
}
OwnedHandle::new(token)
}
fn token_elevation(token: HANDLE) -> Result<bool, WindowsSupportError> {
let elevation: TOKEN_ELEVATION = get_token_info(token, TokenElevation)?;
Ok(elevation.TokenIsElevated != 0)
}
fn token_integrity(token: HANDLE) -> Result<TokenIntegrity, WindowsSupportError> {
let bytes = get_token_info_bytes(token, TokenIntegrityLevel)?;
let label: TOKEN_MANDATORY_LABEL =
unsafe { std::ptr::read_unaligned(bytes.as_ptr().cast::<TOKEN_MANDATORY_LABEL>()) };
if label.Label.Attributes
& u32::try_from(SE_GROUP_INTEGRITY).expect("integrity flag fits u32")
== 0
{
return Ok(TokenIntegrity::Unknown);
}
let sid = label.Label.Sid;
let authority = sub_authority_count(sid)?;
let rid = sub_authority(sid, authority.saturating_sub(1))?;
Ok(match rid {
SECURITY_MANDATORY_UNTRUSTED_RID => TokenIntegrity::Untrusted,
SECURITY_MANDATORY_LOW_RID => TokenIntegrity::Low,
SECURITY_MANDATORY_MEDIUM_RID => TokenIntegrity::Medium,
SECURITY_MANDATORY_MEDIUM_PLUS_RID => TokenIntegrity::MediumPlus,
SECURITY_MANDATORY_HIGH_RID => TokenIntegrity::High,
SECURITY_MANDATORY_SYSTEM_RID => TokenIntegrity::System,
_ => TokenIntegrity::Unknown,
})
}
fn set_token_integrity(
token: HANDLE,
integrity: TokenIntegrity,
) -> Result<(), WindowsSupportError> {
let Some(rid) = integrity_rid(integrity) else {
return Ok(());
};
let mut sid = mandatory_label_sid(rid);
let mut label = TOKEN_MANDATORY_LABEL {
Label: windows_sys::Win32::Security::SID_AND_ATTRIBUTES {
Sid: sid.as_mut_ptr().cast::<c_void>(),
Attributes: u32::try_from(SE_GROUP_INTEGRITY).expect("integrity flag fits u32"),
},
};
#[allow(unsafe_code)]
let updated = unsafe {
SetTokenInformation(
token,
TokenIntegrityLevel,
(&raw mut label).cast::<c_void>(),
u32::try_from(size_of::<TOKEN_MANDATORY_LABEL>() + sid.len())
.expect("mandatory label buffer size fits in u32"),
)
};
if updated == FALSE {
return Err(last_error("SetTokenInformation"));
}
Ok(())
}
const fn integrity_rid(integrity: TokenIntegrity) -> Option<u32> {
match integrity {
TokenIntegrity::Unknown => None,
TokenIntegrity::Untrusted => Some(SECURITY_MANDATORY_UNTRUSTED_RID),
TokenIntegrity::Low => Some(SECURITY_MANDATORY_LOW_RID),
TokenIntegrity::Medium => Some(SECURITY_MANDATORY_MEDIUM_RID),
TokenIntegrity::MediumPlus => Some(SECURITY_MANDATORY_MEDIUM_PLUS_RID),
TokenIntegrity::High => Some(SECURITY_MANDATORY_HIGH_RID),
TokenIntegrity::System => Some(SECURITY_MANDATORY_SYSTEM_RID),
}
}
fn mandatory_label_sid(rid: u32) -> [u8; 12] {
let mut sid = [0_u8; 12];
sid[0] = 1;
sid[1] = 1;
sid[7] = 16;
sid[8..12].copy_from_slice(&rid.to_le_bytes());
sid
}
fn token_user_name(token: HANDLE) -> Result<Option<String>, WindowsSupportError> {
let bytes = get_token_info_bytes(token, windows_sys::Win32::Security::TokenUser)?;
let user: windows_sys::Win32::Security::TOKEN_USER = unsafe {
std::ptr::read_unaligned(
bytes
.as_ptr()
.cast::<windows_sys::Win32::Security::TOKEN_USER>(),
)
};
let sid = user.User.Sid;
let mut name_len = 0_u32;
let mut domain_len = 0_u32;
let mut use_type = 0_i32;
#[allow(unsafe_code)]
let _ = unsafe {
LookupAccountSidW(
null(),
sid,
null_mut(),
&raw mut name_len,
null_mut(),
&raw mut domain_len,
&raw mut use_type,
)
};
if name_len == 0 {
return Ok(None);
}
let mut name = vec![0_u16; usize::try_from(name_len).expect("name length fits usize")];
let mut domain =
vec![0_u16; usize::try_from(domain_len).expect("domain length fits usize")];
#[allow(unsafe_code)]
let looked_up = unsafe {
LookupAccountSidW(
null(),
sid,
name.as_mut_ptr(),
&raw mut name_len,
domain.as_mut_ptr(),
&raw mut domain_len,
&raw mut use_type,
)
};
if looked_up == FALSE {
return Ok(None);
}
let name =
wide_to_string(&name[..usize::try_from(name_len).expect("name length fits usize")]);
let domain = wide_to_string(
&domain[..usize::try_from(domain_len).expect("domain length fits usize")],
);
if domain.is_empty() {
Ok(Some(name))
} else {
Ok(Some(format!("{domain}\\{name}")))
}
}
fn current_token_is_admin_member() -> Result<bool, WindowsSupportError> {
let sid = well_known_sid(WinBuiltinAdministratorsSid)?;
let mut result = FALSE;
#[allow(unsafe_code)]
let checked = unsafe {
CheckTokenMembership(
null_mut(),
sid.as_ptr().cast_mut().cast::<c_void>(),
&raw mut result,
)
};
if checked == FALSE {
return Err(last_error("CheckTokenMembership"));
}
Ok(result != FALSE)
}
fn current_process_session_id() -> Option<u32> {
let mut session = 0_u32;
#[allow(unsafe_code)]
let ok = unsafe { ProcessIdToSessionId(GetCurrentProcessId(), &raw mut session) };
(ok != FALSE).then_some(session)
}
fn active_console_session_id() -> Option<u32> {
#[allow(unsafe_code)]
let session = unsafe { WTSGetActiveConsoleSessionId() };
(session != u32::MAX).then_some(session)
}
fn resolve_target_session_id(explicit_session: Option<u32>) -> Option<u32> {
explicit_session
.or_else(|| current_process_session_id().filter(|session| *session != 0))
.or_else(active_console_session_id)
.or_else(current_process_session_id)
}
fn token_user_matches_sid(token: HANDLE, expected: &[u8]) -> Result<bool, WindowsSupportError> {
let bytes = get_token_info_bytes(token, windows_sys::Win32::Security::TokenUser)?;
let user: windows_sys::Win32::Security::TOKEN_USER = unsafe {
std::ptr::read_unaligned(
bytes
.as_ptr()
.cast::<windows_sys::Win32::Security::TOKEN_USER>(),
)
};
sid_equal(user.User.Sid, expected.as_ptr().cast::<c_void>())
}
fn sid_equal(left: *mut c_void, right: *const c_void) -> Result<bool, WindowsSupportError> {
#[allow(unsafe_code)]
let left_len = unsafe { windows_sys::Win32::Security::GetLengthSid(left) };
#[allow(unsafe_code)]
let right_len = unsafe { windows_sys::Win32::Security::GetLengthSid(right.cast_mut()) };
if left_len != right_len {
return Ok(false);
}
let left = sid_bytes(left, left_len)?;
let right = sid_bytes(right.cast_mut(), right_len)?;
Ok(left == right)
}
fn sid_bytes(sid: *mut c_void, len: u32) -> Result<Vec<u8>, WindowsSupportError> {
let len = usize::try_from(len).expect("sid length fits usize");
if sid.is_null() {
return Err(WindowsSupportError::Process("null SID pointer".to_string()));
}
#[allow(unsafe_code)]
let slice = unsafe { std::slice::from_raw_parts(sid.cast::<u8>(), len) };
Ok(slice.to_vec())
}
fn well_known_sid(kind: i32) -> Result<Vec<u8>, WindowsSupportError> {
let mut bytes = vec![0_u8; SECURITY_MAX_SID_SIZE];
let mut size = u32::try_from(bytes.len()).expect("sid buffer size fits u32");
#[allow(unsafe_code)]
let created = unsafe {
CreateWellKnownSid(
kind,
null_mut(),
bytes.as_mut_ptr().cast::<c_void>(),
&raw mut size,
)
};
if created == FALSE {
return Err(last_error("CreateWellKnownSid"));
}
bytes.truncate(usize::try_from(size).expect("sid size fits usize"));
Ok(bytes)
}
fn sub_authority_count(sid: *mut c_void) -> Result<u32, WindowsSupportError> {
if sid.is_null() {
return Err(WindowsSupportError::Process("null SID pointer".to_string()));
}
#[allow(unsafe_code)]
let count = unsafe { *(sid.cast::<u8>().add(1)) };
Ok(u32::from(count))
}
fn sub_authority(sid: *mut c_void, index: u32) -> Result<u32, WindowsSupportError> {
if sid.is_null() {
return Err(WindowsSupportError::Process("null SID pointer".to_string()));
}
let offset = 8_usize + usize::try_from(index).expect("sid index fits usize") * 4;
#[allow(unsafe_code)]
let value = unsafe { std::ptr::read_unaligned(sid.cast::<u8>().add(offset).cast::<u32>()) };
Ok(value)
}
fn set_token_session_id(token: HANDLE, session: u32) -> Result<(), WindowsSupportError> {
let mut session = session;
#[allow(unsafe_code)]
let updated = unsafe {
SetTokenInformation(
token,
TokenSessionId,
(&raw mut session).cast::<c_void>(),
u32::try_from(size_of::<u32>()).expect("u32 size always fits in u32"),
)
};
if updated == FALSE {
return Err(last_error("SetTokenInformation"));
}
Ok(())
}
fn interactive_desktop(request: &LaunchRequest) -> Option<Vec<u16>> {
if request.new_window || request.same_console {
Some(wide("winsta0\\default"))
} else {
None
}
}
fn build_startup_info(show_window: ShowWindowMode, desktop: Option<&[u16]>) -> STARTUPINFOW {
STARTUPINFOW {
cb: u32::try_from(size_of::<STARTUPINFOW>())
.expect("startup info size always fits in u32"),
dwFlags: STARTF_USESHOWWINDOW,
wShowWindow: show_window_value(show_window),
lpDesktop: desktop.map_or(null_mut(), |value| value.as_ptr().cast_mut()),
..STARTUPINFOW::default()
}
}
fn inherited_standard_handles(
request: &LaunchRequest,
) -> Result<Option<InheritedStdHandles>, WindowsSupportError> {
if request.new_window {
return Ok(None);
}
let (output, stdout_file) = if let Some(path) = &request.stdout_path {
let file = create_relay_output_file(path)?;
(
duplicate_inheritable_handle(file.as_raw_handle().cast::<c_void>(), "stdout file")?,
Some(file),
)
} else {
(duplicate_standard_handle(STD_OUTPUT_HANDLE)?, None)
};
let (error, stderr_file) = if let Some(path) = &request.stderr_path {
let file = create_relay_output_file(path)?;
(
duplicate_inheritable_handle(file.as_raw_handle().cast::<c_void>(), "stderr file")?,
Some(file),
)
} else {
(duplicate_standard_handle(STD_ERROR_HANDLE)?, None)
};
Ok(Some(InheritedStdHandles {
input: duplicate_standard_handle(STD_INPUT_HANDLE)?,
output,
error,
_stdout_file: stdout_file,
_stderr_file: stderr_file,
}))
}
fn duplicate_standard_handle(
handle_id: STD_HANDLE,
) -> Result<OwnedHandle, WindowsSupportError> {
#[allow(unsafe_code)]
let source = unsafe { GetStdHandle(handle_id) };
if source.is_null() || std::ptr::eq(source, INVALID_HANDLE_VALUE) {
return Err(last_error("GetStdHandle"));
}
duplicate_inheritable_handle(source, "standard handle")
}
fn create_relay_output_file(path: &str) -> Result<File, WindowsSupportError> {
OpenOptions::new()
.create_new(true)
.write(true)
.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
.open(path)
.map_err(|error| {
WindowsSupportError::Process(format!(
"failed to create relay output file {path}: {error}"
))
})
}
fn duplicate_inheritable_handle(
source: HANDLE,
action: &'static str,
) -> Result<OwnedHandle, WindowsSupportError> {
#[allow(unsafe_code)]
let current_process = unsafe { GetCurrentProcess() };
let mut duplicate: HANDLE = null_mut();
#[allow(unsafe_code)]
let duplicated = unsafe {
DuplicateHandle(
current_process,
source,
current_process,
&raw mut duplicate,
0,
TRUE,
DUPLICATE_SAME_ACCESS,
)
};
if duplicated == FALSE {
return Err(last_error(action));
}
OwnedHandle::new(duplicate)
}
const fn apply_standard_handles(
startup: &mut STARTUPINFOW,
handles: Option<&InheritedStdHandles>,
) {
if let Some(handles) = handles {
startup.dwFlags |= STARTF_USESTDHANDLES;
startup.hStdInput = handles.input.raw();
startup.hStdOutput = handles.output.raw();
startup.hStdError = handles.error.raw();
}
}
const fn inherit_handles_flag(handles: Option<&InheritedStdHandles>) -> i32 {
if handles.is_some() { TRUE } else { FALSE }
}
const fn creation_flags(request: &LaunchRequest) -> u32 {
let mut flags = priority_flag(request.priority) | CREATE_UNICODE_ENVIRONMENT;
if request.new_window {
flags |= CREATE_NEW_CONSOLE;
} else if matches!(request.show_window, ShowWindowMode::Hidden) {
flags |= CREATE_NO_WINDOW;
}
flags
}
const fn priority_flag(priority: ProcessPriority) -> u32 {
match priority {
ProcessPriority::Idle => IDLE_PRIORITY_CLASS,
ProcessPriority::BelowNormal => BELOW_NORMAL_PRIORITY_CLASS,
ProcessPriority::Normal => NORMAL_PRIORITY_CLASS,
ProcessPriority::AboveNormal => ABOVE_NORMAL_PRIORITY_CLASS,
ProcessPriority::High => HIGH_PRIORITY_CLASS,
ProcessPriority::Realtime => REALTIME_PRIORITY_CLASS,
}
}
const fn show_window_value(mode: ShowWindowMode) -> u16 {
match mode {
ShowWindowMode::Default => SW_SHOWDEFAULT_VALUE,
ShowWindowMode::Hidden => SW_HIDE_VALUE,
ShowWindowMode::Normal => SW_NORMAL_VALUE,
ShowWindowMode::Minimized => SW_SHOWMINIMIZED_VALUE,
ShowWindowMode::Maximized => SW_SHOWMAXIMIZED_VALUE,
}
}
fn build_process_command_line(program: &str, args: &[String]) -> String {
let mut values = Vec::with_capacity(args.len() + 1);
values.push(program);
values.extend(args.iter().map(String::as_str));
quote_command_line(values)
}
fn application_name_wide(program: &str) -> Option<Vec<u16>> {
if should_use_path_search(program) {
None
} else {
Some(wide(program))
}
}
fn should_use_path_search(program: &str) -> bool {
!program.contains(['\\', '/', ':'])
}
fn quote_command_line<'a, I>(values: I) -> String
where
I: IntoIterator<Item = &'a str>,
{
let mut rendered = String::new();
for value in values {
if !rendered.is_empty() {
rendered.push(' ');
}
rendered.push_str(&quote_windows_arg(value));
}
rendered
}
fn quote_windows_arg(value: &str) -> String {
if value.is_empty() {
return "\"\"".to_string();
}
if !value.contains([' ', '\t', '"']) && !value.ends_with('\\') {
return value.to_string();
}
let mut rendered = String::with_capacity(value.len() + 2);
rendered.push('"');
let mut backslashes = 0;
for character in value.chars() {
if character == '\\' {
backslashes += 1;
continue;
}
if character == '"' {
rendered.push_str(&"\\".repeat(backslashes * 2 + 1));
rendered.push('"');
backslashes = 0;
continue;
}
if backslashes > 0 {
rendered.push_str(&"\\".repeat(backslashes));
backslashes = 0;
}
rendered.push(character);
}
if backslashes > 0 {
rendered.push_str(&"\\".repeat(backslashes * 2));
}
rendered.push('"');
rendered
}
fn build_environment_block() -> Vec<u16> {
let mut pairs = std::env::vars_os()
.map(|(name, value)| {
let mut rendered = String::new();
rendered.push_str(&name.to_string_lossy());
rendered.push('=');
rendered.push_str(&value.to_string_lossy());
rendered
})
.collect::<Vec<_>>();
pairs.sort_unstable();
let mut buffer = Vec::new();
for pair in pairs {
buffer.extend(OsStr::new(&pair).encode_wide());
buffer.push(0);
}
buffer.push(0);
buffer
}
fn process_id_from_handle(process: HANDLE) -> Result<u32, WindowsSupportError> {
#[allow(unsafe_code)]
let pid = unsafe { windows_sys::Win32::System::Threading::GetProcessId(process) };
if pid == 0 {
Err(last_error("GetProcessId"))
} else {
Ok(pid)
}
}
fn wait_for_exit_code(process: HANDLE) -> Result<i32, WindowsSupportError> {
#[allow(unsafe_code)]
let waited = unsafe { WaitForSingleObject(process, u32::MAX) };
if waited != WAIT_OBJECT_0 {
return if waited == WAIT_FAILED {
Err(last_error("WaitForSingleObject"))
} else {
Err(WindowsSupportError::Process(format!(
"unexpected wait result: {waited}"
)))
};
}
let mut code = 0_u32;
#[allow(unsafe_code)]
let ok = unsafe { GetExitCodeProcess(process, &raw mut code) };
if ok == FALSE {
return Err(last_error("GetExitCodeProcess"));
}
Ok(i32::try_from(code).unwrap_or(i32::MAX))
}
fn get_token_info<T: Copy>(
token: HANDLE,
class: windows_sys::Win32::Security::TOKEN_INFORMATION_CLASS,
) -> Result<T, WindowsSupportError> {
let bytes = get_token_info_bytes(token, class)?;
#[allow(unsafe_code)]
let value = unsafe { *(bytes.as_ptr().cast::<T>()) };
Ok(value)
}
fn get_token_info_bytes(
token: HANDLE,
class: windows_sys::Win32::Security::TOKEN_INFORMATION_CLASS,
) -> Result<Vec<u8>, WindowsSupportError> {
let mut needed = 0_u32;
#[allow(unsafe_code)]
let _ = unsafe { GetTokenInformation(token, class, null_mut(), 0, &raw mut needed) };
if needed == 0 {
return Err(last_error("GetTokenInformation"));
}
let mut bytes = vec![0_u8; usize::try_from(needed).expect("token info length fits usize")];
#[allow(unsafe_code)]
let ok = unsafe {
GetTokenInformation(
token,
class,
bytes.as_mut_ptr().cast::<c_void>(),
needed,
&raw mut needed,
)
};
if ok == FALSE {
return Err(last_error("GetTokenInformation"));
}
Ok(bytes)
}
fn wide(value: &str) -> Vec<u16> {
let mut wide = OsStr::new(value).encode_wide().collect::<Vec<_>>();
wide.push(0);
wide
}
fn wide_from_os(value: &OsStr) -> Vec<u16> {
let mut wide = value.encode_wide().collect::<Vec<_>>();
wide.push(0);
wide
}
fn wide_to_string(value: &[u16]) -> String {
let terminator = value
.iter()
.position(|item| *item == 0)
.unwrap_or(value.len());
String::from_utf16_lossy(&value[..terminator])
}
fn last_error(action: &'static str) -> WindowsSupportError {
#[allow(unsafe_code)]
let code = unsafe { GetLastError() };
WindowsSupportError::WindowsApi { action, code }
}
#[cfg(test)]
mod tests {
use super::{
CREATE_NEW_CONSOLE, CREATE_NO_WINDOW, ERROR_ACCESS_DENIED, ProcessPriority,
ShowWindowMode, TokenIntegrity, admin_launch_requires_duplicate_token,
application_name_wide, build_environment_block, build_process_command_line,
build_startup_info, create_relay_output_file, creation_flags, integrity_rid,
interactive_desktop, launch_request, mandatory_label_sid, priority_flag,
quote_command_line, quote_windows_arg, resolve_target_session_id,
should_retry_trustedinstaller_token_open, should_use_path_search, show_window_value,
wide, wide_from_os, wide_to_string,
};
use std::ffi::OsStr;
use std::fs;
use crate::WindowsSupportError;
use crate::sudo::{LaunchIdentity, LaunchRequest, PrivilegeMode};
#[test]
fn windows_arg_quoting_handles_spaces_quotes_and_trailing_backslashes() {
assert_eq!(quote_windows_arg("plain"), "plain");
assert_eq!(quote_windows_arg("two words"), "\"two words\"");
assert_eq!(quote_windows_arg("a\"b"), "\"a\\\"b\"");
assert_eq!(quote_windows_arg("c:\\temp\\"), "\"c:\\temp\\\\\"");
}
#[test]
fn command_line_builder_includes_program_first() {
let command = build_process_command_line("tool.exe", &["two words".to_string()]);
assert_eq!(command, "tool.exe \"two words\"");
assert_eq!(
quote_command_line(["", "tab\tvalue", "simple"]),
"\"\" \"tab\tvalue\" simple"
);
}
#[test]
fn bare_commands_use_windows_path_search() {
assert!(should_use_path_search("cmd"));
assert!(should_use_path_search("pwsh.exe"));
assert!(application_name_wide("cmd").is_none());
assert!(application_name_wide("pwsh.exe").is_none());
assert!(application_name_wide("C:\\Windows\\System32\\cmd.exe").is_some());
assert!(application_name_wide(".\\tool.exe").is_some());
}
#[test]
fn enum_mappings_cover_priority_and_show_window() {
assert!(priority_flag(ProcessPriority::Idle) > 0);
assert!(priority_flag(ProcessPriority::BelowNormal) > 0);
assert!(priority_flag(ProcessPriority::Normal) > 0);
assert!(priority_flag(ProcessPriority::AboveNormal) > 0);
assert!(priority_flag(ProcessPriority::High) > 0);
assert!(priority_flag(ProcessPriority::Realtime) > 0);
assert_eq!(show_window_value(ShowWindowMode::Default), 10);
assert_eq!(show_window_value(ShowWindowMode::Hidden), 0);
assert_eq!(show_window_value(ShowWindowMode::Normal), 1);
assert_eq!(show_window_value(ShowWindowMode::Minimized), 2);
assert_eq!(show_window_value(ShowWindowMode::Maximized), 3);
}
#[test]
fn token_integrity_debug_shape_is_stable() {
assert_eq!(format!("{:?}", TokenIntegrity::MediumPlus), "MediumPlus");
}
#[test]
fn trustedinstaller_retry_only_handles_access_denied_token_open() {
let retryable = WindowsSupportError::WindowsApi {
action: "OpenProcessToken",
code: ERROR_ACCESS_DENIED,
};
let retryable_process = WindowsSupportError::WindowsApi {
action: "OpenProcess",
code: ERROR_ACCESS_DENIED,
};
assert!(should_retry_trustedinstaller_token_open(&retryable));
assert!(should_retry_trustedinstaller_token_open(&retryable_process));
}
#[test]
fn interactive_launches_use_winsta0_default_desktop() {
let request = LaunchRequest {
program: "cmd.exe".to_string(),
args: Vec::new(),
current_directory: None,
stdout_path: None,
stderr_path: None,
identity: LaunchIdentity::TrustedInstaller,
privileges: PrivilegeMode::Default,
integrity: None,
priority: ProcessPriority::Normal,
show_window: ShowWindowMode::Default,
session: None,
same_console: false,
new_window: true,
wait: false,
};
let desktop = interactive_desktop(&request).expect("interactive desktop");
let startup = build_startup_info(request.show_window, Some(&desktop));
assert_eq!(wide_to_string(&desktop), "winsta0\\default");
assert!(!startup.lpDesktop.is_null());
}
#[test]
fn same_console_launches_use_winsta0_default_desktop() {
let request = LaunchRequest {
program: "cmd.exe".to_string(),
args: Vec::new(),
current_directory: None,
stdout_path: None,
stderr_path: None,
identity: LaunchIdentity::System,
privileges: PrivilegeMode::Default,
integrity: None,
priority: ProcessPriority::Normal,
show_window: ShowWindowMode::Default,
session: None,
same_console: true,
new_window: false,
wait: true,
};
let desktop = interactive_desktop(&request).expect("interactive desktop");
assert_eq!(wide_to_string(&desktop), "winsta0\\default");
}
#[test]
fn hidden_new_window_does_not_request_no_window_flag() {
let request = LaunchRequest {
program: "cmd.exe".to_string(),
args: Vec::new(),
current_directory: None,
stdout_path: None,
stderr_path: None,
identity: LaunchIdentity::Admin,
privileges: PrivilegeMode::Default,
integrity: None,
priority: ProcessPriority::Normal,
show_window: ShowWindowMode::Hidden,
session: None,
same_console: false,
new_window: true,
wait: false,
};
let flags = creation_flags(&request);
assert_ne!(flags & CREATE_NEW_CONSOLE, 0);
assert_eq!(flags & CREATE_NO_WINDOW, 0);
}
#[test]
fn explicit_session_wins_session_resolution() {
assert_eq!(resolve_target_session_id(Some(42)), Some(42));
}
#[test]
fn creation_flags_cover_window_and_priority_combinations() {
let mut request = LaunchRequest {
program: "cmd.exe".to_string(),
args: Vec::new(),
current_directory: None,
stdout_path: None,
stderr_path: None,
identity: LaunchIdentity::CurrentProcess,
privileges: PrivilegeMode::Default,
integrity: None,
priority: ProcessPriority::High,
show_window: ShowWindowMode::Hidden,
session: None,
same_console: false,
new_window: false,
wait: false,
};
let hidden_same_window = creation_flags(&request);
assert_ne!(hidden_same_window & CREATE_NO_WINDOW, 0);
assert_eq!(hidden_same_window & CREATE_NEW_CONSOLE, 0);
request.new_window = true;
let hidden_new_window = creation_flags(&request);
assert_ne!(hidden_new_window & CREATE_NEW_CONSOLE, 0);
assert_eq!(hidden_new_window & CREATE_NO_WINDOW, 0);
}
#[test]
fn wide_and_environment_helpers_emit_null_terminated_buffers() {
let wide = wide("Mercury");
assert_eq!(wide_to_string(&wide), "Mercury");
assert_eq!(wide.last(), Some(&0));
let from_os = wide_from_os(OsStr::new("Toolbox"));
assert_eq!(wide_to_string(&from_os), "Toolbox");
assert_eq!(from_os.last(), Some(&0));
let environment = build_environment_block();
assert!(environment.ends_with(&[0, 0]));
}
#[test]
fn relay_output_file_refuses_existing_replacements() {
let path = std::env::temp_dir().join(format!(
"windowsupport-relay-output-{}.txt",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("epoch")
.as_nanos()
));
fs::write(&path, "attacker").expect("replacement");
let error = create_relay_output_file(&path.display().to_string())
.expect_err("existing replacement should be refused");
assert!(
error
.to_string()
.contains("failed to create relay output file")
);
assert_eq!(
fs::read_to_string(&path).expect("replacement remains"),
"attacker"
);
let _ = fs::remove_file(path);
}
#[cfg(windows)]
#[test]
fn current_process_launch_waits_for_child_exit_code() {
let request = LaunchRequest {
program: "cmd.exe".to_string(),
args: vec![
"/D".to_string(),
"/S".to_string(),
"/C".to_string(),
"exit /b 7".to_string(),
],
current_directory: None,
stdout_path: None,
stderr_path: None,
identity: LaunchIdentity::CurrentProcess,
privileges: PrivilegeMode::Default,
integrity: None,
priority: ProcessPriority::Normal,
show_window: ShowWindowMode::Hidden,
session: None,
same_console: false,
new_window: true,
wait: true,
};
let result = launch_request(&request).expect("launch current process");
assert_eq!(result.identity, LaunchIdentity::CurrentProcess);
assert_eq!(result.exit_code, Some(7));
assert!(result.pid.is_some());
}
#[cfg(windows)]
#[test]
fn current_process_launch_can_relay_stdout_to_exclusive_file() {
let path = std::env::temp_dir().join(format!(
"windowsupport-relay-capture-{}.txt",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("epoch")
.as_nanos()
));
let request = LaunchRequest {
program: "cmd.exe".to_string(),
args: vec![
"/D".to_string(),
"/S".to_string(),
"/C".to_string(),
"echo MercuryStdout".to_string(),
],
current_directory: None,
stdout_path: Some(path.display().to_string()),
stderr_path: None,
identity: LaunchIdentity::CurrentProcess,
privileges: PrivilegeMode::Default,
integrity: None,
priority: ProcessPriority::Normal,
show_window: ShowWindowMode::Hidden,
session: None,
same_console: false,
new_window: false,
wait: true,
};
let result = launch_request(&request).expect("launch with stdout relay");
assert_eq!(result.exit_code, Some(0));
assert!(
fs::read_to_string(&path)
.expect("relay output")
.contains("MercuryStdout")
);
let _ = fs::remove_file(path);
}
#[cfg(windows)]
#[test]
fn current_user_launch_without_token_shaping_uses_current_identity_path() {
let status = super::current_token_status().expect("token status");
assert!(
status
.current_user
.as_deref()
.is_some_and(|user| !user.is_empty())
);
let request = LaunchRequest {
program: "cmd.exe".to_string(),
args: vec![
"/D".to_string(),
"/S".to_string(),
"/C".to_string(),
"exit /b 0".to_string(),
],
current_directory: None,
stdout_path: None,
stderr_path: None,
identity: LaunchIdentity::CurrentUser,
privileges: PrivilegeMode::Default,
integrity: None,
priority: ProcessPriority::Normal,
show_window: ShowWindowMode::Hidden,
session: None,
same_console: false,
new_window: true,
wait: true,
};
let result = launch_request(&request).expect("launch current user");
assert_eq!(result.identity, LaunchIdentity::CurrentUser);
assert_eq!(result.exit_code, Some(0));
}
#[test]
fn admin_launch_requires_duplicate_token_for_token_mutations() {
let request = LaunchRequest {
program: "cmd.exe".to_string(),
args: Vec::new(),
current_directory: None,
stdout_path: None,
stderr_path: None,
identity: LaunchIdentity::Admin,
privileges: PrivilegeMode::Default,
integrity: None,
priority: ProcessPriority::Normal,
show_window: ShowWindowMode::Default,
session: None,
same_console: false,
new_window: false,
wait: false,
};
assert!(!admin_launch_requires_duplicate_token(&request));
let mut integrity = request.clone();
integrity.integrity = Some(TokenIntegrity::High);
assert!(admin_launch_requires_duplicate_token(&integrity));
let mut privileges = request.clone();
privileges.privileges = PrivilegeMode::DisableAll;
assert!(admin_launch_requires_duplicate_token(&privileges));
let mut session = request;
session.session = Some(1);
assert!(admin_launch_requires_duplicate_token(&session));
}
#[test]
fn mandatory_label_sid_matches_integrity_authority_shape() {
let rid = integrity_rid(TokenIntegrity::MediumPlus).expect("rid");
let sid = mandatory_label_sid(rid);
assert_eq!(sid[0], 1);
assert_eq!(sid[1], 1);
assert_eq!(sid[2..8], [0, 0, 0, 0, 0, 16]);
assert_eq!(u32::from_le_bytes([sid[8], sid[9], sid[10], sid[11]]), rid);
}
}
}
#[cfg(windows)]
pub use imp::{
attach_parent_console, current_token_status, elevate_current_process, launch_request,
};