use std::io;
use std::os::windows::io::AsRawHandle;
use std::os::windows::process::CommandExt;
use std::path::{Path, PathBuf};
use std::process::{Child, Command};
use windows_sys::Win32::Foundation::{
CloseHandle, DUPLICATE_SAME_ACCESS, DuplicateHandle, HANDLE, INVALID_HANDLE_VALUE, LocalFree,
WAIT_OBJECT_0,
};
use windows_sys::Win32::Security::Authorization::{
ConvertStringSidToSidW, EXPLICIT_ACCESS_W, GRANT_ACCESS, GetNamedSecurityInfoW,
NO_MULTIPLE_TRUSTEE, SE_FILE_OBJECT, SetEntriesInAclW, SetNamedSecurityInfoW, TRUSTEE_IS_SID,
TRUSTEE_IS_UNKNOWN,
};
use windows_sys::Win32::Security::Isolation::{
CreateAppContainerProfile, DeleteAppContainerProfile, DeriveAppContainerSidFromAppContainerName,
};
use windows_sys::Win32::Security::{
ACL, DACL_SECURITY_INFORMATION, FreeSid, NO_INHERITANCE, PSECURITY_DESCRIPTOR, PSID,
SECURITY_CAPABILITIES, SUB_CONTAINERS_AND_OBJECTS_INHERIT,
};
use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT;
use windows_sys::Win32::System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, TH32CS_SNAPTHREAD, THREADENTRY32, Thread32First, Thread32Next,
};
use windows_sys::Win32::System::JobObjects::{
AssignProcessToJobObject, CreateJobObjectW, IsProcessInJob, JOB_OBJECT_LIMIT_ACTIVE_PROCESS,
JOB_OBJECT_LIMIT_JOB_MEMORY, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
JOB_OBJECT_LIMIT_PROCESS_MEMORY, JOB_OBJECT_LIMIT_PROCESS_TIME,
JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK, JOB_OBJECT_UILIMIT_EXITWINDOWS,
JOBOBJECT_BASIC_ACCOUNTING_INFORMATION, JOBOBJECT_BASIC_UI_RESTRICTIONS,
JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JobObjectBasicAccountingInformation,
JobObjectBasicUIRestrictions, JobObjectExtendedLimitInformation, QueryInformationJobObject,
SetInformationJobObject, TerminateJobObject,
};
use windows_sys::Win32::System::Threading::{
CREATE_BREAKAWAY_FROM_JOB, CREATE_NO_WINDOW, CREATE_SUSPENDED, CREATE_UNICODE_ENVIRONMENT,
CreateProcessW, DeleteProcThreadAttributeList, EXTENDED_STARTUPINFO_PRESENT, GetCurrentProcess,
GetExitCodeProcess, InitializeProcThreadAttributeList, OpenThread,
PROC_THREAD_ATTRIBUTE_HANDLE_LIST, PROC_THREAD_ATTRIBUTE_JOB_LIST,
PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES, PROCESS_INFORMATION, ResumeThread,
STARTF_USESTDHANDLES, STARTUPINFOEXW, STARTUPINFOW, THREAD_SUSPEND_RESUME,
UpdateProcThreadAttribute, WaitForSingleObject,
};
use super::{ResolvedLimits, Wait};
struct Job(HANDLE);
impl Drop for Job {
fn drop(&mut self) {
unsafe { CloseHandle(self.0) };
}
}
struct InheritableStdio([HANDLE; 3]);
impl InheritableStdio {
fn duplicate(stdio: super::RawStdio) -> io::Result<Self> {
let process = unsafe { GetCurrentProcess() };
let sources = [
stdio.stdin as HANDLE,
stdio.stdout as HANDLE,
stdio.stderr as HANDLE,
];
let mut handles = [std::ptr::null_mut(); 3];
for (index, source) in sources.into_iter().enumerate() {
if unsafe {
DuplicateHandle(
process,
source,
process,
&mut handles[index],
0,
1,
DUPLICATE_SAME_ACCESS,
)
} == 0
{
let error = io::Error::last_os_error();
for handle in handles.into_iter().take(index) {
unsafe { CloseHandle(handle) };
}
return Err(error);
}
}
Ok(Self(handles))
}
}
impl std::ops::Index<usize> for InheritableStdio {
type Output = HANDLE;
fn index(&self, index: usize) -> &Self::Output {
&self.0[index]
}
}
impl Drop for InheritableStdio {
fn drop(&mut self) {
for handle in self.0 {
unsafe { CloseHandle(handle) };
}
}
}
fn create_job(limits: &ResolvedLimits) -> io::Result<Job> {
let handle = unsafe { CreateJobObjectW(std::ptr::null(), std::ptr::null()) };
if handle.is_null() {
return Err(io::Error::last_os_error());
}
let job = Job(handle);
let mut info: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = unsafe { std::mem::zeroed() };
info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
| JOB_OBJECT_LIMIT_ACTIVE_PROCESS
| JOB_OBJECT_LIMIT_PROCESS_MEMORY
| JOB_OBJECT_LIMIT_JOB_MEMORY
| JOB_OBJECT_LIMIT_PROCESS_TIME;
const TICKS_PER_SECOND: i64 = 10_000_000;
info.BasicLimitInformation.PerProcessUserTimeLimit = limits
.cpu_secs
.saturating_mul(TICKS_PER_SECOND as u64)
.min(i64::MAX as u64) as i64;
info.BasicLimitInformation.ActiveProcessLimit =
limits.max_processes.min(u32::MAX as u64) as u32;
info.ProcessMemoryLimit = limits.memory_bytes.min(usize::MAX as u64) as usize;
info.JobMemoryLimit = limits.memory_bytes.min(usize::MAX as u64) as usize;
let ok = unsafe {
SetInformationJobObject(
job.0,
JobObjectExtendedLimitInformation,
&info as *const _ as *const core::ffi::c_void,
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
)
};
if ok == 0 {
return Err(io::Error::last_os_error());
}
let ui = JOBOBJECT_BASIC_UI_RESTRICTIONS {
UIRestrictionsClass: JOB_OBJECT_UILIMIT_EXITWINDOWS,
};
let ok = unsafe {
SetInformationJobObject(
job.0,
JobObjectBasicUIRestrictions,
&ui as *const _ as *const core::ffi::c_void,
std::mem::size_of::<JOBOBJECT_BASIC_UI_RESTRICTIONS>() as u32,
)
};
if ok == 0 {
return Err(io::Error::last_os_error());
}
Ok(job)
}
fn ambient_job_allows_breakaway() -> Option<bool> {
let mut in_job: i32 = 0;
if unsafe { IsProcessInJob(GetCurrentProcess(), std::ptr::null_mut(), &mut in_job) } == 0 {
diag("IsProcessInJob(self) FAILED -> returning None (unknown)");
return None;
}
if in_job == 0 {
diag("self is in NO job -> returning Some(false), no disclosure");
return Some(false);
}
let mut info: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = unsafe { std::mem::zeroed() };
let mut ret: u32 = 0;
let ok = unsafe {
QueryInformationJobObject(
std::ptr::null_mut(),
JobObjectExtendedLimitInformation,
&mut info as *mut _ as *mut core::ffi::c_void,
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
&mut ret,
)
};
if ok == 0 {
diag("QueryInformationJobObject(self) FAILED -> returning None (unknown)");
return None;
}
let flags = info.BasicLimitInformation.LimitFlags;
let silent = flags & JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK != 0;
diag(&format!(
"self IS in a job: LimitFlags=0x{:08X} APL={} SILENT_BREAKAWAY_OK={} -> Some({})",
flags, info.BasicLimitInformation.ActiveProcessLimit, silent, silent
));
Some(silent)
}
fn diag(msg: &str) {
let Ok(path) = std::env::var("CODECALC_DIAG_JOB") else {
return;
};
if path.is_empty() {
return;
}
use std::io::Write;
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
{
let _ = writeln!(f, "ambient_job_allows_breakaway: {msg}");
}
}
fn resume_process(pid: u32) -> io::Result<()> {
let snap = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0) };
if snap == INVALID_HANDLE_VALUE {
return Err(io::Error::last_os_error());
}
let mut entry: THREADENTRY32 = unsafe { std::mem::zeroed() };
entry.dwSize = std::mem::size_of::<THREADENTRY32>() as u32;
let mut resumed = 0usize;
if unsafe { Thread32First(snap, &mut entry) } != 0 {
loop {
if entry.th32OwnerProcessID == pid {
let thread = unsafe { OpenThread(THREAD_SUSPEND_RESUME, 0, entry.th32ThreadID) };
if !thread.is_null() {
if unsafe { ResumeThread(thread) } != u32::MAX {
resumed += 1;
}
unsafe { CloseHandle(thread) };
}
}
if unsafe { Thread32Next(snap, &mut entry) } == 0 {
break;
}
}
}
unsafe { CloseHandle(snap) };
if resumed == 0 {
return Err(io::Error::other(format!(
"created process {pid} suspended and could not resume it: no resumable thread found"
)));
}
Ok(())
}
fn to_wide(s: &str) -> Vec<u16> {
s.encode_utf16().chain(std::iter::once(0)).collect()
}
fn is_app_execution_alias(path: &Path) -> bool {
use std::os::windows::fs::MetadataExt;
super::is_windowsapps_alias_path(path)
|| std::fs::metadata(path)
.map(|m| {
m.is_file()
&& m.len() == 0
&& m.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0
})
.unwrap_or(false)
}
fn alias_refused_error(alias: &Path) -> io::Error {
io::Error::new(
io::ErrorKind::PermissionDenied,
format!(
"refusing to launch {}: it is a Windows Store app-execution alias that runs \
outside the sandbox job object, defeating every resource limit. Install a real \
interpreter (e.g. from python.org) or configure its absolute path.",
alias.display()
),
)
}
fn resolve_command_program(cmd: &Command) -> io::Result<PathBuf> {
let requested = PathBuf::from(cmd.get_program());
if requested.is_absolute() || requested.components().count() > 1 {
if !requested.is_file() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
"configured executable does not exist",
));
}
if is_app_execution_alias(&requested) {
return Err(alias_refused_error(&requested));
}
return Ok(requested);
}
let command_env = |name: &str| {
cmd.get_envs().find_map(|(key, value)| {
key.to_string_lossy()
.eq_ignore_ascii_case(name)
.then(|| value.map(|v| v.to_os_string()))
.flatten()
})
};
let path = command_env("PATH").unwrap_or_default();
let pathext = command_env("PATHEXT")
.unwrap_or_else(|| ".COM;.EXE;.BAT;.CMD".into())
.to_string_lossy()
.into_owned();
let mut names = vec![requested.clone()];
if requested.extension().is_none() {
for extension in pathext.split(';').filter(|extension| !extension.is_empty()) {
let mut name = requested.clone().into_os_string();
name.push(extension);
names.push(PathBuf::from(name));
}
}
let candidates =
std::env::split_paths(&path).flat_map(|dir| names.iter().map(move |name| dir.join(name)));
match super::choose_runtime(candidates, |p| p.is_file(), is_app_execution_alias) {
super::RuntimeChoice::Found(program) => Ok(program),
super::RuntimeChoice::OnlyAlias(alias) => Err(alias_refused_error(&alias)),
super::RuntimeChoice::None => Err(io::Error::new(
io::ErrorKind::NotFound,
format!(
"{} was not found on the configured runtime PATH",
requested.display()
),
)),
}
}
const GENERIC_READ: u32 = 0x8000_0000;
const GENERIC_EXECUTE: u32 = 0x2000_0000;
const GENERIC_ALL: u32 = 0x1000_0000;
const HRESULT_ALREADY_EXISTS: i32 = 0x8007_00B7u32 as i32;
struct AppContainer {
name_w: Vec<u16>,
sid: PSID,
}
impl Drop for AppContainer {
fn drop(&mut self) {
unsafe { DeleteAppContainerProfile(self.name_w.as_ptr()) };
if !self.sid.is_null() {
unsafe { FreeSid(self.sid) };
}
}
}
fn create_appcontainer() -> io::Result<AppContainer> {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let name: String = format!("codecalc.exec.{}.{n}.{nanos}", std::process::id())
.chars()
.take(64)
.collect();
let name_w = to_wide(&name);
let display_w = to_wide("codecalc executor sandbox");
let desc_w = to_wide("codecalc least-privilege execution AppContainer (unverified)");
let mut sid: PSID = std::ptr::null_mut();
let hr = unsafe {
CreateAppContainerProfile(
name_w.as_ptr(),
display_w.as_ptr(),
desc_w.as_ptr(),
std::ptr::null(),
0,
&mut sid,
)
};
if hr != 0 {
if hr == HRESULT_ALREADY_EXISTS {
let hr2 =
unsafe { DeriveAppContainerSidFromAppContainerName(name_w.as_ptr(), &mut sid) };
if hr2 != 0 {
return Err(io::Error::other(format!(
"DeriveAppContainerSidFromAppContainerName failed: HRESULT 0x{:08X}",
hr2 as u32
)));
}
} else {
return Err(io::Error::other(format!(
"CreateAppContainerProfile failed: HRESULT 0x{:08X}",
hr as u32
)));
}
}
if sid.is_null() {
unsafe { DeleteAppContainerProfile(name_w.as_ptr()) };
return Err(io::Error::other("AppContainer profile returned a null SID"));
}
Ok(AppContainer { name_w, sid })
}
fn grant_sid_path_access(sid: PSID, path: &std::path::Path, access: u32) -> io::Result<()> {
set_sid_path_access(sid, path, access, true)
}
fn set_sid_path_access(
sid: PSID,
path: &std::path::Path,
access: u32,
inherit: bool,
) -> io::Result<()> {
let path_w = to_wide(&path.to_string_lossy());
let mut old_dacl: *mut ACL = std::ptr::null_mut();
let mut sd: PSECURITY_DESCRIPTOR = std::ptr::null_mut();
let rc = unsafe {
GetNamedSecurityInfoW(
path_w.as_ptr(),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION,
std::ptr::null_mut(),
std::ptr::null_mut(),
&mut old_dacl,
std::ptr::null_mut(),
&mut sd,
)
};
if rc != 0 {
return Err(io::Error::from_raw_os_error(rc as i32));
}
struct LocalMem(*mut core::ffi::c_void);
impl Drop for LocalMem {
fn drop(&mut self) {
if !self.0.is_null() {
unsafe { LocalFree(self.0) };
}
}
}
let _sd_guard = LocalMem(sd);
let mut ea: EXPLICIT_ACCESS_W = unsafe { std::mem::zeroed() };
ea.grfAccessPermissions = access;
ea.grfAccessMode = GRANT_ACCESS;
ea.grfInheritance = if inherit {
SUB_CONTAINERS_AND_OBJECTS_INHERIT
} else {
NO_INHERITANCE
};
ea.Trustee.MultipleTrusteeOperation = NO_MULTIPLE_TRUSTEE;
ea.Trustee.pMultipleTrustee = std::ptr::null_mut();
ea.Trustee.TrusteeForm = TRUSTEE_IS_SID;
ea.Trustee.TrusteeType = TRUSTEE_IS_UNKNOWN;
ea.Trustee.ptstrName = sid as *mut u16;
let mut new_dacl: *mut ACL = std::ptr::null_mut();
let rc = unsafe { SetEntriesInAclW(1, &ea, old_dacl, &mut new_dacl) };
if rc != 0 {
return Err(io::Error::from_raw_os_error(rc as i32));
}
let _new_dacl_guard = LocalMem(new_dacl as *mut core::ffi::c_void);
let rc = unsafe {
SetNamedSecurityInfoW(
path_w.as_ptr(),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION,
std::ptr::null_mut(),
std::ptr::null_mut(),
new_dacl as *const ACL,
std::ptr::null(),
)
};
if rc != 0 {
return Err(io::Error::from_raw_os_error(rc as i32));
}
Ok(())
}
struct SidBox(PSID);
impl Drop for SidBox {
fn drop(&mut self) {
if !self.0.is_null() {
unsafe { LocalFree(self.0) };
}
}
}
fn all_application_packages_sid() -> io::Result<SidBox> {
let s = to_wide("S-1-15-2-1");
let mut sid: PSID = std::ptr::null_mut();
if unsafe { ConvertStringSidToSidW(s.as_ptr(), &mut sid) } == 0 {
return Err(io::Error::last_os_error());
}
Ok(SidBox(sid))
}
fn ac_grant_marker(program: &std::path::Path, dir: &std::path::Path) -> Option<PathBuf> {
use std::hash::{Hash, Hasher};
let base = std::env::var_os("LOCALAPPDATA")?;
let meta = std::fs::metadata(program).ok()?;
let mtime = meta
.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_nanos())
.unwrap_or(0);
let mut hasher = std::collections::hash_map::DefaultHasher::new();
dir.hash(&mut hasher);
meta.len().hash(&mut hasher);
mtime.hash(&mut hasher);
Some(
PathBuf::from(base)
.join("codecalc")
.join("ac-granted")
.join(format!("{:016x}", hasher.finish())),
)
}
fn grant_tree_explicit_read(sid: PSID, dir: &std::path::Path) -> io::Result<usize> {
const RX: u32 = GENERIC_READ | GENERIC_EXECUTE;
set_sid_path_access(sid, dir, RX, false)?;
let mut failures = 0usize;
let mut stack = vec![dir.to_path_buf()];
while let Some(d) = stack.pop() {
let entries = match std::fs::read_dir(&d) {
Ok(entries) => entries,
Err(_) => {
failures += 1;
continue;
}
};
for entry in entries.flatten() {
let path = entry.path();
if set_sid_path_access(sid, &path, RX, false).is_err() {
failures += 1;
}
if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
stack.push(path);
}
}
}
Ok(failures)
}
fn ensure_interpreter_ac_readable(
program: &std::path::Path,
dir: &std::path::Path,
) -> io::Result<()> {
let marker = ac_grant_marker(program, dir);
if let Some(m) = &marker
&& m.is_file()
{
return Ok(());
}
let aap = all_application_packages_sid()?;
let failures = grant_tree_explicit_read(aap.0, dir)?;
if failures == 0
&& let Some(m) = marker
{
if let Some(parent) = m.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = std::fs::write(&m, b"");
}
Ok(())
}
fn prepare_appcontainer(cmd: &Command) -> io::Result<(AppContainer, Box<SECURITY_CAPABILITIES>)> {
let ac = create_appcontainer()?;
if let Some(work) = cmd.get_current_dir() {
grant_sid_path_access(ac.sid, work, GENERIC_ALL)
.map_err(|e| io::Error::other(format!("appcontainer workdir ACL grant failed: {e}")))?;
}
if let Ok(program) = resolve_command_program(cmd)
&& let Some(dir) = program.parent()
{
ensure_interpreter_ac_readable(&program, dir).map_err(|e| {
io::Error::other(format!("appcontainer interpreter AAP grant failed: {e}"))
})?;
}
let caps = Box::new(SECURITY_CAPABILITIES {
AppContainerSid: ac.sid,
Capabilities: std::ptr::null_mut(),
CapabilityCount: 0,
Reserved: 0,
});
Ok((ac, caps))
}
#[allow(clippy::too_many_arguments)]
fn spawn_with_job_at_creation(
cmd: &Command,
stdio: super::RawStdio,
job: HANDLE,
breakaway: bool,
sec_caps: Option<*const SECURITY_CAPABILITIES>,
) -> io::Result<(HANDLE, HANDLE)> {
let inheritable_stdio = InheritableStdio::duplicate(stdio)?;
let program = resolve_command_program(cmd)?;
let program_w = to_wide(&program.to_string_lossy());
let mut line = super::quote_arg(program.as_os_str());
for a in cmd.get_args() {
line.push(' ');
line.push_str(&super::quote_arg(a));
}
let mut line_w = to_wide(&line);
let mut env_w: Vec<u16> = Vec::new();
for (k, v) in cmd.get_envs() {
let Some(v) = v else { continue };
env_w.extend(k.to_string_lossy().encode_utf16());
env_w.push(u16::from(b'='));
env_w.extend(v.to_string_lossy().encode_utf16());
env_w.push(0);
}
env_w.push(0);
let cwd_w = cmd.get_current_dir().map(|d| to_wide(&d.to_string_lossy()));
let attr_count: u32 = if sec_caps.is_some() { 3 } else { 2 };
let mut size: usize = 0;
unsafe { InitializeProcThreadAttributeList(std::ptr::null_mut(), attr_count, 0, &mut size) };
if size == 0 {
return Err(io::Error::last_os_error());
}
let mut attr_buf = vec![0u8; size];
let attr_list = attr_buf.as_mut_ptr() as *mut core::ffi::c_void;
if unsafe { InitializeProcThreadAttributeList(attr_list, attr_count, 0, &mut size) } == 0 {
return Err(io::Error::last_os_error());
}
let job_handle = job;
let ok = unsafe {
UpdateProcThreadAttribute(
attr_list,
0,
PROC_THREAD_ATTRIBUTE_JOB_LIST as usize,
&job_handle as *const HANDLE as *const core::ffi::c_void,
std::mem::size_of::<HANDLE>(),
std::ptr::null_mut(),
std::ptr::null(),
)
};
if ok == 0 {
let e = io::Error::last_os_error();
unsafe { DeleteProcThreadAttributeList(attr_list) };
return Err(e);
}
let inherited_handles = [
inheritable_stdio[0],
inheritable_stdio[1],
inheritable_stdio[2],
];
let ok = unsafe {
UpdateProcThreadAttribute(
attr_list,
0,
PROC_THREAD_ATTRIBUTE_HANDLE_LIST as usize,
inherited_handles.as_ptr() as *const core::ffi::c_void,
std::mem::size_of_val(&inherited_handles),
std::ptr::null_mut(),
std::ptr::null(),
)
};
if ok == 0 {
let e = io::Error::last_os_error();
unsafe { DeleteProcThreadAttributeList(attr_list) };
return Err(e);
}
if let Some(caps) = sec_caps {
let ok = unsafe {
UpdateProcThreadAttribute(
attr_list,
0,
PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES as usize,
caps as *const core::ffi::c_void,
std::mem::size_of::<SECURITY_CAPABILITIES>(),
std::ptr::null_mut(),
std::ptr::null(),
)
};
if ok == 0 {
let e = io::Error::last_os_error();
unsafe { DeleteProcThreadAttributeList(attr_list) };
return Err(e);
}
}
let mut si: STARTUPINFOEXW = unsafe { std::mem::zeroed() };
si.StartupInfo.cb = std::mem::size_of::<STARTUPINFOEXW>() as u32;
si.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
si.StartupInfo.hStdInput = inheritable_stdio[0];
si.StartupInfo.hStdOutput = inheritable_stdio[1];
si.StartupInfo.hStdError = inheritable_stdio[2];
si.lpAttributeList = attr_list;
let mut flags = EXTENDED_STARTUPINFO_PRESENT | CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT;
if breakaway {
flags |= CREATE_BREAKAWAY_FROM_JOB;
}
let mut pi: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };
let created = unsafe {
CreateProcessW(
program_w.as_ptr(),
line_w.as_mut_ptr(),
std::ptr::null(),
std::ptr::null(),
1, flags,
env_w.as_ptr() as *const core::ffi::c_void,
cwd_w
.as_ref()
.map(|w| w.as_ptr())
.unwrap_or(std::ptr::null()),
&si as *const STARTUPINFOEXW as *const STARTUPINFOW,
&mut pi,
)
};
let err = io::Error::last_os_error();
unsafe { DeleteProcThreadAttributeList(attr_list) };
if created == 0 {
return Err(err);
}
diag(&format!(
"spawn_with_job_at_creation: created PID={} breakaway={breakaway}",
pi.dwProcessId
));
Ok((pi.hProcess, pi.hThread))
}
pub fn spawn_and_wait(
mut cmd: Command,
limits: &ResolvedLimits,
stdio: super::RawStdio,
) -> io::Result<Wait> {
let mut unenforced = vec![
"cpu_limit_counts_user_time_only_on_windows",
"open_file_limit_unavailable_on_windows",
"file_size_limit_unavailable_on_windows",
];
if limits.no_net {
unenforced.push("no_net_unavailable_on_windows");
}
let job = create_job(limits)?;
let appcontainer = std::env::var("CODECALC_WIN_APPCONTAINER")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false);
let at_creation = appcontainer
|| std::env::var("CODECALC_WIN_JOB_AT_CREATION")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(true);
let mut child: Option<Child> = None;
let process: HANDLE;
let mut appcontainer_guard: Option<AppContainer> = None;
let mut caps_box: Option<Box<SECURITY_CAPABILITIES>> = None;
if at_creation {
let breakaway = ambient_job_allows_breakaway().unwrap_or(false);
let mut sec_caps_ptr: Option<*const SECURITY_CAPABILITIES> = None;
if appcontainer {
unenforced.push("appcontainer_isolation_unverified_on_windows");
let (ac, caps) = prepare_appcontainer(&cmd)?;
sec_caps_ptr = Some(caps.as_ref() as *const SECURITY_CAPABILITIES);
appcontainer_guard = Some(ac);
caps_box = Some(caps);
}
let (p, thread) = spawn_with_job_at_creation(&cmd, stdio, job.0, breakaway, sec_caps_ptr)
.map_err(|e| {
io::Error::other(format!(
"creation-time strict launch failed (appcontainer={appcontainer}): {e}"
))
})?;
unsafe { ResumeThread(thread) };
unsafe { CloseHandle(thread) };
process = p;
} else {
cmd.creation_flags(CREATE_NO_WINDOW | CREATE_SUSPENDED);
let spawned: Child = cmd.spawn()?;
process = spawned.as_raw_handle() as HANDLE;
child = Some(spawned);
unenforced.push("process_limit_enforcement_unverified_on_windows");
}
if let Some(c) = child.as_mut() {
if unsafe { AssignProcessToJobObject(job.0, process) } == 0 {
let err = io::Error::last_os_error();
let _ = c.kill();
let _ = c.wait();
return Err(err);
}
if let Err(err) = resume_process(c.id()) {
let _ = c.kill();
let _ = c.wait();
return Err(err);
}
}
let mut limit_unverified: Option<&'static str> = None;
let mut in_our_job: i32 = 0;
let queried = unsafe { IsProcessInJob(process, job.0, &mut in_our_job) };
if queried == 0 {
limit_unverified = Some("process_limit_membership_unverifiable_on_windows");
} else if in_our_job == 0 {
limit_unverified = Some("process_limit_not_enforced_child_escaped_the_job");
}
if limit_unverified.is_none() {
match ambient_job_allows_breakaway() {
Some(true) => {
limit_unverified = Some("process_limit_not_enforced_ambient_job_allows_breakaway")
}
None => limit_unverified = Some("process_limit_enforcement_unknown_on_windows"),
Some(false) => {}
}
}
if let Some(reason) = limit_unverified {
unenforced.push(reason);
}
let timeout_ms = limits
.timeout_secs
.saturating_mul(1000)
.min(u32::MAX as u64) as u32;
let waited = unsafe { WaitForSingleObject(process, timeout_ms) };
let timed_out = waited != WAIT_OBJECT_0;
if timed_out {
unsafe { TerminateJobObject(job.0, 1) };
if let Some(c) = child.as_mut() {
let _ = c.wait();
}
}
let mut code: u32 = 0;
unsafe { GetExitCodeProcess(process, &mut code) };
let mut acct: JOBOBJECT_BASIC_ACCOUNTING_INFORMATION = unsafe { std::mem::zeroed() };
let mut ret: u32 = 0;
let cpu_ms = unsafe {
if QueryInformationJobObject(
job.0,
JobObjectBasicAccountingInformation,
&mut acct as *mut _ as *mut core::ffi::c_void,
std::mem::size_of::<JOBOBJECT_BASIC_ACCOUNTING_INFORMATION>() as u32,
&mut ret,
) != 0
{
((acct.TotalUserTime as u64 + acct.TotalKernelTime as u64) / 10_000) as u64
} else {
0
}
};
let mut ext: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = unsafe { std::mem::zeroed() };
let peak_memory_kb = unsafe {
if QueryInformationJobObject(
job.0,
JobObjectExtendedLimitInformation,
&mut ext as *mut _ as *mut core::ffi::c_void,
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
&mut ret,
) != 0
{
(ext.PeakJobMemoryUsed as u64) / 1024
} else {
0
}
};
drop(caps_box);
drop(appcontainer_guard);
Ok(Wait {
exit_code: code as i64,
signal: None,
timed_out,
cpu_ms,
peak_memory_kb,
no_net_seccomp_enforced: false,
unenforced,
})
}
pub fn current_uid_tasks() -> Option<u64> {
None
}
pub fn no_net_kernel_enforcement_available() -> bool {
false
}