use std::process::Command;
pub struct SanitizedChild {
pid: u32,
#[cfg(windows)]
handle: windows::OwnedHandle,
#[cfg(unix)]
child: std::process::Child,
}
impl SanitizedChild {
pub fn id(&self) -> u32 {
self.pid
}
pub fn kill(&mut self) -> std::io::Result<()> {
#[cfg(windows)]
{
windows::terminate(&self.handle)
}
#[cfg(unix)]
{
self.child.kill()
}
}
pub fn wait(&mut self) -> std::io::Result<i32> {
#[cfg(windows)]
{
windows::wait(&self.handle)
}
#[cfg(unix)]
{
let status = self.child.wait()?;
Ok(exit_code(status))
}
}
pub fn try_wait(&mut self) -> std::io::Result<Option<i32>> {
#[cfg(windows)]
{
windows::try_wait(&self.handle)
}
#[cfg(unix)]
{
Ok(self.child.try_wait()?.map(exit_code))
}
}
}
#[cfg(unix)]
fn exit_code(status: std::process::ExitStatus) -> i32 {
use std::os::unix::process::ExitStatusExt;
status
.code()
.unwrap_or_else(|| -status.signal().unwrap_or(1))
}
pub fn spawn(command: &mut Command) -> std::io::Result<SanitizedChild> {
#[cfg(windows)]
{
windows::spawn(command)
}
#[cfg(unix)]
{
unix::spawn(command)
}
}
#[cfg(windows)]
mod windows {
use std::ffi::{OsStr, OsString};
use std::os::windows::ffi::OsStrExt;
use std::process::Command;
use winapi::shared::minwindef::{BOOL, DWORD, FALSE, TRUE};
use winapi::um::fileapi::{CreateFileW, OPEN_EXISTING};
use winapi::um::handleapi::{CloseHandle, INVALID_HANDLE_VALUE};
use winapi::um::minwinbase::SECURITY_ATTRIBUTES;
use winapi::um::processthreadsapi::{
CreateProcessW, DeleteProcThreadAttributeList, GetExitCodeProcess,
InitializeProcThreadAttributeList, TerminateProcess, UpdateProcThreadAttribute,
LPPROC_THREAD_ATTRIBUTE_LIST, PROCESS_INFORMATION,
};
use winapi::um::synchapi::WaitForSingleObject;
use winapi::um::winbase::{
CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW, CREATE_UNICODE_ENVIRONMENT, DETACHED_PROCESS,
EXTENDED_STARTUPINFO_PRESENT, INFINITE, STARTF_USESTDHANDLES, STARTUPINFOEXW,
WAIT_OBJECT_0,
};
use winapi::um::winnt::{
FILE_SHARE_READ, FILE_SHARE_WRITE, GENERIC_READ, GENERIC_WRITE, HANDLE,
};
const PROC_THREAD_ATTRIBUTE_HANDLE_LIST: usize = 0x00020002;
const STILL_ACTIVE: u32 = 259;
pub struct OwnedHandle(HANDLE);
impl OwnedHandle {
pub fn as_raw(&self) -> HANDLE {
self.0
}
}
impl Drop for OwnedHandle {
fn drop(&mut self) {
if !self.0.is_null() && self.0 != INVALID_HANDLE_VALUE {
unsafe {
CloseHandle(self.0);
}
}
}
}
unsafe impl Send for OwnedHandle {}
unsafe impl Sync for OwnedHandle {}
fn open_nul(write: bool) -> std::io::Result<OwnedHandle> {
let path: Vec<u16> = OsStr::new("NUL")
.encode_wide()
.chain(std::iter::once(0))
.collect();
let mut sa: SECURITY_ATTRIBUTES = unsafe { std::mem::zeroed() };
sa.nLength = std::mem::size_of::<SECURITY_ATTRIBUTES>() as DWORD;
sa.bInheritHandle = TRUE as BOOL;
let access = if write { GENERIC_WRITE } else { GENERIC_READ };
let h = unsafe {
CreateFileW(
path.as_ptr(),
access,
FILE_SHARE_READ | FILE_SHARE_WRITE,
&mut sa as *mut SECURITY_ATTRIBUTES,
OPEN_EXISTING,
0,
std::ptr::null_mut(),
)
};
if h.is_null() || h == INVALID_HANDLE_VALUE {
return Err(std::io::Error::last_os_error());
}
Ok(OwnedHandle(h))
}
pub fn spawn(command: &mut Command) -> std::io::Result<super::SanitizedChild> {
let stdin = open_nul(false)?;
let stdout = open_nul(true)?;
let stderr = open_nul(true)?;
let mut cmdline = build_command_line(command.get_program(), command.get_args());
let envs: Vec<(OsString, Option<OsString>)> = command
.get_envs()
.map(|(k, v)| (k.to_os_string(), v.map(|v| v.to_os_string())))
.collect();
let env_block = if envs.is_empty() {
None
} else {
Some(build_env_block(envs))
};
let cwd_w: Option<Vec<u16>> = command.get_current_dir().map(|p| {
OsStr::new(p)
.encode_wide()
.chain(std::iter::once(0))
.collect()
});
let mut size: usize = 0;
unsafe {
InitializeProcThreadAttributeList(std::ptr::null_mut(), 1, 0, &mut size);
}
let mut attr_buf: Vec<u8> = vec![0; size];
let attr_list = attr_buf.as_mut_ptr() as LPPROC_THREAD_ATTRIBUTE_LIST;
let ok = unsafe { InitializeProcThreadAttributeList(attr_list, 1, 0, &mut size) };
if ok == FALSE {
return Err(std::io::Error::last_os_error());
}
let handle_list: [HANDLE; 3] = [stdin.as_raw(), stdout.as_raw(), stderr.as_raw()];
let ok = unsafe {
UpdateProcThreadAttribute(
attr_list,
0,
PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
handle_list.as_ptr() as *mut _,
std::mem::size_of::<[HANDLE; 3]>(),
std::ptr::null_mut(),
std::ptr::null_mut(),
)
};
if ok == FALSE {
let err = std::io::Error::last_os_error();
unsafe {
DeleteProcThreadAttributeList(attr_list);
}
return Err(err);
}
let mut si: STARTUPINFOEXW = unsafe { std::mem::zeroed() };
si.StartupInfo.cb = std::mem::size_of::<STARTUPINFOEXW>() as DWORD;
si.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
si.StartupInfo.hStdInput = stdin.as_raw();
si.StartupInfo.hStdOutput = stdout.as_raw();
si.StartupInfo.hStdError = stderr.as_raw();
si.lpAttributeList = attr_list;
let mut pi: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };
let mut flags: DWORD = EXTENDED_STARTUPINFO_PRESENT
| DETACHED_PROCESS
| CREATE_NEW_PROCESS_GROUP
| CREATE_NO_WINDOW;
if env_block.is_some() {
flags |= CREATE_UNICODE_ENVIRONMENT;
}
let cwd_ptr = cwd_w
.as_ref()
.map(|v| v.as_ptr())
.unwrap_or(std::ptr::null());
let env_ptr = env_block
.as_ref()
.map(|v| v.as_ptr() as *mut std::ffi::c_void)
.unwrap_or(std::ptr::null_mut());
let ok = unsafe {
CreateProcessW(
std::ptr::null(),
cmdline.as_mut_ptr(),
std::ptr::null_mut(),
std::ptr::null_mut(),
TRUE as BOOL, flags,
env_ptr,
cwd_ptr,
&mut si.StartupInfo,
&mut pi,
)
};
let err = if ok == FALSE {
Some(std::io::Error::last_os_error())
} else {
None
};
unsafe {
DeleteProcThreadAttributeList(attr_list);
}
if let Some(err) = err {
return Err(err);
}
unsafe {
CloseHandle(pi.hThread);
}
Ok(super::SanitizedChild {
pid: pi.dwProcessId,
handle: OwnedHandle(pi.hProcess),
})
}
pub fn terminate(handle: &OwnedHandle) -> std::io::Result<()> {
let ok = unsafe { TerminateProcess(handle.as_raw(), 1) };
if ok == FALSE {
return Err(std::io::Error::last_os_error());
}
Ok(())
}
pub fn wait(handle: &OwnedHandle) -> std::io::Result<i32> {
let rc = unsafe { WaitForSingleObject(handle.as_raw(), INFINITE) };
if rc != WAIT_OBJECT_0 {
return Err(std::io::Error::last_os_error());
}
let mut code: DWORD = 0;
let ok = unsafe { GetExitCodeProcess(handle.as_raw(), &mut code as *mut DWORD) };
if ok == FALSE {
return Err(std::io::Error::last_os_error());
}
Ok(code as i32)
}
pub fn try_wait(handle: &OwnedHandle) -> std::io::Result<Option<i32>> {
let mut code: DWORD = 0;
let ok = unsafe { GetExitCodeProcess(handle.as_raw(), &mut code as *mut DWORD) };
if ok == FALSE {
return Err(std::io::Error::last_os_error());
}
if code == STILL_ACTIVE {
Ok(None)
} else {
Ok(Some(code as i32))
}
}
fn build_command_line<'a>(program: &OsStr, args: impl Iterator<Item = &'a OsStr>) -> Vec<u16> {
let mut s = String::new();
s.push_str("e(&program.to_string_lossy()));
for a in args {
s.push(' ');
s.push_str("e(&a.to_string_lossy()));
}
OsStr::new(&s)
.encode_wide()
.chain(std::iter::once(0))
.collect()
}
fn quote(arg: &str) -> String {
if !arg.is_empty()
&& !arg
.chars()
.any(|c| matches!(c, ' ' | '\t' | '\n' | '\x0b' | '"'))
{
return arg.to_string();
}
let mut out = String::from("\"");
let chars: Vec<char> = arg.chars().collect();
let mut i = 0;
while i < chars.len() {
let mut nbs = 0;
while i < chars.len() && chars[i] == '\\' {
nbs += 1;
i += 1;
}
if i == chars.len() {
for _ in 0..(nbs * 2) {
out.push('\\');
}
break;
} else if chars[i] == '"' {
for _ in 0..(nbs * 2 + 1) {
out.push('\\');
}
out.push('"');
} else {
for _ in 0..nbs {
out.push('\\');
}
out.push(chars[i]);
}
i += 1;
}
out.push('"');
out
}
fn build_env_block(overrides: Vec<(OsString, Option<OsString>)>) -> Vec<u16> {
use std::collections::BTreeMap;
let mut env: BTreeMap<OsString, OsString> = BTreeMap::new();
for (k, v) in std::env::vars_os() {
env.insert(k, v);
}
for (k, v) in overrides {
match v {
Some(val) => {
env.insert(k, val);
}
None => {
env.remove(&k);
}
}
}
let mut block: Vec<u16> = Vec::new();
for (k, v) in env {
block.extend(k.encode_wide());
block.push(b'=' as u16);
block.extend(v.encode_wide());
block.push(0);
}
block.push(0);
block
}
}
#[cfg(unix)]
mod unix {
use std::process::Command;
pub fn spawn(command: &mut Command) -> std::io::Result<super::SanitizedChild> {
use std::os::unix::process::CommandExt;
use std::process::Stdio;
command
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
unsafe {
command.pre_exec(|| {
if libc::setsid() == -1 {
}
close_extra_fds();
Ok(())
});
}
let child = command.spawn()?;
let pid = child.id();
Ok(super::SanitizedChild { pid, child })
}
unsafe fn close_extra_fds() {
#[cfg(target_os = "linux")]
{
#[cfg(any(
target_arch = "x86_64",
target_arch = "aarch64",
target_arch = "x86",
target_arch = "arm",
target_arch = "riscv64",
target_arch = "powerpc64",
))]
{
const SYS_CLOSE_RANGE: libc::c_long = 436;
let rc = libc::syscall(SYS_CLOSE_RANGE, 3u32, libc::c_uint::MAX, 0u32);
if rc == 0 {
return;
}
}
}
let dir = libc::opendir(c"/dev/fd".as_ptr());
if !dir.is_null() {
let dir_fd = libc::dirfd(dir);
loop {
let ent = libc::readdir(dir);
if ent.is_null() {
break;
}
let name_ptr = (*ent).d_name.as_ptr();
let mut fd: libc::c_int = 0;
let mut p = name_ptr;
let mut ok = false;
while *p != 0 {
let c = *p as u8;
if !c.is_ascii_digit() {
ok = false;
break;
}
fd = fd * 10 + (c - b'0') as libc::c_int;
p = p.add(1);
ok = true;
}
if !ok {
continue;
}
if fd > 2 && fd != dir_fd {
libc::close(fd);
}
}
libc::closedir(dir);
return;
}
let max = libc::sysconf(libc::_SC_OPEN_MAX);
let max = if max < 0 { 4096 } else { max as libc::c_int };
for fd in 3..max {
libc::close(fd);
}
}
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
#[test]
fn sanitized_child_unix_holds_child() {
let _ = std::mem::size_of::<SanitizedChild>();
}
}