#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProcessLiveness {
Alive,
Dead,
Unknown,
}
pub const PID_BIRTH_PROCFS_TAG: u64 = 1_u64 << 63;
pub const PID_BIRTH_SYSCTL_TAG: u64 = 1_u64 << 62;
pub const PID_BIRTH_FILETIME_TAG: u64 = 1_u64 << 61;
#[cfg(any(target_os = "macos", target_os = "windows", test))]
const PAYLOAD_MASK: u64 = !(PID_BIRTH_PROCFS_TAG | PID_BIRTH_SYSCTL_TAG | PID_BIRTH_FILETIME_TAG);
#[cfg(any(target_os = "macos", target_os = "windows", test))]
const fn has_exact_platform_tag(token: u64, expected_tag: u64) -> bool {
token & !PAYLOAD_MASK == expected_tag
}
#[cfg(any(target_os = "macos", target_os = "windows", test))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ProbeFailure {
Absent,
Ambiguous,
}
#[cfg(any(target_os = "macos", target_os = "windows", test))]
const fn classify_probe_failure(
error_code: Option<i64>,
definitive_absence_code: i64,
) -> ProbeFailure {
if matches!(error_code, Some(code) if code == definitive_absence_code) {
ProbeFailure::Absent
} else {
ProbeFailure::Ambiguous
}
}
#[must_use]
pub fn current_process_birth_token() -> Option<u64> {
let pid = std::process::id();
#[cfg(target_os = "macos")]
{
macos::birth_token(pid)
}
#[cfg(windows)]
{
windows_impl::birth_token(pid)
}
#[cfg(not(any(target_os = "macos", windows)))]
{
let _ = pid;
None
}
}
#[must_use]
pub fn process_alive(pid: u32, pid_birth: u64) -> ProcessLiveness {
if pid == 0 {
return ProcessLiveness::Dead;
}
#[cfg(target_os = "macos")]
{
macos::alive(pid, pid_birth)
}
#[cfg(windows)]
{
windows_impl::alive(pid, pid_birth)
}
#[cfg(not(any(target_os = "macos", windows)))]
{
let _ = pid_birth;
ProcessLiveness::Unknown
}
}
#[cfg(target_os = "macos")]
mod macos {
use super::{
PAYLOAD_MASK, PID_BIRTH_SYSCTL_TAG, ProbeFailure, ProcessLiveness, classify_probe_failure,
has_exact_platform_tag,
};
enum StartTime {
Present(u64),
Absent,
Error,
}
fn read_start_time_usec(pid: u32) -> StartTime {
let mut info = std::mem::MaybeUninit::<libc::proc_bsdinfo>::uninit();
let size = std::mem::size_of::<libc::proc_bsdinfo>() as libc::c_int;
unsafe { *libc::__error() = 0 };
let written = unsafe {
libc::proc_pidinfo(
pid as libc::c_int,
libc::PROC_PIDTBSDINFO,
0,
info.as_mut_ptr().cast(),
size,
)
};
let error_code = (written <= 0)
.then(|| std::io::Error::last_os_error().raw_os_error())
.flatten()
.map(i64::from);
if written <= 0 {
return match classify_probe_failure(error_code, i64::from(libc::ESRCH)) {
ProbeFailure::Absent => StartTime::Absent,
ProbeFailure::Ambiguous => StartTime::Error,
};
}
if written < size {
return StartTime::Error;
}
let info = unsafe { info.assume_init() };
let usec = info
.pbi_start_tvsec
.wrapping_mul(1_000_000)
.wrapping_add(info.pbi_start_tvusec);
StartTime::Present(usec)
}
pub(super) fn birth_token(pid: u32) -> Option<u64> {
match read_start_time_usec(pid) {
StartTime::Present(usec) => Some(PID_BIRTH_SYSCTL_TAG | (usec & PAYLOAD_MASK)),
StartTime::Absent | StartTime::Error => None,
}
}
pub(super) fn alive(pid: u32, pid_birth: u64) -> ProcessLiveness {
match read_start_time_usec(pid) {
StartTime::Absent => ProcessLiveness::Dead,
StartTime::Error => ProcessLiveness::Unknown,
StartTime::Present(usec) => {
if !has_exact_platform_tag(pid_birth, PID_BIRTH_SYSCTL_TAG) {
return ProcessLiveness::Alive;
}
if (usec & PAYLOAD_MASK) == (pid_birth & PAYLOAD_MASK) {
ProcessLiveness::Alive
} else {
ProcessLiveness::Dead
}
}
}
}
}
#[cfg(windows)]
mod windows_impl {
use super::{
PAYLOAD_MASK, PID_BIRTH_FILETIME_TAG, ProbeFailure, ProcessLiveness,
classify_probe_failure, has_exact_platform_tag,
};
use windows_sys::Win32::Foundation::{
CloseHandle, ERROR_INVALID_PARAMETER, FILETIME, GetLastError,
};
use windows_sys::Win32::System::Threading::{
GetProcessTimes, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
};
enum Creation {
Present(u64),
Absent,
Error,
}
fn read_creation_100ns(pid: u32) -> Creation {
let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) };
if handle.is_null() {
let last = unsafe { GetLastError() };
return match classify_probe_failure(
Some(i64::from(last)),
i64::from(ERROR_INVALID_PARAMETER),
) {
ProbeFailure::Absent => Creation::Absent,
ProbeFailure::Ambiguous => Creation::Error,
};
}
let mut creation = FILETIME {
dwLowDateTime: 0,
dwHighDateTime: 0,
};
let mut exit = creation;
let mut kernel = creation;
let mut user = creation;
let ok = unsafe {
GetProcessTimes(
handle,
&raw mut creation,
&raw mut exit,
&raw mut kernel,
&raw mut user,
)
};
unsafe {
CloseHandle(handle);
}
if ok == 0 {
return Creation::Error;
}
let ticks = (u64::from(creation.dwHighDateTime) << 32) | u64::from(creation.dwLowDateTime);
Creation::Present(ticks)
}
pub(super) fn birth_token(pid: u32) -> Option<u64> {
match read_creation_100ns(pid) {
Creation::Present(ticks) => Some(PID_BIRTH_FILETIME_TAG | (ticks & PAYLOAD_MASK)),
Creation::Absent | Creation::Error => None,
}
}
pub(super) fn alive(pid: u32, pid_birth: u64) -> ProcessLiveness {
match read_creation_100ns(pid) {
Creation::Absent => ProcessLiveness::Dead,
Creation::Error => ProcessLiveness::Unknown,
Creation::Present(ticks) => {
if !has_exact_platform_tag(pid_birth, PID_BIRTH_FILETIME_TAG) {
return ProcessLiveness::Alive;
}
if (ticks & PAYLOAD_MASK) == (pid_birth & PAYLOAD_MASK) {
ProcessLiveness::Alive
} else {
ProcessLiveness::Dead
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn platform_birth_tags_are_distinct_and_high() {
assert_ne!(PID_BIRTH_PROCFS_TAG, PID_BIRTH_SYSCTL_TAG);
assert_ne!(PID_BIRTH_SYSCTL_TAG, PID_BIRTH_FILETIME_TAG);
assert_eq!(PID_BIRTH_PROCFS_TAG & PAYLOAD_MASK, 0);
assert_eq!(PID_BIRTH_SYSCTL_TAG & PAYLOAD_MASK, 0);
assert_eq!(PID_BIRTH_FILETIME_TAG & PAYLOAD_MASK, 0);
}
#[test]
fn platform_birth_tag_must_be_exact() {
assert!(has_exact_platform_tag(
PID_BIRTH_SYSCTL_TAG | 0x1234,
PID_BIRTH_SYSCTL_TAG
));
assert!(!has_exact_platform_tag(0x1234, PID_BIRTH_SYSCTL_TAG));
assert!(!has_exact_platform_tag(
PID_BIRTH_FILETIME_TAG | 0x1234,
PID_BIRTH_SYSCTL_TAG
));
assert!(!has_exact_platform_tag(
PID_BIRTH_SYSCTL_TAG | PID_BIRTH_FILETIME_TAG | 0x1234,
PID_BIRTH_SYSCTL_TAG
));
}
#[test]
fn pid_zero_is_dead() {
assert_eq!(process_alive(0, 0), ProcessLiveness::Dead);
}
#[test]
fn macos_zero_probe_result_requires_esrch_to_prove_absence() {
assert_eq!(
classify_probe_failure(Some(i64::from(libc::ESRCH)), i64::from(libc::ESRCH)),
ProbeFailure::Absent
);
assert_eq!(
classify_probe_failure(Some(i64::from(libc::EACCES)), i64::from(libc::ESRCH)),
ProbeFailure::Ambiguous
);
assert_eq!(
classify_probe_failure(None, i64::from(libc::ESRCH)),
ProbeFailure::Ambiguous
);
}
#[test]
fn windows_open_process_resource_failure_is_ambiguous() {
const ERROR_INVALID_PARAMETER_CODE: i64 = 87;
const ERROR_NOT_ENOUGH_MEMORY_CODE: i64 = 8;
assert_eq!(
classify_probe_failure(
Some(ERROR_INVALID_PARAMETER_CODE),
ERROR_INVALID_PARAMETER_CODE
),
ProbeFailure::Absent
);
assert_eq!(
classify_probe_failure(
Some(ERROR_NOT_ENOUGH_MEMORY_CODE),
ERROR_INVALID_PARAMETER_CODE
),
ProbeFailure::Ambiguous
);
}
#[cfg(not(any(target_os = "macos", windows)))]
#[test]
fn non_macos_non_windows_returns_unknown() {
assert_eq!(
process_alive(std::process::id(), 0),
ProcessLiveness::Unknown
);
assert!(current_process_birth_token().is_none());
}
#[cfg(any(target_os = "macos", windows))]
#[test]
fn current_process_is_alive_with_its_own_birth_token() {
let birth = current_process_birth_token().expect("own birth token available");
assert_eq!(
process_alive(std::process::id(), birth),
ProcessLiveness::Alive,
"the current process must read as alive with its own birth token"
);
}
#[cfg(any(target_os = "macos", windows))]
#[test]
fn recycled_pid_birth_mismatch_reads_dead() {
let birth = current_process_birth_token().expect("own birth token");
let tag = birth & !PAYLOAD_MASK;
let mismatched = tag | ((birth & PAYLOAD_MASK) ^ 0x5A5A);
assert_eq!(
process_alive(std::process::id(), mismatched),
ProcessLiveness::Dead,
"a start-time mismatch on a live PID must read as Dead (reuse-safe)"
);
}
#[cfg(any(target_os = "macos", windows))]
#[test]
fn almost_certainly_dead_pid_reads_dead() {
#[cfg(target_os = "macos")]
let platform_tag = PID_BIRTH_SYSCTL_TAG;
#[cfg(windows)]
let platform_tag = PID_BIRTH_FILETIME_TAG;
let verdict = process_alive(0x7FFF_FFF0, platform_tag | 0x1234);
assert_ne!(verdict, ProcessLiveness::Alive);
}
}