mod acl;
mod cap;
mod proc_thread_attr;
mod process;
mod token;
mod winutil;
use super::SandboxPolicy;
use crate::tools::bash::output::{
BASH_COMMAND_TIMEOUT, BASH_READ_CHUNK_BYTES, MAX_BASH_OUTPUT_BYTES, SUPERVISOR_POLL_INTERVAL,
TerminationReason,
};
use std::collections::HashMap;
use std::ffi::OsStr;
use std::io;
use std::path::{Path, PathBuf};
use std::ptr;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Instant;
use windows_sys::Win32::Foundation::{
CloseHandle, GetLastError, HANDLE, HANDLE_FLAG_INHERIT, SetHandleInformation, WAIT_OBJECT_0,
WAIT_TIMEOUT,
};
use windows_sys::Win32::Storage::FileSystem::ReadFile;
use windows_sys::Win32::System::Pipes::CreatePipe;
use windows_sys::Win32::System::Threading::{
GetExitCodeProcess, TerminateProcess, WaitForSingleObject,
};
const TERMINATE_WAIT_MS: u32 = 2_000;
pub(in crate::tools::bash) struct SupervisedOutput {
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
pub exit_code: Option<i32>,
pub terminated_for: Option<TerminationReason>,
}
#[allow(dead_code)]
pub fn is_available() -> bool {
unsafe {
match token::get_current_token_for_restriction() {
Ok(h) => {
CloseHandle(h);
true
}
Err(_) => false,
}
}
}
pub(in crate::tools::bash) fn run_confined(
shell: &OsStr,
command: &str,
workspace: &Path,
extra_path_dir: Option<&Path>,
policy: &SandboxPolicy,
interrupt_flag: &AtomicBool,
) -> io::Result<SupervisedOutput> {
let cap_sid_string = cap::workspace_cap_sid(workspace)?;
let cap_sid = token::LocalSid::from_string(&cap_sid_string)?;
let cap_ptrs: [*mut std::ffi::c_void; 1] = [cap_sid.as_ptr()];
for root in &policy.writable_roots {
if root.is_dir() {
unsafe {
acl::ensure_allow_write_aces(root, &cap_ptrs)?;
}
}
}
let restricted_token = unsafe {
let base = token::get_current_token_for_restriction()?;
let result = token::create_workspace_write_token_with_caps_from(base, &cap_ptrs);
CloseHandle(base);
result?
};
let _token_guard = TokenHandle(restricted_token);
let env_map = build_child_env(extra_path_dir);
let argv = vec![
shell.to_string_lossy().into_owned(),
"-c".to_string(),
command.to_string(),
];
let (stdin_read, stdin_write) = create_pipe(true, false)?;
let (stdout_read, stdout_write) = create_pipe(false, true).inspect_err(|_| unsafe {
CloseHandle(stdin_read);
CloseHandle(stdin_write);
})?;
let (stderr_read, stderr_write) = create_pipe(false, true).inspect_err(|_| unsafe {
CloseHandle(stdin_read);
CloseHandle(stdin_write);
CloseHandle(stdout_read);
CloseHandle(stdout_write);
})?;
let pi = unsafe {
process::create_process_as_user(
restricted_token,
&argv,
workspace,
&env_map,
(stdin_read, stdout_write, stderr_write),
)
};
unsafe {
CloseHandle(stdin_read);
CloseHandle(stdin_write);
CloseHandle(stdout_write);
CloseHandle(stderr_write);
}
let pi = match pi {
Ok(pi) => pi,
Err(err) => {
unsafe {
CloseHandle(stdout_read);
CloseHandle(stderr_read);
}
return Err(err);
}
};
let _process_guard = ProcessHandles {
process: pi.hProcess,
thread: pi.hThread,
};
let stdout_buf = Arc::new(Mutex::new(Vec::<u8>::new()));
let stderr_buf = Arc::new(Mutex::new(Vec::<u8>::new()));
let stdout_overflow = Arc::new(AtomicBool::new(false));
let stderr_overflow = Arc::new(AtomicBool::new(false));
let stdout_handle = spawn_pipe_reader(
stdout_read,
Arc::clone(&stdout_buf),
Arc::clone(&stdout_overflow),
);
let stderr_handle = spawn_pipe_reader(
stderr_read,
Arc::clone(&stderr_buf),
Arc::clone(&stderr_overflow),
);
let start = Instant::now();
let mut termination: Option<TerminationReason> = None;
loop {
let wait = unsafe { WaitForSingleObject(pi.hProcess, 0) };
if wait == WAIT_OBJECT_0 {
break;
}
if wait != WAIT_TIMEOUT {
termination = Some(TerminationReason::Timeout);
break;
}
if interrupt_flag.load(Ordering::SeqCst) {
termination = Some(TerminationReason::Interrupt);
break;
}
if start.elapsed() > BASH_COMMAND_TIMEOUT {
termination = Some(TerminationReason::Timeout);
break;
}
if stdout_overflow.load(Ordering::SeqCst) {
termination = Some(TerminationReason::StdoutCapExceeded);
break;
}
if stderr_overflow.load(Ordering::SeqCst) {
termination = Some(TerminationReason::StderrCapExceeded);
break;
}
thread::sleep(SUPERVISOR_POLL_INTERVAL);
}
if termination.is_some() {
unsafe {
TerminateProcess(pi.hProcess, 1);
WaitForSingleObject(pi.hProcess, TERMINATE_WAIT_MS);
}
}
let _ = stdout_handle.join();
let _ = stderr_handle.join();
if termination.is_none() {
if stdout_overflow.load(Ordering::SeqCst) {
termination = Some(TerminationReason::StdoutCapExceeded);
} else if stderr_overflow.load(Ordering::SeqCst) {
termination = Some(TerminationReason::StderrCapExceeded);
}
}
let mut exit_code: u32 = 0;
unsafe {
GetExitCodeProcess(pi.hProcess, &mut exit_code);
}
let stdout = drain_into_vec(stdout_buf);
let stderr = drain_into_vec(stderr_buf);
Ok(SupervisedOutput {
stdout,
stderr,
exit_code: Some(exit_code as i32),
terminated_for: termination,
})
}
fn build_child_env(extra_path_dir: Option<&Path>) -> HashMap<String, String> {
let mut env: HashMap<String, String> = std::env::vars().collect();
if let Some(extra) = extra_path_dir {
let original = env.remove("PATH").unwrap_or_default();
let mut dirs: Vec<PathBuf> = vec![extra.to_path_buf()];
dirs.extend(std::env::split_paths(&original));
if let Ok(joined) = std::env::join_paths(dirs) {
env.insert("PATH".to_string(), joined.to_string_lossy().into_owned());
} else {
env.insert("PATH".to_string(), original);
}
}
let cygwin = env.remove("CYGWIN").unwrap_or_default();
env.insert("CYGWIN".to_string(), merge_token(&cygwin, "nontsec"));
let msys = env.remove("MSYS").unwrap_or_default();
env.insert("MSYS".to_string(), merge_token(&msys, "nontsec"));
env
}
fn merge_token(existing: &str, token: &str) -> String {
if existing.split_whitespace().any(|t| t == token) {
existing.to_string()
} else if existing.is_empty() {
token.to_string()
} else {
format!("{existing} {token}")
}
}
fn create_pipe(inherit_read: bool, inherit_write: bool) -> io::Result<(HANDLE, HANDLE)> {
let mut read: HANDLE = ptr::null_mut();
let mut write: HANDLE = ptr::null_mut();
let ok = unsafe { CreatePipe(&mut read, &mut write, ptr::null_mut(), 0) };
if ok == 0 {
return Err(io::Error::from_raw_os_error(
unsafe { GetLastError() } as i32
));
}
unsafe {
if inherit_read {
SetHandleInformation(read, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
}
if inherit_write {
SetHandleInformation(write, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
}
}
Ok((read, write))
}
fn spawn_pipe_reader(
handle: HANDLE,
buf: Arc<Mutex<Vec<u8>>>,
overflow: Arc<AtomicBool>,
) -> thread::JoinHandle<()> {
let raw: usize = handle as usize;
thread::spawn(move || {
let handle = raw as HANDLE;
let mut chunk = [0u8; BASH_READ_CHUNK_BYTES];
loop {
let mut read: u32 = 0;
let ok = unsafe {
ReadFile(
handle,
chunk.as_mut_ptr(),
chunk.len() as u32,
&mut read,
ptr::null_mut(),
)
};
if ok == 0 || read == 0 {
break;
}
if overflow.load(Ordering::Relaxed) {
continue;
}
let Ok(mut stored) = buf.lock() else {
break;
};
let remaining = MAX_BASH_OUTPUT_BYTES.saturating_sub(stored.len());
if remaining == 0 {
overflow.store(true, Ordering::SeqCst);
continue;
}
let take = (read as usize).min(remaining);
stored.extend_from_slice(&chunk[..take]);
if take < read as usize {
overflow.store(true, Ordering::SeqCst);
}
}
unsafe {
CloseHandle(handle);
}
})
}
fn drain_into_vec(buf: Arc<Mutex<Vec<u8>>>) -> Vec<u8> {
Arc::try_unwrap(buf)
.map(|inner| inner.into_inner().unwrap_or_default())
.unwrap_or_else(|shared| shared.lock().map(|guard| guard.clone()).unwrap_or_default())
}
struct TokenHandle(HANDLE);
impl Drop for TokenHandle {
fn drop(&mut self) {
if !self.0.is_null() {
unsafe {
CloseHandle(self.0);
}
}
}
}
struct ProcessHandles {
process: HANDLE,
thread: HANDLE,
}
impl Drop for ProcessHandles {
fn drop(&mut self) {
unsafe {
if !self.thread.is_null() {
CloseHandle(self.thread);
}
if !self.process.is_null() {
CloseHandle(self.process);
}
}
}
}