#![allow(unsafe_code)]
use std::{
fs::File,
io,
mem::size_of,
os::windows::{
fs::OpenOptionsExt,
io::{AsRawHandle, FromRawHandle, OwnedHandle},
},
path::{Path, PathBuf},
time::Duration,
};
use windows_sys::Win32::{
Foundation::{ERROR_INVALID_PARAMETER, FILETIME, HANDLE, WAIT_OBJECT_0, WAIT_TIMEOUT},
Storage::FileSystem::{
FileAttributeTagInfo, FileIdInfo, GetFileInformationByHandleEx,
FILE_ATTRIBUTE_REPARSE_POINT, FILE_ATTRIBUTE_TAG_INFO, FILE_FLAG_OPEN_REPARSE_POINT,
FILE_ID_INFO, FILE_SHARE_READ, FILE_SHARE_WRITE,
},
System::{
ProcessStatus::{GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS},
Threading::{
GetProcessTimes, OpenProcess, TerminateProcess, WaitForSingleObject,
PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_SYNCHRONIZE, PROCESS_TERMINATE,
},
},
};
use crate::{MemoryKind, Observation, Process, ResourceUsage};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct WindowsFileIdentity {
pub volume_serial_number: u64,
pub file_id: [u8; 16],
}
#[derive(Debug)]
pub struct ExecutableCapture {
file: File,
path: PathBuf,
identity: WindowsFileIdentity,
}
#[derive(Debug)]
pub struct SpawnedImage {
process: Process,
path: PathBuf,
start_time: u64,
identity: WindowsFileIdentity,
}
impl PartialEq for SpawnedImage {
fn eq(&self, other: &Self) -> bool {
self.process.pid() == other.process.pid()
&& self.start_time == other.start_time
&& self.identity == other.identity
&& self.path == other.path
}
}
impl Eq for SpawnedImage {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImageUnavailable {
ProcessExited,
ProcessIdentityUnconfirmed,
SpawnPathUnreadable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImageAgreement {
Match(WindowsFileIdentity),
Mismatch {
running: WindowsFileIdentity,
disk: WindowsFileIdentity,
},
Unavailable(ImageUnavailable),
}
fn open_image(path: &Path) -> io::Result<File> {
let file = std::fs::OpenOptions::new()
.read(true)
.share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE)
.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
.open(path)?;
let mut attributes: FILE_ATTRIBUTE_TAG_INFO = Default::default();
if unsafe {
GetFileInformationByHandleEx(
file.as_raw_handle(),
FileAttributeTagInfo,
(&mut attributes as *mut FILE_ATTRIBUTE_TAG_INFO).cast(),
size_of::<FILE_ATTRIBUTE_TAG_INFO>() as u32,
)
} == 0
{
return Err(io::Error::last_os_error());
}
if attributes.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"executable is a reparse point",
));
}
Ok(file)
}
fn identity(file: &File) -> io::Result<WindowsFileIdentity> {
let mut info: FILE_ID_INFO = Default::default();
if unsafe {
GetFileInformationByHandleEx(
file.as_raw_handle(),
FileIdInfo,
(&mut info as *mut FILE_ID_INFO).cast(),
size_of::<FILE_ID_INFO>() as u32,
)
} == 0
{
return Err(io::Error::last_os_error());
}
Ok(WindowsFileIdentity {
volume_serial_number: info.VolumeSerialNumber,
file_id: info.FileId.Identifier,
})
}
impl ExecutableCapture {
pub fn open(path: &Path) -> io::Result<Self> {
let supplied = open_image(path)?;
let path = std::fs::canonicalize(path)?;
let file = open_image(&path)?;
let identity = identity(&file)?;
if identity != self::identity(&supplied)? {
return Err(io::Error::other(
"executable changed while resolving its path",
));
}
Ok(Self {
file,
path,
identity,
})
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn bind(self, child: &std::process::Child) -> io::Result<SpawnedImage> {
let handle = child.as_raw_handle();
let borrowed = unsafe { std::os::windows::io::BorrowedHandle::borrow_raw(handle) };
self.bind_owned(child.id(), borrowed.try_clone_to_owned()?)
}
#[cfg(feature = "tokio")]
pub fn bind_tokio(self, child: &tokio::process::Child) -> io::Result<SpawnedImage> {
let pid = child
.id()
.ok_or_else(|| io::Error::other("child already reaped"))?;
let handle = child
.raw_handle()
.ok_or_else(|| io::Error::other("child handle unavailable"))?;
let borrowed = unsafe { std::os::windows::io::BorrowedHandle::borrow_raw(handle) };
self.bind_owned(pid, borrowed.try_clone_to_owned()?)
}
fn bind_owned(self, pid: u32, handle: OwnedHandle) -> io::Result<SpawnedImage> {
let process = Process { pid, handle };
let start_time = observe(&process)
.ok_or_else(|| io::Error::other("spawned process identity unavailable"))?
.start_time;
drop(self.file);
Ok(SpawnedImage {
process,
path: self.path,
start_time,
identity: self.identity,
})
}
}
impl SpawnedImage {
pub fn process(&self) -> &Process {
&self.process
}
pub fn start_time(&self) -> u64 {
self.start_time
}
pub fn identity(&self) -> WindowsFileIdentity {
self.identity
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn agreement(&self, expected_start_time: u64) -> ImageAgreement {
if expected_start_time != self.start_time {
return ImageAgreement::Unavailable(ImageUnavailable::ProcessIdentityUnconfirmed);
}
if !matches!(observe(&self.process), Some(observed) if observed.start_time == self.start_time)
{
return self.unavailable_process();
}
let disk = match open_image(&self.path).and_then(|file| identity(&file)) {
Ok(identity) => identity,
Err(_) => return ImageAgreement::Unavailable(ImageUnavailable::SpawnPathUnreadable),
};
if !matches!(observe(&self.process), Some(observed) if observed.start_time == self.start_time)
{
return self.unavailable_process();
}
if disk == self.identity {
ImageAgreement::Match(self.identity)
} else {
ImageAgreement::Mismatch {
running: self.identity,
disk,
}
}
}
fn unavailable_process(&self) -> ImageAgreement {
ImageAgreement::Unavailable(if matches!(wait(&self.process, Duration::ZERO), Ok(true)) {
ImageUnavailable::ProcessExited
} else {
ImageUnavailable::ProcessIdentityUnconfirmed
})
}
}
pub(crate) fn open(pid: u32) -> io::Result<Option<OwnedHandle>> {
let handle = unsafe {
OpenProcess(
PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE | PROCESS_TERMINATE,
0,
pid,
)
};
if handle.is_null() {
let error = io::Error::last_os_error();
if error.raw_os_error() == Some(ERROR_INVALID_PARAMETER as i32) {
return Ok(None);
}
return Err(error);
}
Ok(Some(unsafe { OwnedHandle::from_raw_handle(handle) }))
}
fn times(handle: HANDLE) -> Option<(u64, u64, u64)> {
let (mut creation, mut exit, mut kernel, mut user): (FILETIME, FILETIME, FILETIME, FILETIME) =
Default::default();
if unsafe { GetProcessTimes(handle, &mut creation, &mut exit, &mut kernel, &mut user) } == 0 {
return None;
}
let ticks =
|time: FILETIME| (u64::from(time.dwHighDateTime) << 32) | u64::from(time.dwLowDateTime);
Some((ticks(creation), ticks(kernel), ticks(user)))
}
pub(crate) fn wait(process: &Process, timeout: Duration) -> io::Result<bool> {
let millis = u32::try_from(timeout.as_millis())
.unwrap_or(u32::MAX - 1)
.min(u32::MAX - 1);
let result = match unsafe { WaitForSingleObject(process.handle.as_raw_handle(), millis) } {
WAIT_OBJECT_0 => Ok(true),
WAIT_TIMEOUT => Ok(false),
_ => Err(io::Error::last_os_error()),
};
#[cfg(test)]
if !timeout.is_zero() && matches!(result, Ok(true)) {
COMPLETED_BOUNDED_WAITS.with(|count| count.set(count.get() + 1));
}
result
}
#[cfg(test)]
thread_local! {
static COMPLETED_BOUNDED_WAITS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
#[cfg(test)]
pub(crate) fn completed_bounded_waits() -> u64 {
COMPLETED_BOUNDED_WAITS.with(|count| count.get())
}
pub(crate) fn observe(process: &Process) -> Option<Observation> {
if wait(process, Duration::ZERO).ok()? {
return None;
}
let start_time = times(process.handle.as_raw_handle())?.0;
if wait(process, Duration::ZERO).ok()? {
return None;
}
Some(Observation {
start_time,
executable: None,
})
}
pub(crate) fn force_stop(process: &Process, expected: u64, timeout: Duration) -> io::Result<bool> {
let Some(observed) = observe(process) else {
return if wait(process, Duration::ZERO)? {
Ok(true)
} else {
Err(io::Error::other("process identity unavailable"))
};
};
if observed.start_time != expected {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"process creation time does not match",
));
}
if unsafe { TerminateProcess(process.handle.as_raw_handle(), 1) } == 0 {
let error = io::Error::last_os_error();
if !wait(process, Duration::ZERO)? {
return Err(error);
}
}
wait(process, timeout)
}
pub(crate) fn resource_usage(process: &Process) -> Option<ResourceUsage> {
observe(process)?;
let mut counters: PROCESS_MEMORY_COUNTERS = Default::default();
if unsafe {
GetProcessMemoryInfo(
process.handle.as_raw_handle(),
&mut counters,
size_of::<PROCESS_MEMORY_COUNTERS>() as u32,
)
} == 0
{
return None;
}
let (_, kernel, user) = times(process.handle.as_raw_handle())?;
observe(process)?;
let duration = |ticks: u64| {
Duration::from_secs(ticks / 10_000_000) + Duration::from_nanos((ticks % 10_000_000) * 100)
};
Some(ResourceUsage {
memory_bytes: counters.WorkingSetSize as u64,
memory_kind: MemoryKind::WindowsWorkingSet,
swap_bytes: None,
cpu_user: duration(user),
cpu_system: duration(kernel),
})
}