use std::io;
use std::os::windows::io::AsRawHandle;
use std::os::windows::process::CommandExt;
use std::path::PathBuf;
use std::process::{Child, Command};
use windows_sys::Win32::Foundation::{
CloseHandle, DUPLICATE_SAME_ACCESS, DuplicateHandle, HANDLE, INVALID_HANDLE_VALUE,
WAIT_OBJECT_0,
};
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, 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 resolve_command_program(cmd: &Command) -> io::Result<PathBuf> {
let requested = PathBuf::from(cmd.get_program());
if requested.is_absolute() || requested.components().count() > 1 {
return requested.is_file().then_some(requested).ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
"configured executable does not exist",
)
});
}
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));
}
}
for directory in std::env::split_paths(&path) {
for name in &names {
let candidate = directory.join(name);
if candidate.is_file() {
return Ok(candidate);
}
}
}
Err(io::Error::new(
io::ErrorKind::NotFound,
format!(
"{} was not found on the configured runtime PATH",
requested.display()
),
))
}
#[allow(clippy::too_many_arguments)]
fn spawn_with_job_at_creation(
cmd: &Command,
stdio: super::RawStdio,
job: HANDLE,
breakaway: bool,
) -> 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 mut size: usize = 0;
unsafe { InitializeProcThreadAttributeList(std::ptr::null_mut(), 2, 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, 2, 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);
}
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 at_creation = 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;
if at_creation {
let breakaway = ambient_job_allows_breakaway().unwrap_or(false);
let (p, thread) =
spawn_with_job_at_creation(&cmd, stdio, job.0, breakaway).map_err(|e| {
io::Error::other(format!(
"CODECALC_WIN_JOB_AT_CREATION=1 but creation-time job assignment failed: {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
}
};
Ok(Wait {
exit_code: code as i64,
signal: None,
timed_out,
cpu_ms,
peak_memory_kb,
unenforced,
})
}
pub fn current_uid_tasks() -> Option<u64> {
None
}