#![cfg(windows)]
use std::ffi::OsStr;
use std::io;
use std::mem::{size_of, size_of_val, zeroed};
use std::os::windows::ffi::OsStrExt;
use std::path::Path;
use std::ptr::{null, null_mut};
use windows_sys::Win32::Foundation::{
CloseHandle, GENERIC_READ, GENERIC_WRITE, INVALID_HANDLE_VALUE,
};
use windows_sys::Win32::Security::SECURITY_ATTRIBUTES;
use windows_sys::Win32::Storage::FileSystem::{
CreateFileW, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
};
use windows_sys::Win32::System::Threading::{
CreateProcessW, DeleteProcThreadAttributeList, InitializeProcThreadAttributeList,
UpdateProcThreadAttribute, CREATE_NO_WINDOW, CREATE_UNICODE_ENVIRONMENT, DETACHED_PROCESS,
EXTENDED_STARTUPINFO_PRESENT, PROCESS_INFORMATION, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
STARTF_USESTDHANDLES, STARTUPINFOEXW,
};
pub fn spawn_daemon_sanitized(
bin: &Path,
args: &[&str],
env_overrides: &[(String, String)],
) -> Result<(), String> {
unsafe {
let sa = SECURITY_ATTRIBUTES {
nLength: size_of::<SECURITY_ATTRIBUTES>() as u32,
lpSecurityDescriptor: null_mut(),
bInheritHandle: 1,
};
let nul: Vec<u16> = OsStr::new("NUL").encode_wide().chain(Some(0)).collect();
let open = |access| {
CreateFileW(
nul.as_ptr(),
access,
FILE_SHARE_READ | FILE_SHARE_WRITE,
&sa as *const _ as _,
OPEN_EXISTING,
0,
std::ptr::null_mut(),
)
};
let h_in = open(GENERIC_READ);
let h_out = open(GENERIC_WRITE);
let h_err = open(GENERIC_WRITE);
let close_nuls = || {
for &h in &[h_in, h_out, h_err] {
if !h.is_null() && h != INVALID_HANDLE_VALUE {
CloseHandle(h);
}
}
};
if h_in.is_null()
|| h_in == INVALID_HANDLE_VALUE
|| h_out.is_null()
|| h_out == INVALID_HANDLE_VALUE
|| h_err.is_null()
|| h_err == INVALID_HANDLE_VALUE
{
let err = io::Error::last_os_error();
close_nuls();
return Err(format!("CreateFileW(NUL) failed: {err}"));
}
let mut attr_size: usize = 0;
let _ = InitializeProcThreadAttributeList(null_mut(), 1, 0, &mut attr_size);
let mut attr_buf: Vec<u8> = vec![0; attr_size];
let attr_list = attr_buf.as_mut_ptr() as _;
if InitializeProcThreadAttributeList(attr_list, 1, 0, &mut attr_size) == 0 {
let err = io::Error::last_os_error();
close_nuls();
return Err(format!("InitializeProcThreadAttributeList failed: {err}"));
}
let handles = [h_in, h_out, h_err];
if UpdateProcThreadAttribute(
attr_list,
0,
PROC_THREAD_ATTRIBUTE_HANDLE_LIST as usize,
handles.as_ptr() as _,
size_of_val(&handles),
null_mut(),
null_mut(),
) == 0
{
let err = io::Error::last_os_error();
DeleteProcThreadAttributeList(attr_list);
close_nuls();
return Err(format!("UpdateProcThreadAttribute failed: {err}"));
}
let mut si: STARTUPINFOEXW = zeroed();
si.StartupInfo.cb = size_of::<STARTUPINFOEXW>() as u32;
si.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
si.StartupInfo.hStdInput = h_in;
si.StartupInfo.hStdOutput = h_out;
si.StartupInfo.hStdError = h_err;
si.lpAttributeList = attr_list;
let mut cmd_line_w = build_command_line(bin, args);
let env_block = build_env_block(env_overrides);
let mut pi: PROCESS_INFORMATION = zeroed();
let ok = CreateProcessW(
null(),
cmd_line_w.as_mut_ptr(),
null_mut(),
null_mut(),
1, EXTENDED_STARTUPINFO_PRESENT
| DETACHED_PROCESS
| CREATE_NO_WINDOW
| CREATE_UNICODE_ENVIRONMENT,
env_block.as_ptr() as _,
null(),
&si.StartupInfo,
&mut pi,
);
DeleteProcThreadAttributeList(attr_list);
close_nuls();
if ok != 0 {
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
Ok(())
} else {
Err(format!(
"CreateProcessW failed: {}",
io::Error::last_os_error()
))
}
}
}
fn build_command_line(bin: &Path, args: &[&str]) -> Vec<u16> {
let mut out = String::new();
push_quoted(&mut out, &bin.to_string_lossy());
for a in args {
out.push(' ');
push_quoted(&mut out, a);
}
OsStr::new(&out).encode_wide().chain(Some(0)).collect()
}
fn push_quoted(out: &mut String, s: &str) {
let needs_quotes = s.is_empty() || s.chars().any(|c| c == ' ' || c == '\t' || c == '"');
if !needs_quotes {
out.push_str(s);
return;
}
out.push('"');
let mut backslashes = 0usize;
for c in s.chars() {
match c {
'\\' => {
backslashes += 1;
}
'"' => {
for _ in 0..(backslashes * 2 + 1) {
out.push('\\');
}
out.push('"');
backslashes = 0;
}
_ => {
for _ in 0..backslashes {
out.push('\\');
}
backslashes = 0;
out.push(c);
}
}
}
for _ in 0..(backslashes * 2) {
out.push('\\');
}
out.push('"');
}
fn build_env_block(overrides: &[(String, String)]) -> Vec<u16> {
use std::collections::BTreeMap;
let mut map: BTreeMap<String, (String, String)> = BTreeMap::new();
for (k, v) in std::env::vars() {
map.insert(k.to_uppercase(), (k, v));
}
for (k, v) in overrides {
map.insert(k.to_uppercase(), (k.clone(), v.clone()));
}
let mut block: Vec<u16> = Vec::new();
for (k, v) in map.values() {
let entry = format!("{k}={v}");
block.extend(OsStr::new(&entry).encode_wide());
block.push(0);
}
block.push(0);
block
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Read;
use std::time::{Duration, Instant};
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Security::SECURITY_ATTRIBUTES;
use windows_sys::Win32::System::Pipes::CreatePipe;
#[test]
fn sanitized_spawn_does_not_inherit_orphan_pipe() {
let sa = SECURITY_ATTRIBUTES {
nLength: size_of::<SECURITY_ATTRIBUTES>() as u32,
lpSecurityDescriptor: null_mut(),
bInheritHandle: 1,
};
let mut read_h: HANDLE = std::ptr::null_mut();
let mut write_h: HANDLE = std::ptr::null_mut();
let ok = unsafe { CreatePipe(&mut read_h, &mut write_h, &sa, 0) };
assert!(ok != 0, "CreatePipe failed: {}", io::Error::last_os_error());
let cmd_exe = std::env::var_os("ComSpec").unwrap_or_else(|| "cmd.exe".into());
let res = spawn_daemon_sanitized(
Path::new(&cmd_exe),
&["/C", "ping", "-n", "6", "127.0.0.1", ">", "NUL"],
&[],
);
if let Err(e) = &res {
unsafe {
CloseHandle(read_h);
CloseHandle(write_h);
}
panic!("spawn_daemon_sanitized failed: {e}");
}
unsafe {
CloseHandle(write_h);
}
let mut file = unsafe {
<std::fs::File as std::os::windows::io::FromRawHandle>::from_raw_handle(read_h as _)
};
let start = Instant::now();
let mut buf = [0u8; 16];
let n = file.read(&mut buf).expect("read");
let elapsed = start.elapsed();
assert_eq!(n, 0, "expected EOF, got {n} bytes");
assert!(
elapsed < Duration::from_secs(2),
"read took {elapsed:?}, child must have inherited the pipe write-end \
(regression of #289)"
);
}
}