use super::{Job, OwnedHandle, QUERY, unavailable};
use crate::core::error::{AppError, Result};
use crate::native::process::Pinned;
use std::collections::BTreeMap;
use std::ffi::{OsStr, OsString};
use std::os::windows::ffi::OsStrExt;
use std::path::Path;
use std::time::Instant;
use windows::Win32::Foundation::{
DUPLICATE_HANDLE_OPTIONS, DUPLICATE_SAME_ACCESS, DuplicateHandle, HANDLE,
};
use windows::Win32::System::Threading::*;
use windows::core::{PCWSTR, PWSTR};
pub struct SpawnSpec<'a> {
pub program: &'a Path,
pub arguments: &'a [OsString],
pub environment: &'a BTreeMap<OsString, OsString>,
pub directory: &'a Path,
}
impl SpawnSpec<'_> {
pub fn validate(&self) -> Result<()> {
if !local_path(self.program)
|| !self.program.is_file()
|| !local_path(self.directory)
|| !self.directory.is_dir()
{
return Err(invalid());
}
crate::storage::state::reject_links(self.program)?;
crate::storage::state::reject_links(self.directory)?;
wide(self.program.as_os_str())?;
wide(self.directory.as_os_str())?;
command(self.program.as_os_str(), self.arguments)?;
environment(self.environment)?;
Ok(())
}
}
pub struct Spawned {
process: Pinned,
}
impl Spawned {
pub(crate) fn into_process(self) -> Pinned {
self.process
}
pub fn process(&self) -> &Pinned {
&self.process
}
}
struct Attributes {
_storage: Vec<u128>,
pointer: LPPROC_THREAD_ATTRIBUTE_LIST,
}
impl Attributes {
fn new() -> Result<Self> {
let mut size = 0usize;
let sizing = unsafe { InitializeProcThreadAttributeList(None, 2, None, &mut size) };
if sizing.is_ok() || size == 0 || size > 64 * 1024 {
return Err(unavailable());
}
let mut storage = vec![0u128; size.div_ceil(16)];
let pointer = LPPROC_THREAD_ATTRIBUTE_LIST(storage.as_mut_ptr().cast());
unsafe { InitializeProcThreadAttributeList(Some(pointer), 2, None, &mut size) }
.map_err(|_| unavailable())?;
Ok(Self {
_storage: storage,
pointer,
})
}
}
impl Drop for Attributes {
fn drop(&mut self) {
unsafe { DeleteProcThreadAttributeList(self.pointer) };
}
}
impl Job {
pub fn spawn(&self, spec: &SpawnSpec<'_>, deadline: Instant) -> Result<Spawned> {
crate::core::deadline::check(deadline)?;
if !self.creator {
return Err(unavailable());
}
self.check_limits(deadline)?;
spec.validate()?;
let application = wide(spec.program.as_os_str())?;
let directory = wide(spec.directory.as_os_str())?;
let mut command = command(spec.program.as_os_str(), spec.arguments)?;
let environment = environment(spec.environment)?;
let mut inherited = HANDLE::default();
unsafe {
DuplicateHandle(
GetCurrentProcess(),
self.handle.0,
GetCurrentProcess(),
&mut inherited,
QUERY,
true,
DUPLICATE_HANDLE_OPTIONS(0),
)
}
.map_err(|_| unavailable())?;
let inherited = OwnedHandle(inherited);
let null = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(r"\\.\NUL")
.map_err(|_| unavailable())?;
use std::os::windows::io::AsRawHandle;
let mut null_inherited = HANDLE::default();
unsafe {
DuplicateHandle(
GetCurrentProcess(),
HANDLE(null.as_raw_handle()),
GetCurrentProcess(),
&mut null_inherited,
0,
true,
DUPLICATE_SAME_ACCESS,
)
}
.map_err(|_| unavailable())?;
let null_inherited = OwnedHandle(null_inherited);
let jobs = [self.handle.0];
let handles = [inherited.0, null_inherited.0];
let attributes = Attributes::new()?;
unsafe {
UpdateProcThreadAttribute(
attributes.pointer,
0,
PROC_THREAD_ATTRIBUTE_JOB_LIST as usize,
Some(jobs.as_ptr().cast()),
std::mem::size_of_val(&jobs),
None,
None,
)
.map_err(|_| unavailable())?;
UpdateProcThreadAttribute(
attributes.pointer,
0,
PROC_THREAD_ATTRIBUTE_HANDLE_LIST as usize,
Some(handles.as_ptr().cast()),
std::mem::size_of_val(&handles),
None,
None,
)
}
.map_err(|_| unavailable())?;
let mut startup = STARTUPINFOEXW::default();
startup.StartupInfo.cb = std::mem::size_of::<STARTUPINFOEXW>() as u32;
startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
startup.StartupInfo.hStdInput = null_inherited.0;
startup.StartupInfo.hStdOutput = null_inherited.0;
startup.StartupInfo.hStdError = null_inherited.0;
startup.lpAttributeList = attributes.pointer;
let mut info = PROCESS_INFORMATION::default();
crate::core::deadline::check(deadline)?;
unsafe {
CreateProcessW(
PCWSTR(application.as_ptr()),
Some(PWSTR(command.as_mut_ptr())),
None,
None,
true,
CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT | CREATE_NO_WINDOW,
Some(environment.as_ptr().cast()),
PCWSTR(directory.as_ptr()),
&startup.StartupInfo,
&mut info,
)
}
.map_err(|_| {
AppError::new(
"START_FAILED",
"Native process creation failed; no successful creation was reported.",
3,
)
})?;
let process = OwnedHandle(info.hProcess);
let _thread = OwnedHandle(info.hThread);
let process = super::pin(process, deadline).map_err(|error| {
#[cfg(test)]
eprintln!("synthetic original process observation failed: {error:?}");
let _ = error;
unknown()
})?;
if process.identity().process_id() != info.dwProcessId
|| !self.contains(&process, deadline).map_err(|_| unknown())?
{
return Err(unknown());
}
Ok(Spawned { process })
}
}
fn wide(value: &OsStr) -> Result<Vec<u16>> {
let value: Vec<u16> = value.encode_wide().collect();
if value.contains(&0) || value.len() > 32766 {
return Err(invalid());
}
Ok(value.into_iter().chain([0]).collect())
}
fn command(program: &OsStr, arguments: &[OsString]) -> Result<Vec<u16>> {
if arguments.len() > 128 {
return Err(invalid());
}
let mut output = Vec::new();
for (i, value) in std::iter::once(program)
.chain(arguments.iter().map(OsString::as_os_str))
.enumerate()
{
if i > 0 {
output.push(b' ' as u16);
}
output.push(b'"' as u16);
let value = wide(value)?;
let mut slashes = 0;
for &unit in &value[..value.len() - 1] {
if unit == b'\\' as u16 {
slashes += 1;
continue;
}
output.extend(std::iter::repeat_n(
b'\\' as u16,
if unit == b'"' as u16 {
slashes * 2 + 1
} else {
slashes
},
));
output.push(unit);
slashes = 0;
}
output.extend(std::iter::repeat_n(b'\\' as u16, slashes * 2));
output.push(b'"' as u16);
if output.len() > 32766 {
return Err(invalid());
}
}
output.push(0);
Ok(output)
}
fn environment(values: &BTreeMap<OsString, OsString>) -> Result<Vec<u16>> {
if values.len() > 128 {
return Err(invalid());
}
let mut output = Vec::new();
let mut seen = std::collections::HashSet::new();
let mut entries = Vec::new();
for (name, value) in values {
let name = name.to_str().ok_or_else(invalid)?;
if name.is_empty()
|| name.len() > 128
|| !name
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'(' | b')'))
|| !seen.insert(name.to_ascii_uppercase())
{
return Err(invalid());
}
entries.push((name.to_ascii_uppercase(), value));
}
entries.sort_by(|a, b| a.0.cmp(&b.0));
for (name, value) in entries {
output.extend(name.encode_utf16());
output.push(b'=' as u16);
let value = wide(value)?;
output.extend(value);
if output.len() > 32766 {
return Err(invalid());
}
}
if output.is_empty() {
output.push(0);
}
output.push(0);
Ok(output)
}
fn local_path(path: &Path) -> bool {
let units: Vec<u16> = path.as_os_str().encode_wide().take(7).collect();
let drive = |s: &[u16]| {
s.len() >= 3
&& s[0] <= 127
&& (s[0] as u8).is_ascii_alphabetic()
&& s[1] == b':' as u16
&& matches!(s[2], 47 | 92)
};
drive(&units) || (units.starts_with(&[92, 92, 63, 92]) && drive(&units[4..]))
}
fn invalid() -> AppError {
AppError::new(
"LAUNCH_INPUT_INVALID",
"Native process inputs are invalid; no process was started.",
2,
)
}
fn unknown() -> AppError {
let mut error = AppError::new(
"START_OUTCOME_UNKNOWN",
"A process was created but verification did not complete; do not start another automatically.",
5,
);
error.outcome = Some(crate::core::contracts::write::Outcome::Unknown);
error
}
#[cfg(test)]
mod tests;