#![cfg(windows)]
#![allow(dead_code)]
use std::ffi::{c_void, OsStr, OsString};
use std::os::windows::ffi::OsStrExt;
use windows::core::{Error as WinError, PCWSTR, PWSTR};
use windows::Win32::Foundation::{
CloseHandle, LocalFree, ERROR_INSUFFICIENT_BUFFER, ERROR_INVALID_PARAMETER,
ERROR_NOT_ENOUGH_MEMORY, GENERIC_EXECUTE, GENERIC_READ, GENERIC_WRITE, HANDLE, HLOCAL,
WAIT_OBJECT_0, WAIT_TIMEOUT, WIN32_ERROR,
};
use windows::Win32::Security::Authorization::{
GetNamedSecurityInfoW, SetEntriesInAclW, SetNamedSecurityInfoW, EXPLICIT_ACCESS_W,
GRANT_ACCESS, NO_MULTIPLE_TRUSTEE, SE_FILE_OBJECT, TRUSTEE_IS_SID, TRUSTEE_IS_UNKNOWN,
TRUSTEE_W,
};
use windows::Win32::Security::Isolation::{
CreateAppContainerProfile, DeriveAppContainerSidFromAppContainerName,
};
use windows::Win32::Security::{
CopySid, DeleteAce, EqualSid, FreeSid, GetAce, GetLengthSid, ACCESS_ALLOWED_ACE, ACE_FLAGS,
ACE_HEADER, ACL, DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, PSID, SECURITY_CAPABILITIES,
};
use windows::Win32::System::JobObjects::{
AssignProcessToJobObject, CreateJobObjectW, JobObjectExtendedLimitInformation,
SetInformationJobObject, TerminateJobObject, JOBOBJECT_BASIC_LIMIT_INFORMATION,
JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JOB_OBJECT_LIMIT_ACTIVE_PROCESS,
JOB_OBJECT_LIMIT_JOB_MEMORY, JOB_OBJECT_LIMIT_JOB_TIME, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
};
use windows::Win32::System::Memory::{LocalAlloc, LPTR};
use windows::Win32::System::SystemServices::ACCESS_ALLOWED_ACE_TYPE;
use windows::Win32::System::Threading::{
CreateProcessW, DeleteProcThreadAttributeList, GetExitCodeProcess,
InitializeProcThreadAttributeList, ResumeThread, TerminateProcess, UpdateProcThreadAttribute,
WaitForSingleObject, CREATE_SUSPENDED, CREATE_UNICODE_ENVIRONMENT,
EXTENDED_STARTUPINFO_PRESENT, INFINITE, LPPROC_THREAD_ATTRIBUTE_LIST, PROCESS_INFORMATION,
PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES, STARTUPINFOEXW, STARTUPINFOW,
};
use tirith_core::capsule::windows::{
command_line_wide_for, AclAccess, AclGrant, WindowsLaunchPlan,
};
use tirith_core::capsule::{CapsuleSpec, EnvironmentPolicy};
#[derive(Debug)]
pub enum WindowsLaunchError {
AppContainer(String, WinError),
Acl(String, WIN32_ERROR),
AttributeList(String, WinError),
CreateProcess(String, WinError),
JobObject(String, WinError),
Wait(String),
Encoding(String),
}
impl std::fmt::Display for WindowsLaunchError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
WindowsLaunchError::AppContainer(m, e) => write!(f, "appcontainer: {m}: {e}"),
WindowsLaunchError::Acl(m, e) => write!(f, "acl: {m}: {e:?}"),
WindowsLaunchError::AttributeList(m, e) => write!(f, "attribute-list: {m}: {e}"),
WindowsLaunchError::CreateProcess(m, e) => write!(f, "create-process: {m}: {e}"),
WindowsLaunchError::JobObject(m, e) => write!(f, "job-object: {m}: {e}"),
WindowsLaunchError::Wait(m) => write!(f, "wait: {m}"),
WindowsLaunchError::Encoding(m) => write!(f, "encoding: {m}"),
}
}
}
impl std::error::Error for WindowsLaunchError {}
pub struct ContainedChild {
job: OwnedHandle,
process: OwnedHandle,
thread: OwnedHandle,
acl_guards: Vec<AclGuard>,
wall_clock_seconds: Option<u64>,
}
impl ContainedChild {
pub fn process_handle(&self) -> HANDLE {
self.process.0
}
fn job_handle(&self) -> HANDLE {
self.job.0
}
fn wait_timeout_ms(&self) -> u32 {
self.wall_clock_seconds
.map(|s| s.saturating_mul(1000).min(u32::MAX as u64 - 1) as u32)
.unwrap_or(INFINITE)
}
pub fn finish(&mut self) -> Result<(), WindowsLaunchError> {
let mut first_err = None;
for guard in &mut self.acl_guards {
if let Err(e) = guard.revert_now() {
if first_err.is_none() {
first_err = Some(e);
}
}
}
match first_err {
Some(e) => Err(e),
None => Ok(()),
}
}
}
pub fn launch_contained(
spec: &CapsuleSpec,
program: &str,
args: &[String],
) -> Result<ContainedChild, WindowsLaunchError> {
let args_os: Vec<OsString> = args.iter().map(OsString::from).collect();
launch_contained_os(spec, OsStr::new(program), &args_os, None)
}
pub fn launch_contained_os(
spec: &CapsuleSpec,
program: &OsStr,
args: &[OsString],
cwd: Option<&std::path::Path>,
) -> Result<ContainedChild, WindowsLaunchError> {
let plan = tirith_core::capsule::windows::windows_launch_plan_os(spec, program, args)
.map_err(|e| WindowsLaunchError::Encoding(e.to_string()))?;
apply_plan(
&plan,
&spec.environment,
spec.resources.wall_clock_seconds,
cwd,
)
}
pub fn wait_for(child: &ContainedChild) -> Result<i32, WindowsLaunchError> {
let handle = child.process_handle();
let timeout_ms = child.wait_timeout_ms();
let wait = unsafe { WaitForSingleObject(handle, timeout_ms) };
if wait == WAIT_TIMEOUT {
let _ = unsafe { TerminateJobObject(child.job_handle(), 137) };
return Err(WindowsLaunchError::Wait(format!(
"contained child exceeded its wall-clock deadline ({timeout_ms} ms); \
the Job was terminated",
)));
}
if wait != WAIT_OBJECT_0 {
return Err(WindowsLaunchError::Wait(format!(
"WaitForSingleObject did not report the child as exited (returned {:#x}); \
the child may still be running, refusing to synthesize an exit code",
wait.0
)));
}
let mut code: u32 = 0;
unsafe { GetExitCodeProcess(handle, &mut code) }.map_err(|e| {
WindowsLaunchError::CreateProcess("GetExitCodeProcess failed".to_string(), e)
})?;
Ok((code & 0x7fff_ffff) as i32)
}
fn apply_plan(
plan: &WindowsLaunchPlan,
env: &EnvironmentPolicy,
wall_clock_seconds: Option<u64>,
cwd: Option<&std::path::Path>,
) -> Result<ContainedChild, WindowsLaunchError> {
let container_sid = create_or_open_appcontainer(plan)?;
if let Some(home) = &plan.temp_home {
std::fs::create_dir_all(home).map_err(|e| {
WindowsLaunchError::Encoding(format!(
"cannot create capsule temporary home {}: {e}",
home.display()
))
})?;
}
let mut acl_guards = Vec::with_capacity(plan.acl_grants.len());
for grant in &plan.acl_grants {
match apply_acl_grant(grant, container_sid.psid()) {
Ok(guard) => acl_guards.push(guard),
Err(e) => {
revert_all(&mut acl_guards);
return Err(e);
}
}
}
let mut caps = SECURITY_CAPABILITIES {
AppContainerSid: container_sid.psid(),
Capabilities: std::ptr::null_mut(),
CapabilityCount: 0,
..Default::default()
};
let mut attr_list = match ProcThreadAttributeList::with_security_capabilities(&mut caps) {
Ok(l) => l,
Err(e) => {
revert_all(&mut acl_guards);
return Err(e);
}
};
let launched = match create_process(plan, env, &mut attr_list, cwd) {
Ok(l) => l,
Err(e) => {
revert_all(&mut acl_guards);
return Err(e);
}
};
let job = match create_and_assign_job(plan, launched.process.0) {
Ok(j) => j,
Err(e) => {
terminate_quietly(launched.process.0);
revert_all(&mut acl_guards);
return Err(e);
}
};
let resume_rc = unsafe { ResumeThread(launched.thread.0) };
if resume_rc == u32::MAX {
let err = WinError::from_thread();
terminate_quietly(launched.process.0);
drop(job);
revert_all(&mut acl_guards);
return Err(WindowsLaunchError::CreateProcess(
"ResumeThread failed".to_string(),
err,
));
}
Ok(ContainedChild {
job,
process: launched.process,
thread: launched.thread,
acl_guards,
wall_clock_seconds,
})
}
struct OwnedSid(PSID);
impl OwnedSid {
fn psid(&self) -> PSID {
self.0
}
}
impl Drop for OwnedSid {
fn drop(&mut self) {
if !self.0 .0.is_null() {
unsafe {
let _ = FreeSid(self.0);
}
self.0 = PSID(std::ptr::null_mut());
}
}
}
fn create_or_open_appcontainer(plan: &WindowsLaunchPlan) -> Result<OwnedSid, WindowsLaunchError> {
let name = wide_nul(&plan.profile.name)
.map_err(|_| WindowsLaunchError::Encoding("appcontainer name has NUL".to_string()))?;
let display = wide_nul(&plan.profile.display_name)
.map_err(|_| WindowsLaunchError::Encoding("display name has NUL".to_string()))?;
let created = unsafe {
CreateAppContainerProfile(
PCWSTR(name.as_ptr()),
PCWSTR(display.as_ptr()),
PCWSTR(display.as_ptr()),
None,
)
};
match created {
Ok(psid) => Ok(OwnedSid(psid)),
Err(_) => {
let derived =
unsafe { DeriveAppContainerSidFromAppContainerName(PCWSTR(name.as_ptr())) };
match derived {
Ok(psid) => Ok(OwnedSid(psid)),
Err(e) => Err(WindowsLaunchError::AppContainer(
"create and derive both failed".to_string(),
e,
)),
}
}
}
}
struct AclGuard {
path_wide: Vec<u16>,
container_sid: PSID,
reverted: bool,
}
impl AclGuard {
fn revert_now(&mut self) -> Result<(), WindowsLaunchError> {
if self.reverted {
return Ok(());
}
let mut current_dacl: *mut ACL = std::ptr::null_mut();
let mut sd = PSECURITY_DESCRIPTOR::default();
let get_rc = unsafe {
GetNamedSecurityInfoW(
PCWSTR(self.path_wide.as_ptr()),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION,
None,
None,
Some(&mut current_dacl as *mut *mut ACL),
None,
&mut sd,
)
};
if get_rc.is_err() {
return Err(WindowsLaunchError::Acl(
"read current DACL for revert".to_string(),
get_rc,
));
}
let result = (|| -> Result<(), WindowsLaunchError> {
if current_dacl.is_null() {
return Ok(());
}
let ace_count = unsafe { (*current_dacl).AceCount } as u32;
for i in (0..ace_count).rev() {
let mut pace: *mut core::ffi::c_void = std::ptr::null_mut();
if unsafe { GetAce(current_dacl, i, &mut pace) }.is_err() || pace.is_null() {
continue;
}
let header = unsafe { &*(pace as *const ACE_HEADER) };
if header.AceType != ACCESS_ALLOWED_ACE_TYPE as u8 {
continue;
}
let ace = pace as *mut ACCESS_ALLOWED_ACE;
let ace_sid =
PSID(unsafe { std::ptr::addr_of_mut!((*ace).SidStart) }
as *mut core::ffi::c_void);
let is_ours = unsafe { EqualSid(ace_sid, self.container_sid) }.is_ok();
if is_ours {
let _ = unsafe { DeleteAce(current_dacl, i) };
}
}
let set_rc = unsafe {
SetNamedSecurityInfoW(
PCWSTR(self.path_wide.as_ptr()),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION,
None,
None,
Some(current_dacl as *const ACL),
None,
)
};
if set_rc.is_err() {
return Err(WindowsLaunchError::Acl(
"remove capsule ACE from DACL".to_string(),
set_rc,
));
}
Ok(())
})();
unsafe {
let _ = LocalFree(Some(HLOCAL(sd.0)));
}
result?;
self.reverted = true;
Ok(())
}
}
impl Drop for AclGuard {
fn drop(&mut self) {
let _ = self.revert_now();
if !self.container_sid.0.is_null() {
unsafe {
let _ = FreeSid(self.container_sid);
}
self.container_sid = PSID(std::ptr::null_mut());
}
}
}
fn revert_all(guards: &mut [AclGuard]) {
for g in guards.iter_mut() {
let _ = g.revert_now();
}
}
fn access_mask(access: AclAccess) -> u32 {
match access {
AclAccess::ReadExecute => (GENERIC_READ | GENERIC_EXECUTE).0,
AclAccess::Modify => (GENERIC_READ | GENERIC_WRITE | GENERIC_EXECUTE).0,
}
}
fn apply_acl_grant(grant: &AclGrant, container_sid: PSID) -> Result<AclGuard, WindowsLaunchError> {
let path_str = grant
.path
.to_str()
.ok_or_else(|| WindowsLaunchError::Encoding(format!("non-UTF-8 path: {:?}", grant.path)))?;
let path_wide =
wide_nul(path_str).map_err(|_| WindowsLaunchError::Encoding("path has NUL".to_string()))?;
let mut existing_dacl: *mut ACL = std::ptr::null_mut();
let mut sd = PSECURITY_DESCRIPTOR::default();
let get_rc = unsafe {
GetNamedSecurityInfoW(
PCWSTR(path_wide.as_ptr()),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION,
None,
None,
Some(&mut existing_dacl as *mut *mut ACL),
None,
&mut sd,
)
};
if get_rc.is_err() {
return Err(WindowsLaunchError::Acl(
"read existing DACL".to_string(),
get_rc,
));
}
let ea = EXPLICIT_ACCESS_W {
grfAccessPermissions: access_mask(grant.access),
grfAccessMode: GRANT_ACCESS,
grfInheritance: ACE_FLAGS(SUB_CONTAINERS_AND_OBJECTS_INHERIT),
Trustee: TRUSTEE_W {
pMultipleTrustee: std::ptr::null_mut(),
MultipleTrusteeOperation: NO_MULTIPLE_TRUSTEE,
TrusteeForm: TRUSTEE_IS_SID,
TrusteeType: TRUSTEE_IS_UNKNOWN,
ptstrName: PWSTR(container_sid.0 as *mut u16),
},
};
let mut new_dacl: *mut ACL = std::ptr::null_mut();
let entries = [ea];
let set_rc = unsafe {
SetEntriesInAclW(
Some(&entries),
Some(existing_dacl as *const ACL),
&mut new_dacl as *mut *mut ACL,
)
};
if set_rc.is_err() {
unsafe {
let _ = LocalFree(Some(HLOCAL(sd.0)));
}
return Err(WindowsLaunchError::Acl("merge ACE".to_string(), set_rc));
}
let apply_rc = unsafe {
SetNamedSecurityInfoW(
PCWSTR(path_wide.as_ptr()),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION,
None,
None,
Some(new_dacl as *const ACL),
None,
)
};
unsafe {
let _ = LocalFree(Some(HLOCAL(new_dacl as *mut core::ffi::c_void)));
}
if apply_rc.is_err() {
unsafe {
let _ = LocalFree(Some(HLOCAL(sd.0)));
}
return Err(WindowsLaunchError::Acl(
"install DACL".to_string(),
apply_rc,
));
}
unsafe {
let _ = LocalFree(Some(HLOCAL(sd.0)));
}
let sid_len = unsafe { GetLengthSid(container_sid) };
let sid_buf = unsafe { LocalAlloc(LPTR, sid_len as usize) }.map_err(|e| {
WindowsLaunchError::Acl(
"allocate container SID copy".to_string(),
WIN32_ERROR::from_error(&e).unwrap_or(ERROR_NOT_ENOUGH_MEMORY),
)
})?;
if let Err(e) = unsafe {
CopySid(
sid_len,
PSID(sid_buf.0 as *mut core::ffi::c_void),
container_sid,
)
} {
unsafe {
let _ = LocalFree(Some(sid_buf));
}
return Err(WindowsLaunchError::Acl(
"copy container SID for revert".to_string(),
WIN32_ERROR::from_error(&e).unwrap_or(ERROR_INVALID_PARAMETER),
));
}
Ok(AclGuard {
path_wide,
container_sid: PSID(sid_buf.0 as *mut core::ffi::c_void),
reverted: false,
})
}
struct Launched {
process: OwnedHandle,
thread: OwnedHandle,
}
fn create_process(
plan: &WindowsLaunchPlan,
env: &EnvironmentPolicy,
attr_list: &mut ProcThreadAttributeList,
cwd: Option<&std::path::Path>,
) -> Result<Launched, WindowsLaunchError> {
let app = wide_nul_os(&plan.program)
.map_err(|_| WindowsLaunchError::Encoding("program path has NUL".to_string()))?;
let mut cmdline = command_line_wide_for(plan);
if cmdline.contains(&0) {
return Err(WindowsLaunchError::Encoding(
"command line has NUL".to_string(),
));
}
cmdline.push(0);
let mut env_block = build_environment_block(env, plan.temp_home.as_deref());
let mut si = STARTUPINFOEXW::default();
si.StartupInfo.cb = std::mem::size_of::<STARTUPINFOEXW>() as u32;
si.lpAttributeList = attr_list.as_ptr();
let mut pi = PROCESS_INFORMATION::default();
let cwd_wide = match cwd {
Some(dir) => Some(wide_nul_os(dir.as_os_str()).map_err(|_| {
WindowsLaunchError::Encoding("working directory path has NUL".to_string())
})?),
None => None,
};
let cwd_ptr = cwd_wide
.as_ref()
.map(|w| PCWSTR(w.as_ptr()))
.unwrap_or(PCWSTR::null());
let ok = unsafe {
CreateProcessW(
PCWSTR(app.as_ptr()),
Some(PWSTR(cmdline.as_mut_ptr())),
None,
None,
false,
CREATE_SUSPENDED | EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT,
Some(env_block.as_mut_ptr() as *const c_void),
cwd_ptr,
&si as *const STARTUPINFOEXW as *const STARTUPINFOW,
&mut pi,
)
};
if let Err(e) = ok {
return Err(WindowsLaunchError::CreateProcess(
"CreateProcessW failed".to_string(),
e,
));
}
Ok(Launched {
process: OwnedHandle(pi.hProcess),
thread: OwnedHandle(pi.hThread),
})
}
fn build_environment_block(
policy: &EnvironmentPolicy,
temp_home_override: Option<&std::path::Path>,
) -> Vec<u16> {
let present: Vec<String> = std::env::vars_os()
.filter_map(|(k, _)| k.into_string().ok())
.collect();
let mut survivors = policy.surviving_vars(present.iter().map(|s| s.as_str()));
if policy.deny_sensitive {
survivors.retain(|name| {
std::env::var_os(name).is_none_or(|value| {
value
.to_str()
.map(|value| policy.assignment_survives(name, value))
.unwrap_or_else(|| !tirith_core::sensitive_assets::is_registered_env_name(name))
})
});
}
let temp_home = if policy.temporary_home {
temp_home_override
.map(|p| p.to_path_buf())
.unwrap_or_else(|| {
std::env::temp_dir().join(format!("tirith-capsule-{}", std::process::id()))
})
} else {
std::path::PathBuf::new()
};
let mut block: Vec<u16> = Vec::new();
for name in &survivors {
if policy.temporary_home && is_home_or_temp(name) {
continue;
}
if let Ok(val) = std::env::var(name) {
push_env_entry(&mut block, name, &val);
}
}
if policy.temporary_home {
let home = temp_home.to_string_lossy().to_string();
for key in [
"USERPROFILE",
"HOME",
"TEMP",
"TMP",
"LOCALAPPDATA",
"APPDATA",
] {
push_env_entry(&mut block, key, &home);
}
}
if block.is_empty() {
block.push(0);
}
block.push(0);
block
}
fn is_home_or_temp(name: &str) -> bool {
matches!(
name.to_ascii_uppercase().as_str(),
"USERPROFILE" | "HOME" | "TEMP" | "TMP" | "LOCALAPPDATA" | "APPDATA"
)
}
fn push_env_entry(block: &mut Vec<u16>, key: &str, value: &str) {
block.extend(OsStr::new(key).encode_wide());
block.push(u16::from(b'='));
block.extend(OsStr::new(value).encode_wide());
block.push(0);
}
struct ProcThreadAttributeList {
buf: Vec<u8>,
}
impl ProcThreadAttributeList {
fn with_security_capabilities(
caps: &mut SECURITY_CAPABILITIES,
) -> Result<Self, WindowsLaunchError> {
let mut size: usize = 0;
let _ = unsafe { InitializeProcThreadAttributeList(None, 1, None, &mut size) };
if size == 0 {
return Err(WindowsLaunchError::AttributeList(
"sizing returned zero".to_string(),
WinError::from(ERROR_INSUFFICIENT_BUFFER),
));
}
let mut buf = vec![0u8; size];
let list = LPPROC_THREAD_ATTRIBUTE_LIST(buf.as_mut_ptr() as *mut c_void);
let init = unsafe { InitializeProcThreadAttributeList(Some(list), 1, None, &mut size) };
if let Err(e) = init {
return Err(WindowsLaunchError::AttributeList(
"InitializeProcThreadAttributeList".to_string(),
e,
));
}
let upd = unsafe {
UpdateProcThreadAttribute(
list,
0,
PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES as usize,
Some(caps as *const SECURITY_CAPABILITIES as *const c_void),
std::mem::size_of::<SECURITY_CAPABILITIES>(),
None,
None,
)
};
if let Err(e) = upd {
unsafe { DeleteProcThreadAttributeList(list) };
return Err(WindowsLaunchError::AttributeList(
"UpdateProcThreadAttribute".to_string(),
e,
));
}
Ok(ProcThreadAttributeList { buf })
}
fn as_ptr(&mut self) -> LPPROC_THREAD_ATTRIBUTE_LIST {
LPPROC_THREAD_ATTRIBUTE_LIST(self.buf.as_mut_ptr() as *mut c_void)
}
}
impl Drop for ProcThreadAttributeList {
fn drop(&mut self) {
let list = LPPROC_THREAD_ATTRIBUTE_LIST(self.buf.as_mut_ptr() as *mut c_void);
unsafe { DeleteProcThreadAttributeList(list) };
}
}
fn create_and_assign_job(
plan: &WindowsLaunchPlan,
process: HANDLE,
) -> Result<OwnedHandle, WindowsLaunchError> {
let job = unsafe { CreateJobObjectW(None, PCWSTR::null()) }
.map_err(|e| WindowsLaunchError::JobObject("CreateJobObjectW".to_string(), e))?;
let job = OwnedHandle(job);
let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
let mut basic = JOBOBJECT_BASIC_LIMIT_INFORMATION::default();
let mut flags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
if let Some(ticks) = plan.job_limits.per_job_user_time_100ns {
basic.PerJobUserTimeLimit = ticks as i64;
flags |= JOB_OBJECT_LIMIT_JOB_TIME;
}
if let Some(procs) = plan.job_limits.active_process_limit {
basic.ActiveProcessLimit = procs;
flags |= JOB_OBJECT_LIMIT_ACTIVE_PROCESS;
}
basic.LimitFlags = flags;
info.BasicLimitInformation = basic;
if let Some(mem) = plan.job_limits.job_memory_bytes {
info.JobMemoryLimit = mem as usize;
info.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_JOB_MEMORY;
}
let set = unsafe {
SetInformationJobObject(
job.0,
JobObjectExtendedLimitInformation,
&info as *const _ as *const c_void,
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
)
};
if let Err(e) = set {
return Err(WindowsLaunchError::JobObject(
"SetInformationJobObject".to_string(),
e,
));
}
let assign = unsafe { AssignProcessToJobObject(job.0, process) };
if let Err(e) = assign {
return Err(WindowsLaunchError::JobObject(
"AssignProcessToJobObject".to_string(),
e,
));
}
Ok(job)
}
fn terminate_quietly(process: HANDLE) {
unsafe {
let _ = TerminateProcess(process, 1);
}
}
struct OwnedHandle(HANDLE);
impl Drop for OwnedHandle {
fn drop(&mut self) {
if !self.0.is_invalid() {
unsafe {
let _ = CloseHandle(self.0);
}
}
}
}
fn wide_nul(s: &str) -> Result<Vec<u16>, ()> {
if s.contains('\0') {
return Err(());
}
let mut v: Vec<u16> = OsStr::new(s).encode_wide().collect();
v.push(0);
Ok(v)
}
fn wide_nul_os(s: &OsStr) -> Result<Vec<u16>, ()> {
let mut out: Vec<u16> = s.encode_wide().collect();
if out.contains(&0) {
return Err(());
}
out.push(0);
Ok(out)
}
const SUB_CONTAINERS_AND_OBJECTS_INHERIT: u32 = 0x3;
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::test_harness::{EnvGuard, ENV_LOCK};
use std::path::Path;
use tirith_core::capsule::windows::windows_launch_plan;
use tirith_core::capsule::CapsuleSpec;
#[test]
fn wide_nul_encodes_and_terminates() {
let w = wide_nul("ab").expect("encode");
assert_eq!(w, vec![b'a' as u16, b'b' as u16, 0]);
}
#[test]
fn wide_nul_rejects_interior_nul() {
assert!(wide_nul("a\0b").is_err());
}
#[test]
fn access_mask_modify_includes_write() {
let m = access_mask(AclAccess::Modify);
assert!(m & GENERIC_WRITE.0 != 0, "modify must include write");
let r = access_mask(AclAccess::ReadExecute);
assert!(
r & GENERIC_WRITE.0 == 0,
"read-execute must not include write"
);
assert!(r & GENERIC_READ.0 != 0, "read-execute must include read");
}
#[test]
fn environment_block_strips_sensitive_and_double_nul_terminates() {
let _global = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _benign = EnvGuard::set("TIRITH_TEST_BENIGN", Path::new("ok"));
let _secret = EnvGuard::set("AWS_SECRET_ACCESS_KEY", Path::new("shh"));
let policy = EnvironmentPolicy {
inherit: true,
allow: Vec::new(),
deny_sensitive: true,
temporary_home: false,
};
let block = build_environment_block(&policy, None);
let decoded = String::from_utf16_lossy(&block);
assert!(decoded.contains("TIRITH_TEST_BENIGN=ok"));
assert!(
!decoded.contains("AWS_SECRET_ACCESS_KEY"),
"sensitive var must be stripped from the child env block"
);
assert_eq!(*block.last().unwrap(), 0, "block must end with a NUL");
}
#[test]
fn environment_block_overrides_home_under_temporary_home() {
let _global = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _profile = EnvGuard::set("USERPROFILE", Path::new("C:/Users/real"));
let policy = EnvironmentPolicy {
inherit: true,
allow: Vec::new(),
deny_sensitive: true,
temporary_home: true,
};
let block = build_environment_block(&policy, None);
let decoded = String::from_utf16_lossy(&block);
assert!(
!decoded.contains("C:/Users/real"),
"real USERPROFILE must be replaced under temporary_home"
);
assert!(decoded.contains("USERPROFILE="));
}
#[test]
fn apply_plan_refuses_allowlisted_via_core_plan() {
let mut spec = CapsuleSpec::locked_down();
spec.network = tirith_core::capsule::NetworkPolicy::AllowListedDomains {
domains: ["pypi.org".to_string()].into_iter().collect(),
ports: [443u16].into_iter().collect(),
};
assert!(windows_launch_plan(&spec, "C:/cmd.exe", &[]).is_err());
}
}