#![cfg(target_os = "windows")]
use std::os::windows::io::{AsRawHandle, RawHandle};
use std::os::windows::process::CommandExt;
use std::process::{Child, Command};
use std::ptr;
use windows_sys::Win32::Foundation::{CloseHandle, GetLastError, ERROR_SUCCESS, FALSE, HANDLE};
use windows_sys::Win32::System::JobObjects::{
AssignProcessToJobObject, CreateJobObjectW, JobObjectBasicLimitInformation,
JobObjectBasicUIRestrictions, SetInformationJobObject, JOBOBJECT_BASIC_LIMIT_INFORMATION,
JOBOBJECT_BASIC_UI_RESTRICTIONS, JOB_OBJECT_LIMIT_ACTIVE_PROCESS,
JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
JOB_OBJECT_UILIMIT_DESKTOP, JOB_OBJECT_UILIMIT_DISPLAYSETTINGS, JOB_OBJECT_UILIMIT_EXITWINDOWS,
JOB_OBJECT_UILIMIT_GLOBALATOMS, JOB_OBJECT_UILIMIT_HANDLES, JOB_OBJECT_UILIMIT_READCLIPBOARD,
JOB_OBJECT_UILIMIT_SYSTEMPARAMETERS, JOB_OBJECT_UILIMIT_WRITECLIPBOARD,
};
use windows_sys::Win32::System::Threading::CREATE_SUSPENDED;
use super::SandboxTier;
#[link(name = "ntdll")]
extern "system" {
fn NtResumeProcess(process_handle: HANDLE) -> i32;
}
pub struct OwnedJob {
handle: HANDLE,
}
impl OwnedJob {
fn new() -> std::io::Result<Self> {
let h = unsafe { CreateJobObjectW(ptr::null_mut(), ptr::null()) };
if h.is_null() {
return Err(last_os_error("CreateJobObjectW"));
}
Ok(Self { handle: h })
}
}
impl Drop for OwnedJob {
fn drop(&mut self) {
if !self.handle.is_null() {
unsafe {
CloseHandle(self.handle);
}
}
}
}
pub fn apply_to_process(tier: SandboxTier, child: &Child) -> std::io::Result<Option<OwnedJob>> {
if matches!(tier, SandboxTier::None) {
return Ok(None);
}
let job = OwnedJob::new()?;
set_basic_limits(job.handle, tier)?;
if matches!(tier, SandboxTier::Lockdown) {
set_ui_restrictions(job.handle)?;
}
let process_handle: RawHandle = child.as_raw_handle();
let assigned = unsafe { AssignProcessToJobObject(job.handle, process_handle as HANDLE) };
if assigned == FALSE {
return Err(last_os_error("AssignProcessToJobObject"));
}
Ok(Some(job))
}
fn set_basic_limits(job: HANDLE, tier: SandboxTier) -> std::io::Result<()> {
let mut limit_flags =
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION;
let mut active_process_limit = 0u32;
if matches!(tier, SandboxTier::Lockdown) {
limit_flags |= JOB_OBJECT_LIMIT_ACTIVE_PROCESS;
active_process_limit = 1;
}
let mut basic = JOBOBJECT_BASIC_LIMIT_INFORMATION {
LimitFlags: limit_flags,
ActiveProcessLimit: active_process_limit,
..unsafe { std::mem::zeroed() }
};
#[allow(clippy::cast_possible_truncation)]
let size = std::mem::size_of::<JOBOBJECT_BASIC_LIMIT_INFORMATION>() as u32;
let rc = unsafe {
SetInformationJobObject(
job,
JobObjectBasicLimitInformation,
std::ptr::addr_of_mut!(basic).cast(),
size,
)
};
if rc == FALSE {
return Err(last_os_error(
"SetInformationJobObject(BasicLimitInformation)",
));
}
Ok(())
}
fn set_ui_restrictions(job: HANDLE) -> std::io::Result<()> {
let mut ui = JOBOBJECT_BASIC_UI_RESTRICTIONS {
UIRestrictionsClass: JOB_OBJECT_UILIMIT_DESKTOP
| JOB_OBJECT_UILIMIT_DISPLAYSETTINGS
| JOB_OBJECT_UILIMIT_EXITWINDOWS
| JOB_OBJECT_UILIMIT_GLOBALATOMS
| JOB_OBJECT_UILIMIT_HANDLES
| JOB_OBJECT_UILIMIT_READCLIPBOARD
| JOB_OBJECT_UILIMIT_SYSTEMPARAMETERS
| JOB_OBJECT_UILIMIT_WRITECLIPBOARD,
};
#[allow(clippy::cast_possible_truncation)]
let size = std::mem::size_of::<JOBOBJECT_BASIC_UI_RESTRICTIONS>() as u32;
let rc = unsafe {
SetInformationJobObject(
job,
JobObjectBasicUIRestrictions,
std::ptr::addr_of_mut!(ui).cast(),
size,
)
};
if rc == FALSE {
return Err(last_os_error(
"SetInformationJobObject(BasicUIRestrictions)",
));
}
Ok(())
}
fn last_os_error(label: &str) -> std::io::Error {
let err = unsafe { GetLastError() };
if err == ERROR_SUCCESS {
return std::io::Error::other(format!("{label} failed with no error code"));
}
let raw = i32::try_from(err).unwrap_or(i32::MAX);
std::io::Error::from_raw_os_error(raw)
}
#[must_use]
pub fn available() -> bool {
true
}
pub fn spawn_in_job(
cmd: &mut Command,
tier: SandboxTier,
) -> std::io::Result<(Child, Option<OwnedJob>)> {
if matches!(tier, SandboxTier::None) {
let child = cmd.spawn()?;
return Ok((child, None));
}
cmd.creation_flags(CREATE_SUSPENDED);
let mut child = cmd.spawn()?;
let job = match prepare_job(tier) {
Ok(j) => j,
Err(e) => {
terminate_and_release(&mut child);
return Err(e);
}
};
let process_handle = child.as_raw_handle() as HANDLE;
let assigned = unsafe { AssignProcessToJobObject(job.handle, process_handle) };
if assigned == FALSE {
let err = last_os_error("AssignProcessToJobObject");
terminate_and_release(&mut child);
return Err(err);
}
let status = unsafe { NtResumeProcess(process_handle) };
if status < 0 {
terminate_and_release(&mut child);
return Err(std::io::Error::other(format!(
"NtResumeProcess failed with NTSTATUS 0x{status:x}"
)));
}
Ok((child, Some(job)))
}
fn prepare_job(tier: SandboxTier) -> std::io::Result<OwnedJob> {
let job = OwnedJob::new()?;
set_basic_limits(job.handle, tier)?;
if matches!(tier, SandboxTier::Lockdown) {
set_ui_restrictions(job.handle)?;
}
Ok(job)
}
fn terminate_and_release(child: &mut Child) {
let _ = child.kill();
let _ = child.wait();
}