800 lines
26 KiB
Rust
800 lines
26 KiB
Rust
//! Windows Restricted Token sandbox helper binary.
|
|
//!
|
|
//! Usage:
|
|
//! ```bash
|
|
//! # Capture mode: run command, capture output, print JSON result
|
|
//! qiming-sandbox-helper run \
|
|
//! --mode <read-only|workspace-write> \
|
|
//! --cwd <path> \
|
|
//! [--home <path>] \
|
|
//! [--policy-json <json>] \
|
|
//! -- <command> [args...]
|
|
//!
|
|
//! # Proxy mode: run command as persistent stdio proxy (stdin/stdout forwarded)
|
|
//! qiming-sandbox-helper serve \
|
|
//! --mode <read-only|workspace-write> \
|
|
//! --cwd <path> \
|
|
//! [--home <path>] \
|
|
//! [--policy-json <json>] \
|
|
//! -- <command> [args...]
|
|
//! ```
|
|
|
|
#![cfg(target_os = "windows")]
|
|
|
|
mod acl;
|
|
mod allow;
|
|
mod audit;
|
|
mod cap;
|
|
mod env;
|
|
mod logging;
|
|
mod policy;
|
|
mod token;
|
|
mod winutil;
|
|
|
|
use acl::{add_allow_ace, add_deny_write_ace, allow_null_device, revoke_ace};
|
|
use allow::compute_allow_paths;
|
|
use audit::apply_world_writable_scan_and_denies;
|
|
use cap::{cap_sid_file, load_or_create_cap_sids};
|
|
use clap::{Parser, ValueEnum};
|
|
use env::{apply_no_network_to_env, ensure_non_interactive_pager, normalize_null_device_env};
|
|
use logging::{debug_log, log_failure, log_start, log_success};
|
|
use policy::SandboxPolicy;
|
|
use std::collections::HashMap;
|
|
use std::ffi::c_void;
|
|
use std::fs;
|
|
use std::io::{self, Read, Write};
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
use std::ptr;
|
|
use std::thread;
|
|
use std::sync::mpsc;
|
|
use std::time::Duration;
|
|
use token::{convert_string_sid_to_sid, create_restricted_token};
|
|
use winutil::{format_last_error, to_wide};
|
|
use windows_sys::Win32::Foundation::{CloseHandle, GetLastError, SetHandleInformation, HANDLE, HANDLE_FLAG_INHERIT};
|
|
use windows_sys::Win32::System::Pipes::CreatePipe;
|
|
use windows_sys::Win32::System::Threading::{
|
|
CreateProcessAsUserW, GetExitCodeProcess, WaitForSingleObject, CREATE_UNICODE_ENVIRONMENT,
|
|
INFINITE, PROCESS_INFORMATION, STARTF_USESTDHANDLES, STARTUPINFOW,
|
|
};
|
|
|
|
// ============================================================================
|
|
// CLI
|
|
// ============================================================================
|
|
|
|
#[derive(ValueEnum, Clone, Debug)]
|
|
enum Mode {
|
|
ReadOnly,
|
|
WorkspaceWrite,
|
|
}
|
|
|
|
#[derive(Parser, Debug)]
|
|
#[command(name = "qiming-sandbox-helper")]
|
|
#[command(author = "Qiming Agent")]
|
|
#[command(version = "0.1.0")]
|
|
struct Args {
|
|
#[command(subcommand)]
|
|
subcommand: Subcommand,
|
|
}
|
|
|
|
#[derive(Parser, Debug)]
|
|
enum Subcommand {
|
|
/// Run a command in the sandbox, capture output, print JSON result.
|
|
Run(RunArgs),
|
|
/// Run a command in the sandbox as a persistent stdio proxy.
|
|
/// Stdin/stdout/stderr are forwarded bidirectionally.
|
|
///
|
|
/// By default uses a token without WRITE_RESTRICTED for maximum
|
|
/// compatibility. Pass --write-restricted to enable filesystem
|
|
/// write protection via restricting SIDs + DACL ACEs (needed for
|
|
/// strict/compat sandbox modes).
|
|
Serve(ServeArgs),
|
|
}
|
|
|
|
#[derive(Parser, Debug)]
|
|
struct ServeArgs {
|
|
#[command(flatten)]
|
|
common: CommonArgs,
|
|
/// Enable WRITE_RESTRICTED on the sandbox token.
|
|
/// When set, write access is restricted to paths with ALLOW ACEs
|
|
/// for the restricting SIDs (capability, logon, everyone).
|
|
/// Required for strict/compat sandbox modes to enforce filesystem
|
|
/// write protection.
|
|
#[arg(long, default_value_t = false)]
|
|
write_restricted: bool,
|
|
}
|
|
|
|
#[derive(Parser, Debug)]
|
|
struct RunArgs {
|
|
#[command(flatten)]
|
|
common: CommonArgs,
|
|
/// Skip WRITE_RESTRICTED on the sandbox token. Allows child processes
|
|
/// (e.g. Git Bash) to create pipes and modify their own token DACL.
|
|
/// Filesystem write protection is still enforced by DACL ACEs.
|
|
#[arg(long, default_value_t = false)]
|
|
no_write_restricted: bool,
|
|
}
|
|
|
|
#[derive(Parser, Debug)]
|
|
struct CommonArgs {
|
|
#[arg(long)]
|
|
mode: Mode,
|
|
#[arg(long)]
|
|
cwd: PathBuf,
|
|
#[arg(long, env = "QIMING_SANDBOX_HOME")]
|
|
home: Option<PathBuf>,
|
|
#[arg(long)]
|
|
policy_json: Option<String>,
|
|
#[arg(last = true)]
|
|
command: Vec<String>,
|
|
}
|
|
|
|
// ============================================================================
|
|
// Shared helpers
|
|
// ============================================================================
|
|
|
|
type PipeHandles = ((HANDLE, HANDLE), (HANDLE, HANDLE), (HANDLE, HANDLE));
|
|
|
|
fn should_apply_network_block(policy: &SandboxPolicy) -> bool {
|
|
!policy.has_full_network_access()
|
|
}
|
|
|
|
fn ensure_dir(p: &Path) -> anyhow::Result<()> {
|
|
if let Some(d) = p.parent() {
|
|
std::fs::create_dir_all(d)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn ensure_sandbox_home_exists(p: &Path) -> anyhow::Result<()> {
|
|
std::fs::create_dir_all(p)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn make_env_block(env: &HashMap<String, String>) -> Vec<u16> {
|
|
let mut items: Vec<(String, String)> = env.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
|
|
items.sort_by(|a, b| a.0.to_uppercase().cmp(&b.0.to_uppercase()).then(a.0.cmp(&b.0)));
|
|
let mut w: Vec<u16> = Vec::new();
|
|
for (k, v) in items {
|
|
let mut s = to_wide(format!("{}={}", k, v));
|
|
s.pop();
|
|
w.extend_from_slice(&s);
|
|
w.push(0);
|
|
}
|
|
w.push(0);
|
|
w
|
|
}
|
|
|
|
fn quote_windows_arg(arg: &str) -> String {
|
|
let needs_quotes =
|
|
arg.is_empty() || arg.chars().any(|c| matches!(c, ' ' | '\t' | '\n' | '\r' | '"'));
|
|
if !needs_quotes {
|
|
return arg.to_string();
|
|
}
|
|
let mut quoted = String::with_capacity(arg.len() + 2);
|
|
quoted.push('"');
|
|
let mut backslashes = 0usize;
|
|
for ch in arg.chars() {
|
|
match ch {
|
|
'\\' => backslashes += 1,
|
|
'"' => {
|
|
quoted.push_str(&"\\".repeat(backslashes * 2 + 1));
|
|
quoted.push('"');
|
|
backslashes = 0;
|
|
}
|
|
_ => {
|
|
if backslashes > 0 {
|
|
quoted.push_str(&"\\".repeat(backslashes));
|
|
backslashes = 0;
|
|
}
|
|
quoted.push(ch);
|
|
}
|
|
}
|
|
}
|
|
if backslashes > 0 {
|
|
quoted.push_str(&"\\".repeat(backslashes * 2));
|
|
}
|
|
quoted.push('"');
|
|
quoted
|
|
}
|
|
|
|
unsafe fn setup_stdio_pipes() -> io::Result<PipeHandles> {
|
|
let mut in_r: HANDLE = 0;
|
|
let mut in_w: HANDLE = 0;
|
|
let mut out_r: HANDLE = 0;
|
|
let mut out_w: HANDLE = 0;
|
|
let mut err_r: HANDLE = 0;
|
|
let mut err_w: HANDLE = 0;
|
|
|
|
if CreatePipe(&mut in_r, &mut in_w, ptr::null_mut(), 0) == 0 {
|
|
return Err(io::Error::from_raw_os_error(GetLastError() as i32));
|
|
}
|
|
if CreatePipe(&mut out_r, &mut out_w, ptr::null_mut(), 0) == 0 {
|
|
return Err(io::Error::from_raw_os_error(GetLastError() as i32));
|
|
}
|
|
if CreatePipe(&mut err_r, &mut err_w, ptr::null_mut(), 0) == 0 {
|
|
return Err(io::Error::from_raw_os_error(GetLastError() as i32));
|
|
}
|
|
if SetHandleInformation(in_r, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) == 0 {
|
|
return Err(io::Error::from_raw_os_error(GetLastError() as i32));
|
|
}
|
|
if SetHandleInformation(out_w, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) == 0 {
|
|
return Err(io::Error::from_raw_os_error(GetLastError() as i32));
|
|
}
|
|
if SetHandleInformation(err_w, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) == 0 {
|
|
return Err(io::Error::from_raw_os_error(GetLastError() as i32));
|
|
}
|
|
Ok(((in_r, in_w), (out_r, out_w), (err_r, err_w)))
|
|
}
|
|
|
|
/// Helper to close an array of HANDLEs, ignoring nulls.
|
|
unsafe fn close_handles(handles: &[HANDLE]) {
|
|
for &h in handles {
|
|
if h != 0 {
|
|
CloseHandle(h);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Shared sandbox context (token + ACL + env setup)
|
|
// ============================================================================
|
|
|
|
/// Holds the state needed to spawn a sandboxed child process.
|
|
struct SandboxContext {
|
|
/// Modified environment block for the child.
|
|
env_map: HashMap<String, String>,
|
|
/// Restricted token for CreateProcessAsUserW.
|
|
h_token: HANDLE,
|
|
/// Whether ACEs should persist (workspace-write mode).
|
|
persist_aces: bool,
|
|
/// ACE guards to revoke on cleanup (only when !persist_aces).
|
|
guards: Vec<(PathBuf, *mut c_void)>,
|
|
/// Logging directory.
|
|
logs_base_dir: Option<PathBuf>,
|
|
/// Full command line (for logging).
|
|
command: Vec<String>,
|
|
}
|
|
|
|
impl SandboxContext {
|
|
/// Set up sandbox policy, restricted token, ACLs, and environment.
|
|
fn setup(
|
|
policy_json_or_preset: &str,
|
|
sandbox_policy_cwd: &Path,
|
|
home: &Path,
|
|
command: Vec<String>,
|
|
cwd: &Path,
|
|
args: Vec<String>,
|
|
write_restricted: bool,
|
|
) -> anyhow::Result<Self> {
|
|
let mut env_map: HashMap<String, String> = std::env::vars().collect();
|
|
let mut command = command;
|
|
command.extend(args);
|
|
|
|
let policy = policy::parse_policy(policy_json_or_preset)?;
|
|
if should_apply_network_block(&policy) {
|
|
normalize_null_device_env(&mut env_map);
|
|
ensure_non_interactive_pager(&mut env_map);
|
|
apply_no_network_to_env(&mut env_map)?;
|
|
} else {
|
|
normalize_null_device_env(&mut env_map);
|
|
ensure_non_interactive_pager(&mut env_map);
|
|
}
|
|
ensure_sandbox_home_exists(home)?;
|
|
|
|
let current_dir = cwd.to_path_buf();
|
|
let logs_base_dir = Some(home.to_path_buf());
|
|
log_start(&command, logs_base_dir.as_deref());
|
|
|
|
let cap_sid_path = cap_sid_file(home);
|
|
let is_workspace_write = matches!(&policy, SandboxPolicy::WorkspaceWrite { .. });
|
|
|
|
let (h_token, psid): (HANDLE, *mut c_void) = unsafe {
|
|
let caps = load_or_create_cap_sids(home);
|
|
ensure_dir(&cap_sid_path)?;
|
|
fs::write(&cap_sid_path, serde_json::to_string(&caps)?)?;
|
|
|
|
let cap_sid_str = if is_workspace_write { &caps.workspace } else { &caps.readonly };
|
|
let psid = convert_string_sid_to_sid(cap_sid_str).unwrap();
|
|
|
|
create_restricted_token(psid, write_restricted)?
|
|
};
|
|
|
|
unsafe {
|
|
if is_workspace_write {
|
|
if let Ok(base) = token::get_current_token_for_restriction() {
|
|
if let Ok(bytes) = token::get_logon_sid_bytes(base) {
|
|
let mut tmp = bytes.clone();
|
|
let psid2 = tmp.as_mut_ptr() as *mut c_void;
|
|
allow_null_device(psid2);
|
|
}
|
|
CloseHandle(base);
|
|
}
|
|
}
|
|
}
|
|
|
|
let persist_aces = is_workspace_write;
|
|
let allow_deny = compute_allow_paths(&policy, sandbox_policy_cwd, ¤t_dir, &env_map);
|
|
let mut guards: Vec<(PathBuf, *mut c_void)> = Vec::new();
|
|
|
|
unsafe {
|
|
for p in &allow_deny.allow {
|
|
if let Ok(added) = add_allow_ace(p, psid) {
|
|
if added && !persist_aces {
|
|
guards.push((p.clone(), psid));
|
|
}
|
|
}
|
|
}
|
|
for p in &allow_deny.deny {
|
|
if let Ok(added) = add_deny_write_ace(p, psid) {
|
|
if added && !persist_aces {
|
|
guards.push((p.clone(), psid));
|
|
}
|
|
}
|
|
}
|
|
allow_null_device(psid);
|
|
}
|
|
|
|
let _ = apply_world_writable_scan_and_denies(
|
|
home, ¤t_dir, &env_map, &policy, logs_base_dir.as_deref(),
|
|
);
|
|
|
|
// Mark the child as running inside a sandbox so it skips nested sandboxing.
|
|
env_map.insert("QIMING_IN_SANDBOX".to_string(), "1".to_string());
|
|
|
|
Ok(Self {
|
|
env_map,
|
|
h_token,
|
|
persist_aces,
|
|
guards,
|
|
logs_base_dir,
|
|
command,
|
|
})
|
|
}
|
|
|
|
/// Spawn the child process with restricted token and the given stdio handles.
|
|
unsafe fn spawn_child(
|
|
&self,
|
|
cwd: &Path,
|
|
child_stdin_r: HANDLE,
|
|
child_stdout_w: HANDLE,
|
|
child_stderr_w: HANDLE,
|
|
all_pipe_handles: &[HANDLE],
|
|
) -> anyhow::Result<PROCESS_INFORMATION> {
|
|
let mut si: STARTUPINFOW = std::mem::zeroed();
|
|
si.cb = std::mem::size_of::<STARTUPINFOW>() as u32;
|
|
si.dwFlags |= STARTF_USESTDHANDLES;
|
|
si.hStdInput = child_stdin_r;
|
|
si.hStdOutput = child_stdout_w;
|
|
si.hStdError = child_stderr_w;
|
|
|
|
let cmdline_str = self.command
|
|
.iter()
|
|
.map(|a| quote_windows_arg(a))
|
|
.collect::<Vec<_>>()
|
|
.join(" ");
|
|
let mut cmdline: Vec<u16> = to_wide(&cmdline_str);
|
|
let env_block = make_env_block(&self.env_map);
|
|
let desktop = to_wide("Winsta0\\Default");
|
|
si.lpDesktop = desktop.as_ptr() as *mut u16;
|
|
|
|
let mut pi: PROCESS_INFORMATION = std::mem::zeroed();
|
|
|
|
let spawn_res = CreateProcessAsUserW(
|
|
self.h_token,
|
|
ptr::null(),
|
|
cmdline.as_mut_ptr(),
|
|
ptr::null_mut(),
|
|
ptr::null_mut(),
|
|
1,
|
|
CREATE_UNICODE_ENVIRONMENT,
|
|
env_block.as_ptr() as *mut c_void,
|
|
to_wide(cwd).as_ptr(),
|
|
&si,
|
|
&mut pi,
|
|
);
|
|
|
|
if spawn_res == 0 {
|
|
let err = GetLastError() as i32;
|
|
let msg = format!(
|
|
"CreateProcessAsUserW failed: {} ({}) | cwd={} | cmd={} | env_u16_len={}",
|
|
err,
|
|
format_last_error(err),
|
|
cwd.display(),
|
|
cmdline_str,
|
|
env_block.len(),
|
|
);
|
|
debug_log(&msg, self.logs_base_dir.as_deref());
|
|
close_handles(all_pipe_handles);
|
|
CloseHandle(self.h_token);
|
|
return Err(anyhow::anyhow!("CreateProcessAsUserW failed: {}", err));
|
|
}
|
|
|
|
Ok(pi)
|
|
}
|
|
|
|
/// Log the result and revoke temporary ACEs.
|
|
fn cleanup(&mut self, exit_code: i32) {
|
|
if exit_code == 0 {
|
|
log_success(&self.command, self.logs_base_dir.as_deref());
|
|
} else {
|
|
log_failure(&self.command, &format!("exit code {}", exit_code), self.logs_base_dir.as_deref());
|
|
}
|
|
|
|
if !self.persist_aces {
|
|
unsafe {
|
|
for (p, sid) in &self.guards {
|
|
revoke_ace(p, *sid);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Resolve common args
|
|
// ============================================================================
|
|
|
|
fn resolve_home(args: &CommonArgs) -> PathBuf {
|
|
args.home.clone().unwrap_or_else(|| {
|
|
dirs_next::home_dir()
|
|
.unwrap_or_else(|| PathBuf::from("C:\\Users\\Default"))
|
|
.join(".qiming-sandbox")
|
|
})
|
|
}
|
|
|
|
fn resolve_policy(args: &CommonArgs) -> String {
|
|
if let Some(json) = &args.policy_json {
|
|
json.clone()
|
|
} else {
|
|
match args.mode {
|
|
Mode::ReadOnly => "read-only".to_string(),
|
|
Mode::WorkspaceWrite => "workspace-write".to_string(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn split_command(args: &CommonArgs) -> anyhow::Result<(String, Vec<String>)> {
|
|
if args.command.is_empty() {
|
|
anyhow::bail!("no command provided; use `-- -- <command> [args...]`");
|
|
}
|
|
Ok((args.command[0].clone(), args.command[1..].to_vec()))
|
|
}
|
|
|
|
// ============================================================================
|
|
// Capture mode (run subcommand)
|
|
// ============================================================================
|
|
|
|
#[derive(Debug)]
|
|
pub struct CaptureResult {
|
|
pub exit_code: i32,
|
|
pub stdout: Vec<u8>,
|
|
pub stderr: Vec<u8>,
|
|
pub timed_out: bool,
|
|
}
|
|
|
|
pub fn run_sandbox_capture(
|
|
policy_json_or_preset: &str,
|
|
sandbox_policy_cwd: &Path,
|
|
home: &Path,
|
|
command: Vec<String>,
|
|
cwd: &Path,
|
|
args: Vec<String>,
|
|
timeout_ms: Option<u64>,
|
|
write_restricted: bool,
|
|
) -> anyhow::Result<CaptureResult> {
|
|
let mut ctx = SandboxContext::setup(policy_json_or_preset, sandbox_policy_cwd, home, command, cwd, args, write_restricted)?;
|
|
|
|
let (stdin_pair, stdout_pair, stderr_pair) = unsafe { setup_stdio_pipes()? };
|
|
let ((in_r, in_w), (out_r, out_w), (err_r, err_w)) = (stdin_pair, stdout_pair, stderr_pair);
|
|
|
|
let all_pipes = [in_r, in_w, out_r, out_w, err_r, err_w];
|
|
let pi = unsafe { ctx.spawn_child(cwd, in_r, out_w, err_w, &all_pipes)? };
|
|
|
|
// Close all child-ends and the stdin write-end (capture mode doesn't forward stdin)
|
|
unsafe {
|
|
CloseHandle(in_r); // child's stdin read end
|
|
CloseHandle(in_w); // parent's stdin write end (not forwarding stdin in capture mode)
|
|
CloseHandle(out_w); // child's stdout write end
|
|
CloseHandle(err_w); // child's stderr write end
|
|
}
|
|
|
|
// Read stdout/stderr in background threads
|
|
let (tx_out, rx_out) = mpsc::channel::<Vec<u8>>();
|
|
let (tx_err, rx_err) = mpsc::channel::<Vec<u8>>();
|
|
|
|
let t_out = thread::spawn(move || {
|
|
let mut buf = Vec::new();
|
|
let mut tmp = [0u8; 8192];
|
|
loop {
|
|
let mut read_bytes: u32 = 0;
|
|
let ok = unsafe {
|
|
windows_sys::Win32::Storage::FileSystem::ReadFile(
|
|
out_r,
|
|
tmp.as_mut_ptr(),
|
|
tmp.len() as u32,
|
|
&mut read_bytes,
|
|
ptr::null_mut(),
|
|
)
|
|
};
|
|
if ok == 0 || read_bytes == 0 {
|
|
break;
|
|
}
|
|
buf.extend_from_slice(&tmp[..read_bytes as usize]);
|
|
}
|
|
let _ = tx_out.send(buf);
|
|
});
|
|
|
|
let t_err = thread::spawn(move || {
|
|
let mut buf = Vec::new();
|
|
let mut tmp = [0u8; 8192];
|
|
loop {
|
|
let mut read_bytes: u32 = 0;
|
|
let ok = unsafe {
|
|
windows_sys::Win32::Storage::FileSystem::ReadFile(
|
|
err_r,
|
|
tmp.as_mut_ptr(),
|
|
tmp.len() as u32,
|
|
&mut read_bytes,
|
|
ptr::null_mut(),
|
|
)
|
|
};
|
|
if ok == 0 || read_bytes == 0 {
|
|
break;
|
|
}
|
|
buf.extend_from_slice(&tmp[..read_bytes as usize]);
|
|
}
|
|
let _ = tx_err.send(buf);
|
|
});
|
|
|
|
let timeout = timeout_ms.unwrap_or(u64::MAX).min(u64::from(INFINITE)) as u32;
|
|
let res = unsafe { WaitForSingleObject(pi.hProcess, timeout) };
|
|
let timed_out = res == 0x0000_0102;
|
|
|
|
let mut exit_code_u32: u32 = 1;
|
|
if !timed_out {
|
|
unsafe { GetExitCodeProcess(pi.hProcess, &mut exit_code_u32); }
|
|
} else {
|
|
unsafe {
|
|
windows_sys::Win32::System::Threading::TerminateProcess(pi.hProcess, 1);
|
|
}
|
|
}
|
|
|
|
unsafe {
|
|
close_handles(&[pi.hThread, pi.hProcess, ctx.h_token]);
|
|
}
|
|
|
|
let _ = t_out.join();
|
|
let _ = t_err.join();
|
|
let stdout = rx_out.recv().unwrap_or_default();
|
|
let stderr = rx_err.recv().unwrap_or_default();
|
|
let exit_code = if timed_out { 128 + 64 } else { exit_code_u32 as i32 };
|
|
|
|
ctx.cleanup(exit_code);
|
|
|
|
Ok(CaptureResult { exit_code, stdout, stderr, timed_out })
|
|
}
|
|
|
|
// ============================================================================
|
|
// Proxy mode (serve subcommand)
|
|
// ============================================================================
|
|
|
|
/// Result of a proxy-mode sandbox run.
|
|
pub struct ProxyResult {
|
|
pub exit_code: i32,
|
|
}
|
|
|
|
/// Run a sandboxed process with bidirectional stdio forwarding.
|
|
pub fn run_sandbox_proxy(
|
|
policy_json_or_preset: &str,
|
|
sandbox_policy_cwd: &Path,
|
|
home: &Path,
|
|
command: Vec<String>,
|
|
cwd: &Path,
|
|
args: Vec<String>,
|
|
write_restricted: bool,
|
|
) -> anyhow::Result<ProxyResult> {
|
|
// When write_restricted=true, the restricted token gets WRITE_RESTRICTED
|
|
// flag + restricting SIDs, so only paths with explicit ALLOW ACEs are
|
|
// writable. When false, only DACL-level token restrictions apply
|
|
// (permissive mode — no filesystem write protection).
|
|
debug_log(
|
|
&format!(
|
|
"serve mode: write_restricted={} ({}), policy={}",
|
|
write_restricted,
|
|
if write_restricted { "STRICT/COMPAT — filesystem writes restricted to ALLOW ACEs" } else { "PERMISSIVE — no filesystem write protection" },
|
|
policy_json_or_preset,
|
|
),
|
|
None,
|
|
);
|
|
let mut ctx = SandboxContext::setup(policy_json_or_preset, sandbox_policy_cwd, home, command, cwd, args, write_restricted)?;
|
|
|
|
let (stdin_pair, stdout_pair, stderr_pair) = unsafe { setup_stdio_pipes()? };
|
|
let ((child_stdin_r, child_stdin_w), (child_stdout_r, child_stdout_w), (child_stderr_r, child_stderr_w)) =
|
|
(stdin_pair, stdout_pair, stderr_pair);
|
|
|
|
let all_pipes = [child_stdin_r, child_stdin_w, child_stdout_r, child_stdout_w, child_stderr_r, child_stderr_w];
|
|
let pi = unsafe { ctx.spawn_child(cwd, child_stdin_r, child_stdout_w, child_stderr_w, &all_pipes)? };
|
|
|
|
// Close child-side handles that the parent doesn't need
|
|
unsafe {
|
|
CloseHandle(child_stdin_r); // child's stdin read end
|
|
CloseHandle(child_stdout_w); // child's stdout write end
|
|
CloseHandle(child_stderr_w); // child's stderr write end
|
|
}
|
|
|
|
// --- Bidirectional forwarding threads ---
|
|
// stdin: parent_stdin → child_stdin_w
|
|
let stdin_writer_w = child_stdin_w;
|
|
let (stdin_done_tx, stdin_done_rx) = mpsc::channel::<()>();
|
|
let stdin_thread = thread::spawn(move || {
|
|
let mut buf = [0u8; 8192];
|
|
let mut stdin_handle = io::stdin();
|
|
loop {
|
|
match stdin_handle.read(&mut buf) {
|
|
Ok(0) => break, // EOF
|
|
Ok(n) => {
|
|
let mut written = 0usize;
|
|
while written < n {
|
|
let mut bytes_written: u32 = 0;
|
|
let ok = unsafe {
|
|
windows_sys::Win32::Storage::FileSystem::WriteFile(
|
|
stdin_writer_w,
|
|
buf[written..].as_ptr(),
|
|
(n - written) as u32,
|
|
&mut bytes_written,
|
|
ptr::null_mut(),
|
|
)
|
|
};
|
|
if ok == 0 || bytes_written == 0 {
|
|
break;
|
|
}
|
|
written += bytes_written as usize;
|
|
}
|
|
if written < n {
|
|
break; // pipe broken
|
|
}
|
|
}
|
|
Err(_) => break,
|
|
}
|
|
}
|
|
unsafe { CloseHandle(stdin_writer_w); }
|
|
let _ = stdin_done_tx.send(());
|
|
});
|
|
|
|
// stdout: child_stdout_r → parent_stdout
|
|
let stdout_reader_r = child_stdout_r;
|
|
let stdout_thread = thread::spawn(move || {
|
|
let mut buf = [0u8; 8192];
|
|
let mut stdout_handle = io::stdout();
|
|
loop {
|
|
let mut read_bytes: u32 = 0;
|
|
let ok = unsafe {
|
|
windows_sys::Win32::Storage::FileSystem::ReadFile(
|
|
stdout_reader_r,
|
|
buf.as_mut_ptr(),
|
|
buf.len() as u32,
|
|
&mut read_bytes,
|
|
ptr::null_mut(),
|
|
)
|
|
};
|
|
if ok == 0 || read_bytes == 0 {
|
|
break;
|
|
}
|
|
if stdout_handle.write_all(&buf[..read_bytes as usize]).is_err() {
|
|
break;
|
|
}
|
|
let _ = stdout_handle.flush();
|
|
}
|
|
unsafe { CloseHandle(stdout_reader_r); }
|
|
});
|
|
|
|
// stderr: child_stderr_r → parent_stderr
|
|
let stderr_reader_r = child_stderr_r;
|
|
let stderr_thread = thread::spawn(move || {
|
|
let mut buf = [0u8; 8192];
|
|
let mut stderr_handle = io::stderr();
|
|
loop {
|
|
let mut read_bytes: u32 = 0;
|
|
let ok = unsafe {
|
|
windows_sys::Win32::Storage::FileSystem::ReadFile(
|
|
stderr_reader_r,
|
|
buf.as_mut_ptr(),
|
|
buf.len() as u32,
|
|
&mut read_bytes,
|
|
ptr::null_mut(),
|
|
)
|
|
};
|
|
if ok == 0 || read_bytes == 0 {
|
|
break;
|
|
}
|
|
if stderr_handle.write_all(&buf[..read_bytes as usize]).is_err() {
|
|
break;
|
|
}
|
|
let _ = stderr_handle.flush();
|
|
}
|
|
unsafe { CloseHandle(stderr_reader_r); }
|
|
});
|
|
|
|
// Wait for child process to exit
|
|
unsafe { WaitForSingleObject(pi.hProcess, INFINITE) };
|
|
|
|
let mut exit_code_u32: u32 = 1;
|
|
unsafe { GetExitCodeProcess(pi.hProcess, &mut exit_code_u32); }
|
|
|
|
// Close child process handles so stdout/stderr forwarding threads can unblock
|
|
unsafe {
|
|
close_handles(&[pi.hThread, pi.hProcess, ctx.h_token]);
|
|
}
|
|
|
|
// Wait for stdout/stderr threads to drain
|
|
let _ = stdout_thread.join();
|
|
let _ = stderr_thread.join();
|
|
|
|
// stdin thread may block on parent stdin — give it a short window then detach
|
|
if stdin_done_rx.recv_timeout(Duration::from_secs(2)).is_err() {
|
|
drop(stdin_thread);
|
|
}
|
|
|
|
let exit_code = exit_code_u32 as i32;
|
|
ctx.cleanup(exit_code);
|
|
|
|
Ok(ProxyResult { exit_code })
|
|
}
|
|
|
|
// ============================================================================
|
|
// main
|
|
// ============================================================================
|
|
|
|
fn main() -> anyhow::Result<()> {
|
|
let args = Args::parse();
|
|
|
|
match args.subcommand {
|
|
Subcommand::Run(run) => {
|
|
let home = resolve_home(&run.common);
|
|
let policy = resolve_policy(&run.common);
|
|
let (cmd, cmd_args) = split_command(&run.common)?;
|
|
let write_restricted = !run.no_write_restricted;
|
|
|
|
let timeout_ms: u64 = 300_000;
|
|
let result = run_sandbox_capture(
|
|
&policy,
|
|
&run.common.cwd,
|
|
&home,
|
|
vec![cmd],
|
|
&run.common.cwd,
|
|
cmd_args,
|
|
Some(timeout_ms),
|
|
write_restricted,
|
|
)?;
|
|
|
|
println!(
|
|
"{{\"exit_code\":{},\"stdout\":{},\"stderr\":{},\"timed_out\":{}}}",
|
|
result.exit_code,
|
|
serde_json::to_string(&String::from_utf8_lossy(&result.stdout))?,
|
|
serde_json::to_string(&String::from_utf8_lossy(&result.stderr))?,
|
|
result.timed_out
|
|
);
|
|
}
|
|
Subcommand::Serve(serve) => {
|
|
let home = resolve_home(&serve.common);
|
|
let policy = resolve_policy(&serve.common);
|
|
let (cmd, cmd_args) = split_command(&serve.common)?;
|
|
|
|
let result = run_sandbox_proxy(
|
|
&policy,
|
|
&serve.common.cwd,
|
|
&home,
|
|
vec![cmd],
|
|
&serve.common.cwd,
|
|
cmd_args,
|
|
serve.write_restricted,
|
|
)?;
|
|
|
|
std::process::exit(result.exit_code);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|