#![cfg(target_os = "windows")]
use tracing::{info, warn};
use windows::Win32::Foundation::{CloseHandle, HANDLE};
use windows::Win32::System::JobObjects::{
AssignProcessToJobObject, CreateJobObjectW, JOB_OBJECT_LIMIT_ACTIVE_PROCESS,
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, JOB_OBJECT_LIMIT_PROCESS_MEMORY,
JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JobObjectExtendedLimitInformation,
SetInformationJobObject,
};
use windows::Win32::System::Threading::{OpenProcess, PROCESS_ALL_ACCESS};
pub struct JobGuard {
handle: HANDLE,
}
unsafe impl Send for JobGuard {}
impl Drop for JobGuard {
fn drop(&mut self) {
if !self.handle.is_invalid() {
unsafe {
let _ = CloseHandle(self.handle);
}
}
}
}
pub fn open_process_handle(pid: u32) -> Result<std::os::windows::io::RawHandle, String> {
let handle = unsafe { OpenProcess(PROCESS_ALL_ACCESS, false, pid) }
.map_err(|e| format!("OpenProcess({pid}) failed: {e}"))?;
Ok(handle.0 as std::os::windows::io::RawHandle)
}
pub fn apply_job_confinement(
child_handle: std::os::windows::io::RawHandle,
max_memory_bytes: Option<u64>,
) -> Result<Option<JobGuard>, String> {
let job = unsafe { CreateJobObjectW(None, None) }
.map_err(|e| format!("CreateJobObjectW failed: {e}"))?;
if job.is_invalid() {
warn!("roboticus: warning: Windows Job Object creation returned invalid handle");
return Ok(None);
}
let mut limit_info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
limit_info.BasicLimitInformation.LimitFlags =
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_ACTIVE_PROCESS;
limit_info.BasicLimitInformation.ActiveProcessLimit = 8;
if let Some(max_bytes) = max_memory_bytes {
limit_info.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_PROCESS_MEMORY;
limit_info.ProcessMemoryLimit = max_bytes as usize;
}
unsafe {
SetInformationJobObject(
job,
JobObjectExtendedLimitInformation,
&limit_info as *const _ as *const std::ffi::c_void,
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
)
}
.map_err(|e| format!("SetInformationJobObject failed: {e}"))?;
let child = HANDLE(child_handle as *mut std::ffi::c_void);
unsafe { AssignProcessToJobObject(job, child) }
.map_err(|e| format!("AssignProcessToJobObject failed: {e}"))?;
info!("applied Windows Job Object confinement to script process");
Ok(Some(JobGuard { handle: job }))
}
pub fn warn_fs_confinement_limited() {
warn!(
"roboticus: Windows Job Objects do not restrict filesystem paths. \
script_fs_confinement provides memory/process limits only. \
Full path restriction requires AppContainer (not yet implemented)."
);
}
#[cfg(test)]
#[cfg(target_os = "windows")]
mod tests {
use super::*;
use std::os::windows::io::AsRawHandle;
use std::process::Command;
#[test]
fn job_guard_is_created() {
let child = Command::new("cmd")
.args(["/C", "echo hello"])
.spawn()
.unwrap();
let handle = child.as_raw_handle();
let guard = apply_job_confinement(handle, Some(256 * 1024 * 1024));
assert!(guard.is_ok());
let guard = guard.unwrap();
assert!(guard.is_some());
}
#[test]
fn job_guard_with_no_memory_limit() {
let child = Command::new("cmd")
.args(["/C", "echo hello"])
.spawn()
.unwrap();
let handle = child.as_raw_handle();
let guard = apply_job_confinement(handle, None);
assert!(guard.is_ok());
}
}