use std::collections::HashMap;
use std::ffi::OsString;
use std::fmt::Write;
use std::mem::size_of;
use std::os::windows::ffi::{OsStrExt, OsStringExt};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Mutex, OnceLock};
use std::{env, iter, ptr};
use rand::RngExt;
use windows::Win32::Foundation::{
CloseHandle, ERROR_ACCESS_DENIED, ERROR_INVALID_PARAMETER, FILETIME, GetLastError, HANDLE,
INVALID_HANDLE_VALUE, WAIT_OBJECT_0, WAIT_TIMEOUT,
};
use windows::Win32::Storage::FileSystem::SearchPathW;
use windows::Win32::System::Console::HPCON;
use windows::Win32::System::JobObjects::{
CreateJobObjectW, IsProcessInJob, JOB_OBJECT_LIMIT, JOB_OBJECT_LIMIT_BREAKAWAY_OK,
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
JobObjectExtendedLimitInformation, QueryInformationJobObject, SetInformationJobObject,
};
use windows::Win32::System::Threading::{
CREATE_BREAKAWAY_FROM_JOB, CREATE_UNICODE_ENVIRONMENT, CreateProcessW, DETACHED_PROCESS,
DeleteProcThreadAttributeList, EXTENDED_STARTUPINFO_PRESENT, GetCurrentProcess,
GetCurrentProcessId, GetExitCodeProcess, GetProcessId, GetProcessTimes, INFINITE,
InitializeProcThreadAttributeList, LPPROC_THREAD_ATTRIBUTE_LIST, OpenProcess,
PROC_THREAD_ATTRIBUTE_JOB_LIST, PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, PROCESS_ACCESS_RIGHTS,
PROCESS_INFORMATION, PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_SYNCHRONIZE, PROCESS_TERMINATE,
STARTF_USESTDHANDLES, STARTUPINFOEXW, STARTUPINFOW, TerminateProcess,
UpdateProcThreadAttribute, WaitForSingleObject,
};
use windows::core::{BOOL, PCWSTR, PWSTR};
use crate::constants::TERMINATE_TIMEOUT;
use crate::durability::Durability;
use crate::pal::error::{PalError, PalErrorKind};
use crate::pal::ids::{AppId, JobId};
use crate::pal::processes::{
AppSpawn, Breakaway, ProcessLiveness, Processes, SupervisorSpawn, resolve_command_path,
windows_command_line,
};
use crate::pal::pseudoconsole::hpcon_for;
use crate::pal::raw_handle::RawHandle;
use crate::session_record::ProcessIdentity;
#[derive(Debug, Default)]
pub(crate) struct BuildTargetProcesses;
struct HandleTable {
jobs: HashMap<u64, Vec<RawHandle>>,
apps: HashMap<u64, RawHandle>,
}
fn table() -> &'static Mutex<HandleTable> {
static TABLE: OnceLock<Mutex<HandleTable>> = OnceLock::new();
TABLE.get_or_init(|| {
Mutex::new(HandleTable {
jobs: HashMap::new(),
apps: HashMap::new(),
})
})
}
fn next_id() -> u64 {
static NEXT: AtomicU64 = AtomicU64::new(1);
NEXT.fetch_add(1, Ordering::Relaxed)
}
const APP_SPAWN_ATTRIBUTE_COUNT: u32 = 2;
fn filetime_u64(time: FILETIME) -> u64 {
let high = u64::from(time.dwHighDateTime);
let low = u64::from(time.dwLowDateTime);
high.checked_shl(32)
.expect("shifting a u32 into the high half of a u64 cannot overflow")
| low
}
fn identity_of(handle: HANDLE) -> Result<ProcessIdentity, PalError> {
let pid = {
unsafe { GetProcessId(handle) }
};
if pid == 0 {
return Err(PalError::new(PalErrorKind::InspectFailed));
}
let mut creation = FILETIME::default();
let mut exit = FILETIME::default();
let mut kernel = FILETIME::default();
let mut user = FILETIME::default();
unsafe {
GetProcessTimes(
handle,
&raw mut creation,
&raw mut exit,
&raw mut kernel,
&raw mut user,
)
}
.map_err(|_error| PalError::new(PalErrorKind::InspectFailed))?;
Ok(ProcessIdentity {
pid,
creation_time: filetime_u64(creation),
})
}
fn open_verified(
identity: &ProcessIdentity,
access: PROCESS_ACCESS_RIGHTS,
) -> Result<HANDLE, PalError> {
let opened = unsafe { OpenProcess(access, false, identity.pid) };
let Ok(handle) = opened else {
let err = unsafe { GetLastError() };
if err == ERROR_INVALID_PARAMETER {
return Err(PalError::new(PalErrorKind::NotFound));
}
return Err(PalError::new(PalErrorKind::InspectFailed));
};
match identity_of(handle) {
Ok(actual) if actual.creation_time == identity.creation_time => Ok(handle),
Ok(_) => {
close(handle);
Err(PalError::new(PalErrorKind::NotFound))
}
Err(error) => {
close(handle);
Err(error)
}
}
}
fn close(handle: HANDLE) {
_ = unsafe { CloseHandle(handle) };
}
fn search_executable(exe: &Path) -> PathBuf {
if exe.components().count() != 1 {
return exe.to_path_buf();
}
let name = wide(&exe.to_string_lossy());
let extension = wide(".exe");
let mut buf = vec![0_u16; 260];
for _attempt in 0..2_u8 {
let len = unsafe {
SearchPathW(
PCWSTR::null(),
PCWSTR(name.as_ptr()),
PCWSTR(extension.as_ptr()),
Some(&mut buf),
None,
)
} as usize;
if len == 0 {
break;
}
if len < buf.len() {
return buf.get(..len).map_or_else(
|| exe.to_path_buf(),
|found| PathBuf::from(OsString::from_wide(found)),
);
}
buf = vec![0_u16; len];
}
exe.to_path_buf()
}
fn wide(s: &str) -> Vec<u16> {
OsString::from(s)
.encode_wide()
.chain(iter::once(0))
.collect()
}
fn process_in_a_job(process: HANDLE) -> Result<bool, PalError> {
let mut in_job = BOOL::default();
unsafe { IsProcessInJob(process, None, &raw mut in_job) }
.map_err(|_error| PalError::new(PalErrorKind::InspectFailed))?;
Ok(in_job.as_bool())
}
enum JobLimits {
None,
Unknown,
Known(JOB_OBJECT_LIMIT),
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn immediate_job_limits() -> JobLimits {
let current = unsafe { GetCurrentProcess() };
match process_in_a_job(current) {
Ok(false) => return JobLimits::None,
Ok(true) => {}
Err(_error) => return JobLimits::Unknown,
}
let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
let queried = unsafe {
QueryInformationJobObject(
None,
JobObjectExtendedLimitInformation,
ptr::from_mut(&mut info).cast(),
u32::try_from(size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>())
.expect("job info size fits in u32"),
None,
)
};
if queried.is_err() {
return JobLimits::Unknown;
}
JobLimits::Known(info.BasicLimitInformation.LimitFlags)
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn breakaway_forbidden() -> bool {
match immediate_job_limits() {
JobLimits::None => false,
JobLimits::Unknown => true,
JobLimits::Known(flags) => (flags & JOB_OBJECT_LIMIT_BREAKAWAY_OK).0 == 0,
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg_attr(test, mutants::skip)]
impl BuildTargetProcesses {
pub(crate) fn create_job(breakaway: Breakaway) -> Result<JobId, PalError> {
Self::create_job_chain(&[breakaway])
}
pub(crate) fn create_job_chain(policies: &[Breakaway]) -> Result<JobId, PalError> {
let mut handles = Vec::with_capacity(policies.len());
for breakaway in policies {
match create_job_handle(*breakaway) {
Ok(handle) => handles.push(RawHandle::from_handle(handle)),
Err(error) => {
for handle in handles {
close(handle.as_handle());
}
return Err(error);
}
}
}
let id = next_id();
table()
.lock()
.expect("handle table")
.jobs
.insert(id, handles);
Ok(JobId(id))
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn create_job_handle(breakaway: Breakaway) -> Result<HANDLE, PalError> {
let handle = unsafe { CreateJobObjectW(None, PCWSTR::null()) }
.map_err(|_error| PalError::new(PalErrorKind::Other))?;
let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
info.BasicLimitInformation.LimitFlags = match breakaway {
Breakaway::Permitted => JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_BREAKAWAY_OK,
#[cfg(feature = "private-test-util")]
Breakaway::Forbidden => JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
};
let configured = unsafe {
SetInformationJobObject(
handle,
JobObjectExtendedLimitInformation,
ptr::from_mut(&mut info).cast(),
u32::try_from(size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>())
.expect("job info size fits in u32"),
)
};
if configured.is_err() {
unsafe {
_ = CloseHandle(handle);
}
return Err(PalError::new(PalErrorKind::Other));
}
Ok(handle)
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg_attr(test, mutants::skip)]
impl Processes for BuildTargetProcesses {
fn current_exe(&self) -> Result<PathBuf, PalError> {
env::current_exe().map_err(PalError::from_io)
}
fn spawn_supervisor(&self, request: &SupervisorSpawn) -> Result<ProcessIdentity, PalError> {
let mut cmd_wide = wide(&windows_command_line(
&request.exe.to_string_lossy(),
&request.args,
));
let mut exe_wide = wide(&request.exe.to_string_lossy());
let si = STARTUPINFOW {
cb: u32::try_from(size_of::<STARTUPINFOW>()).expect("STARTUPINFOW fits in u32"),
..Default::default()
};
let mut pi = PROCESS_INFORMATION::default();
let flags = CREATE_BREAKAWAY_FROM_JOB | DETACHED_PROCESS | CREATE_UNICODE_ENVIRONMENT;
let created = unsafe {
CreateProcessW(
PCWSTR(exe_wide.as_mut_ptr()),
Some(PWSTR(cmd_wide.as_mut_ptr())),
None,
None,
false,
flags,
None,
None,
&raw const si,
&raw mut pi,
)
};
created.map_err(|error| {
if error.code() == ERROR_ACCESS_DENIED.to_hresult() && breakaway_forbidden() {
PalError::new(PalErrorKind::BreakawayDenied)
} else {
PalError::new(PalErrorKind::Other)
}
})?;
close(pi.hThread);
let identity = identity_of(pi.hProcess);
close(pi.hProcess);
identity
}
fn durability(&self) -> Durability {
match immediate_job_limits() {
JobLimits::None => Durability::Durable,
JobLimits::Unknown => Durability::TiedToLauncher,
JobLimits::Known(flags) => {
if (flags & JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE).0 == 0 {
Durability::Durable
} else {
Durability::TiedToLauncher
}
}
}
}
fn probe(&self, identity: &ProcessIdentity) -> ProcessLiveness {
let access = PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE;
let handle = match open_verified(identity, access) {
Ok(handle) => handle,
Err(error) if error.kind() == PalErrorKind::NotFound => return ProcessLiveness::Dead,
Err(_) => return ProcessLiveness::InspectFailed,
};
let wait = unsafe { WaitForSingleObject(handle, 0) };
close(handle);
if wait == WAIT_TIMEOUT {
ProcessLiveness::Live
} else if wait == WAIT_OBJECT_0 {
ProcessLiveness::Dead
} else {
ProcessLiveness::InspectFailed
}
}
fn terminate(&self, identity: &ProcessIdentity) -> Result<(), PalError> {
let access = PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE | PROCESS_TERMINATE;
let handle = open_verified(identity, access).map_err(|error| {
if error.kind() == PalErrorKind::NotFound {
PalError::new(PalErrorKind::NotFound)
} else {
PalError::new(PalErrorKind::InspectFailed)
}
})?;
let result = unsafe { TerminateProcess(handle, 1) };
let settled = if result.is_ok() {
let wait = unsafe {
WaitForSingleObject(
handle,
u32::try_from(TERMINATE_TIMEOUT.as_millis())
.expect("terminate timeout fits in u32 milliseconds"),
)
};
wait == WAIT_OBJECT_0
} else {
false
};
close(handle);
result.map_err(|_error| PalError::new(PalErrorKind::Other))?;
if settled {
Ok(())
} else {
Err(PalError::new(PalErrorKind::Other))
}
}
fn create_lifetime_job(&self) -> Result<JobId, PalError> {
Self::create_job(Breakaway::Permitted)
}
fn close_job(&self, job: JobId) {
if let Some(handles) = table().lock().expect("handle table").jobs.remove(&job.0) {
for handle in handles.into_iter().rev() {
close(handle.as_handle());
}
}
}
fn spawn_app(&self, request: &AppSpawn) -> Result<AppId, PalError> {
let hpcon = hpcon_for(request.pty).ok_or_else(|| PalError::new(PalErrorKind::NotFound))?;
let exe = search_executable(&resolve_command_path(
request
.command
.first()
.ok_or_else(|| PalError::new(PalErrorKind::Other))?,
&request.launch_directory,
));
let rest = request.command.get(1..).unwrap_or(&[]);
let mut cmd_wide = wide(&windows_command_line(&exe.to_string_lossy(), rest));
let mut exe_wide = wide(&exe.to_string_lossy());
let mut dir_wide = wide(&request.launch_directory.to_string_lossy());
let mut job_list = table()
.lock()
.expect("handle table")
.jobs
.get(&request.job.0)
.ok_or_else(|| PalError::new(PalErrorKind::NotFound))?
.iter()
.map(|handle| handle.as_handle())
.collect::<Vec<_>>();
let attribute_count = APP_SPAWN_ATTRIBUTE_COUNT;
let mut attr_size: usize = 0;
_ = unsafe {
InitializeProcThreadAttributeList(None, attribute_count, None, &raw mut attr_size)
};
let mut attr_buf = vec![0_u8; attr_size];
let attr_list = LPPROC_THREAD_ATTRIBUTE_LIST(attr_buf.as_mut_ptr().cast());
unsafe {
InitializeProcThreadAttributeList(
Some(attr_list),
attribute_count,
None,
&raw mut attr_size,
)
}
.map_err(|_error| PalError::new(PalErrorKind::Other))?;
let hpcon_value = hpcon.0 as *const core::ffi::c_void;
unsafe {
UpdateProcThreadAttribute(
attr_list,
0,
usize::try_from(PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE)
.expect("PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE fits in usize"),
Some(hpcon_value),
size_of::<HPCON>(),
None,
None,
)
}
.map_err(|_error| PalError::new(PalErrorKind::Other))?;
unsafe {
UpdateProcThreadAttribute(
attr_list,
0,
usize::try_from(PROC_THREAD_ATTRIBUTE_JOB_LIST)
.expect("PROC_THREAD_ATTRIBUTE_JOB_LIST fits in usize"),
Some(job_list.as_mut_ptr().cast()),
size_of::<HANDLE>()
.checked_mul(job_list.len())
.expect("job handle list size fits in usize"),
None,
None,
)
}
.map_err(|_error| PalError::new(PalErrorKind::Other))?;
let mut si = STARTUPINFOEXW::default();
si.StartupInfo.cb =
u32::try_from(size_of::<STARTUPINFOEXW>()).expect("STARTUPINFOEXW fits in u32");
si.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
si.StartupInfo.hStdInput = INVALID_HANDLE_VALUE;
si.StartupInfo.hStdOutput = INVALID_HANDLE_VALUE;
si.StartupInfo.hStdError = INVALID_HANDLE_VALUE;
si.lpAttributeList = attr_list;
let mut pi = PROCESS_INFORMATION::default();
let flags = EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT;
let created = unsafe {
CreateProcessW(
PCWSTR(exe_wide.as_mut_ptr()),
Some(PWSTR(cmd_wide.as_mut_ptr())),
None,
None,
false,
flags,
None,
PCWSTR(dir_wide.as_mut_ptr()),
ptr::from_ref(&si.StartupInfo),
&raw mut pi,
)
};
unsafe {
DeleteProcThreadAttributeList(attr_list);
}
created.map_err(|_error| PalError::new(PalErrorKind::Other))?;
close(pi.hThread);
let id = next_id();
table()
.lock()
.expect("handle table")
.apps
.insert(id, RawHandle::from_handle(pi.hProcess));
Ok(AppId(id))
}
fn wait_app(&self, app: AppId) -> Result<i32, PalError> {
let handle = table()
.lock()
.expect("handle table")
.apps
.get(&app.0)
.copied()
.ok_or_else(|| PalError::new(PalErrorKind::NotFound))?
.as_handle();
let wait = unsafe { WaitForSingleObject(handle, INFINITE) };
if wait != WAIT_OBJECT_0 {
return Err(PalError::new(PalErrorKind::Other));
}
let mut code = 0_u32;
unsafe { GetExitCodeProcess(handle, &raw mut code) }
.map_err(|_error| PalError::new(PalErrorKind::Other))?;
Ok(code.cast_signed())
}
fn current_identity(&self) -> Result<ProcessIdentity, PalError> {
let handle = unsafe { GetCurrentProcess() };
let mut identity = identity_of(handle)?;
identity.pid = {
unsafe { GetCurrentProcessId() }
};
Ok(identity)
}
fn random_nonce(&self) -> String {
let bytes: [u8; 16] = rand::rng().random();
let mut nonce = String::with_capacity(32);
for byte in bytes {
write!(nonce, "{byte:02x}").expect("writing to String cannot fail");
}
nonce
}
}