use super::{Backend, RunSpec, Sandbox, SandboxOutcome};
use crate::error::Result;
pub struct WindowsSandbox;
impl Sandbox for WindowsSandbox {
async fn run(&self, spec: RunSpec<'_>) -> Result<SandboxOutcome> {
#[cfg(windows)]
{
job::run(spec).await
}
#[cfg(not(windows))]
{
super::run_capped(Backend::PortableFloor, spec, |_cmd| {}).await
}
}
fn backend(&self) -> Backend {
#[cfg(windows)]
{
Backend::WindowsJobObject
}
#[cfg(not(windows))]
{
Backend::PortableFloor
}
}
}
#[cfg_attr(not(windows), allow(dead_code))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct JobLimits {
pub process_memory: Option<u64>,
pub active_processes: Option<u64>,
pub cpu_ticks: Option<u64>,
pub kill_on_close: bool,
}
impl From<&super::SandboxLimits> for JobLimits {
fn from(l: &super::SandboxLimits) -> Self {
Self {
process_memory: l.max_memory_bytes,
active_processes: l.max_processes,
cpu_ticks: l.max_cpu_secs.map(|s| s.saturating_mul(10_000_000)),
kill_on_close: true,
}
}
}
#[cfg(windows)]
mod job {
use std::io;
use windows_sys::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE};
use windows_sys::Win32::System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, Thread32First, Thread32Next, TH32CS_SNAPTHREAD, THREADENTRY32,
};
use windows_sys::Win32::System::JobObjects::{
AssignProcessToJobObject, CreateJobObjectW, JobObjectBasicAccountingInformation,
JobObjectExtendedLimitInformation, QueryInformationJobObject, SetInformationJobObject,
JOBOBJECTINFOCLASS, JOBOBJECT_BASIC_ACCOUNTING_INFORMATION,
JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JOB_OBJECT_LIMIT_ACTIVE_PROCESS,
JOB_OBJECT_LIMIT_JOB_TIME, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
JOB_OBJECT_LIMIT_PROCESS_MEMORY,
};
use windows_sys::Win32::System::Threading::{
OpenThread, ResumeThread, CREATE_SUSPENDED, THREAD_SUSPEND_RESUME,
};
use super::JobLimits;
use crate::error::{Error, Result};
use crate::sandbox::{run_capped, run_capped_hooked, Backend, Cap, RunSpec, SandboxOutcome};
pub(super) async fn run(spec: RunSpec<'_>) -> Result<SandboxOutcome> {
let limits = JobLimits::from(spec.limits);
let job = match Job::create(&limits) {
Ok(job) => job,
Err(e) => {
tracing::warn!(
"sandbox: could not create a Windows job object ({e}); \
falling back to the portable floor, which on this platform \
enforces the wall clock and nothing else"
);
return run_capped(Backend::PortableFloor, spec, |_cmd| {}).await;
}
};
let mut outcome = run_capped_hooked(
Backend::WindowsJobObject,
spec,
|cmd| {
cmd.creation_flags(CREATE_SUSPENDED);
},
|child| job.adopt(child),
)
.await?;
if outcome.cap_hit.is_none() && outcome.exit_code != Some(0) {
outcome.cap_hit = job.cap_hit(&limits);
}
Ok(outcome)
}
struct Job(HANDLE);
unsafe impl Send for Job {}
unsafe impl Sync for Job {}
impl Job {
fn create(limits: &JobLimits) -> io::Result<Self> {
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::default();
let basic = &mut info.BasicLimitInformation;
debug_assert!(limits.kill_on_close);
basic.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
if let Some(bytes) = limits.process_memory {
info.ProcessMemoryLimit = usize::try_from(bytes).unwrap_or(usize::MAX);
basic.LimitFlags |= JOB_OBJECT_LIMIT_PROCESS_MEMORY;
}
if let Some(n) = limits.active_processes {
basic.ActiveProcessLimit = u32::try_from(n).unwrap_or(u32::MAX);
basic.LimitFlags |= JOB_OBJECT_LIMIT_ACTIVE_PROCESS;
}
if let Some(ticks) = limits.cpu_ticks {
basic.PerJobUserTimeLimit = i64::try_from(ticks).unwrap_or(i64::MAX);
basic.LimitFlags |= JOB_OBJECT_LIMIT_JOB_TIME;
}
let ok = unsafe {
SetInformationJobObject(
job.0,
JobObjectExtendedLimitInformation,
std::ptr::from_ref(&info).cast(),
size_of_val(&info) as u32,
)
};
if ok == 0 {
return Err(io::Error::last_os_error());
}
Ok(job)
}
fn adopt(&self, child: &tokio::process::Child) -> Result<()> {
let sandbox = |reason: String| Error::Sandbox { reason };
let handle = child.raw_handle().ok_or_else(|| {
sandbox("the sandboxed child exited before it could be put in the job".into())
})?;
let pid = child.id().ok_or_else(|| {
sandbox("the sandboxed child exited before it could be put in the job".into())
})?;
let ok = unsafe { AssignProcessToJobObject(self.0, handle.cast()) };
if ok == 0 {
return Err(sandbox(format!(
"could not assign the sandboxed process to its job object: {}",
io::Error::last_os_error()
)));
}
resume(pid).map_err(|e| {
sandbox(format!(
"the sandboxed process was put in its job object but could not be \
resumed ({e}); it is being killed rather than left suspended"
))
})
}
fn cap_hit(&self, limits: &JobLimits) -> Option<Cap> {
let acct: JOBOBJECT_BASIC_ACCOUNTING_INFORMATION =
self.query(JobObjectBasicAccountingInformation)?;
if let Some(ticks) = limits.cpu_ticks {
if acct.TotalUserTime.max(0) as u64 >= ticks {
return Some(Cap::Cpu);
}
}
if let Some(bytes) = limits.process_memory {
let ext: JOBOBJECT_EXTENDED_LIMIT_INFORMATION =
self.query(JobObjectExtendedLimitInformation)?;
if ext.PeakProcessMemoryUsed as u64 >= bytes - bytes / 10 {
return Some(Cap::Memory);
}
}
if let Some(n) = limits.active_processes {
if u64::from(acct.TotalProcesses) >= n {
return Some(Cap::Processes);
}
}
None
}
fn query<T: Default>(&self, class: JOBOBJECTINFOCLASS) -> Option<T> {
let mut out = T::default();
let ok = unsafe {
QueryInformationJobObject(
self.0,
class,
std::ptr::from_mut(&mut out).cast(),
size_of::<T>() as u32,
std::ptr::null_mut(),
)
};
(ok != 0).then_some(out)
}
}
impl Drop for Job {
fn drop(&mut self) {
unsafe { CloseHandle(self.0) };
}
}
fn resume(pid: u32) -> io::Result<()> {
let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0) };
if snapshot == INVALID_HANDLE_VALUE {
return Err(io::Error::last_os_error());
}
let mut entry = THREADENTRY32 {
dwSize: size_of::<THREADENTRY32>() as u32,
..Default::default()
};
let mut resumed = 0usize;
let mut more = unsafe { Thread32First(snapshot, &mut entry) } != 0;
while more {
if entry.th32OwnerProcessID == pid {
let thread = unsafe { OpenThread(THREAD_SUSPEND_RESUME, 0, entry.th32ThreadID) };
if !thread.is_null() {
unsafe {
ResumeThread(thread);
CloseHandle(thread);
}
resumed += 1;
}
}
entry.dwSize = size_of::<THREADENTRY32>() as u32;
more = unsafe { Thread32Next(snapshot, &mut entry) } != 0;
}
unsafe { CloseHandle(snapshot) };
if resumed == 0 {
return Err(io::Error::other(format!(
"no thread of the sandboxed process {pid} could be resumed"
)));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sandbox::SandboxLimits;
#[test]
fn maps_limits_to_job_object_fields_and_ticks() {
let lim = SandboxLimits {
max_cpu_secs: Some(3),
max_memory_bytes: Some(64 * 1024 * 1024),
max_processes: Some(8),
..SandboxLimits::default()
};
let job = JobLimits::from(&lim);
assert_eq!(job.process_memory, Some(64 * 1024 * 1024));
assert_eq!(job.active_processes, Some(8));
assert_eq!(job.cpu_ticks, Some(30_000_000)); assert!(job.kill_on_close, "job must kill the tree on close");
}
#[test]
fn absent_limits_map_to_nothing_rather_than_zero() {
let lim = SandboxLimits {
max_cpu_secs: None,
max_memory_bytes: None,
max_processes: None,
..SandboxLimits::default()
};
let job = JobLimits::from(&lim);
assert_eq!(job.process_memory, None);
assert_eq!(job.active_processes, None);
assert_eq!(job.cpu_ticks, None);
assert!(job.kill_on_close);
}
}