#[cfg(windows)]
use windows_sys::Win32::Foundation::{
CloseHandle, ERROR_INVALID_PARAMETER, FILETIME, GetLastError, HANDLE, STILL_ACTIVE,
};
#[cfg(windows)]
use windows_sys::Win32::System::Threading::{
GetExitCodeProcess, GetProcessTimes, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
PROCESS_TERMINATE, QueryFullProcessImageNameW, TerminateProcess,
};
pub(crate) const IDENTITY_CREATION_SLACK_MICROS: i64 = 5_000_000;
pub(crate) const REAP_EXIT_WAIT: std::time::Duration = std::time::Duration::from_secs(5);
const FILETIME_UNIX_EPOCH_OFFSET_MICROS: i64 = 11_644_473_600_000_000;
#[cfg(windows)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum ReapOutcome {
AlreadyDead,
Terminated,
IdentityMismatch,
Unverifiable,
}
#[cfg(windows)]
struct OwnedProcessHandle(HANDLE);
pub(crate) fn creation_may_belong_to_run(
creation_unix_micros: i64,
run_started_unix_micros: i64,
) -> bool {
creation_unix_micros <= run_started_unix_micros + IDENTITY_CREATION_SLACK_MICROS
}
pub(crate) fn filetime_ticks_to_unix_micros(ticks: u64) -> i64 {
(ticks / 10) as i64 - FILETIME_UNIX_EPOCH_OFFSET_MICROS
}
pub(crate) fn image_basename_is_msb(image_path: &str) -> bool {
let basename = image_path.rsplit(['/', '\\']).next().unwrap_or(image_path);
let stem = match basename.len().checked_sub(4) {
Some(idx)
if basename.is_char_boundary(idx) && basename[idx..].eq_ignore_ascii_case(".exe") =>
{
&basename[..idx]
}
_ => basename,
};
stem.eq_ignore_ascii_case("msb")
}
#[cfg(windows)]
pub(crate) fn terminate_runtime_process_checked(
pid: u32,
run_started_unix_micros: i64,
) -> std::io::Result<ReapOutcome> {
let handle = unsafe {
OpenProcess(
PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_TERMINATE,
0,
pid,
)
};
if handle.is_null() {
let err = unsafe { GetLastError() };
if err == ERROR_INVALID_PARAMETER {
return Ok(ReapOutcome::AlreadyDead);
}
return Ok(ReapOutcome::Unverifiable);
}
let handle = OwnedProcessHandle(handle);
let mut exit_code = 0u32;
if unsafe { GetExitCodeProcess(handle.0, &mut exit_code) } == 0 {
return Ok(ReapOutcome::Unverifiable);
}
if exit_code != STILL_ACTIVE as u32 {
return Ok(ReapOutcome::AlreadyDead);
}
let Some(creation_unix_micros) = process_creation_unix_micros(handle.0) else {
return Ok(ReapOutcome::Unverifiable);
};
if !creation_may_belong_to_run(creation_unix_micros, run_started_unix_micros) {
return Ok(ReapOutcome::IdentityMismatch);
}
if let Some(image) = process_image_path(handle.0)
&& !image_basename_is_msb(&image)
{
return Ok(ReapOutcome::IdentityMismatch);
}
if unsafe { TerminateProcess(handle.0, 1) } == 0 {
return Err(std::io::Error::last_os_error());
}
Ok(ReapOutcome::Terminated)
}
#[cfg(windows)]
fn process_creation_unix_micros(handle: HANDLE) -> Option<i64> {
let mut creation = FILETIME {
dwLowDateTime: 0,
dwHighDateTime: 0,
};
let mut exit = creation;
let mut kernel = creation;
let mut user = creation;
if unsafe { GetProcessTimes(handle, &mut creation, &mut exit, &mut kernel, &mut user) } == 0 {
return None;
}
let ticks = (u64::from(creation.dwHighDateTime) << 32) | u64::from(creation.dwLowDateTime);
Some(filetime_ticks_to_unix_micros(ticks))
}
#[cfg(windows)]
fn process_image_path(handle: HANDLE) -> Option<String> {
let mut buf = vec![0u16; 4096];
let mut len = buf.len() as u32;
let ok = unsafe { QueryFullProcessImageNameW(handle, 0, buf.as_mut_ptr(), &mut len) };
(ok != 0).then(|| String::from_utf16_lossy(&buf[..len as usize]))
}
#[cfg(windows)]
impl Drop for OwnedProcessHandle {
fn drop(&mut self) {
let _ = unsafe { CloseHandle(self.0) };
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn creation_before_run_start_matches() {
assert!(creation_may_belong_to_run(1_000_000, 2_000_000));
assert!(creation_may_belong_to_run(2_000_000, 2_000_000));
}
#[test]
fn creation_within_slack_after_run_start_matches() {
assert!(creation_may_belong_to_run(
2_000_000 + IDENTITY_CREATION_SLACK_MICROS,
2_000_000
));
}
#[test]
fn creation_after_slack_is_a_recycled_pid() {
assert!(!creation_may_belong_to_run(
2_000_001 + IDENTITY_CREATION_SLACK_MICROS,
2_000_000
));
}
#[test]
fn filetime_epoch_offset_maps_to_unix_epoch() {
assert_eq!(filetime_ticks_to_unix_micros(116_444_736_000_000_000), 0);
assert_eq!(
filetime_ticks_to_unix_micros(116_444_736_000_000_000 + 10_000_000),
1_000_000
);
}
#[test]
fn msb_image_names_match() {
assert!(image_basename_is_msb(r"C:\Program Files\msb\msb.exe"));
assert!(image_basename_is_msb(r"C:\Users\dev\MSB.EXE"));
assert!(image_basename_is_msb("/usr/local/bin/msb"));
assert!(image_basename_is_msb("msb"));
}
#[test]
fn non_msb_image_names_do_not_match() {
assert!(!image_basename_is_msb(r"C:\Windows\System32\svchost.exe"));
assert!(!image_basename_is_msb(r"C:\tools\msb-agent.exe"));
assert!(!image_basename_is_msb("/usr/bin/cargo"));
assert!(!image_basename_is_msb(""));
}
}