#[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 = "linux", target_os = "macos", windows, test))]
const PAYLOAD_MASK: u64 = !(PID_BIRTH_PROCFS_TAG | PID_BIRTH_SYSCTL_TAG | PID_BIRTH_FILETIME_TAG);
#[cfg(any(target_os = "linux", target_os = "macos", windows, test))]
const fn has_exact_platform_tag(token: u64, expected_tag: u64) -> bool {
token & !PAYLOAD_MASK == expected_tag
}
#[cfg(any(target_os = "linux", target_os = "macos", windows, test))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ProbeFailure {
Absent,
Ambiguous,
}
#[cfg(any(target_os = "linux", target_os = "macos", 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 = "linux")]
{
linux::birth_token(pid)
}
#[cfg(target_os = "macos")]
{
macos::birth_token(pid)
}
#[cfg(windows)]
{
windows_impl::birth_token(pid)
}
#[cfg(not(any(target_os = "linux", 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 = "linux")]
{
linux::alive(pid, pid_birth)
}
#[cfg(target_os = "macos")]
{
macos::alive(pid, pid_birth)
}
#[cfg(windows)]
{
windows_impl::alive(pid, pid_birth)
}
#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
{
let _ = pid_birth;
ProcessLiveness::Unknown
}
}
#[cfg(target_os = "linux")]
mod linux {
use std::io;
use super::{
PID_BIRTH_PROCFS_TAG, ProbeFailure, ProcessLiveness, classify_probe_failure,
has_exact_platform_tag,
};
fn read_stat(pid: u32) -> io::Result<Vec<u8>> {
let path = std::path::Path::new("/proc")
.join(pid.to_string())
.join("stat");
std::fs::read(path)
}
fn start_ticks(stat: &[u8], expected_pid: u32) -> Option<u64> {
let comm_start = stat.iter().position(|byte| *byte == b'(')?;
let recorded_pid = std::str::from_utf8(&stat[..comm_start])
.ok()?
.trim()
.parse::<u32>()
.ok()?;
if recorded_pid != expected_pid {
return None;
}
let comm_end = stat.iter().rposition(|byte| *byte == b')')?;
if comm_end < comm_start {
return None;
}
std::str::from_utf8(stat.get(comm_end + 1..)?)
.ok()?
.split_ascii_whitespace()
.nth(19)?
.parse::<u64>()
.ok()
}
pub(super) fn birth_token(pid: u32) -> Option<u64> {
let stat = read_stat(pid).ok()?;
let ticks = start_ticks(&stat, pid)?;
Some(PID_BIRTH_PROCFS_TAG | (ticks & !PID_BIRTH_PROCFS_TAG))
}
fn presence(pid: u32) -> ProcessLiveness {
let Ok(native_pid) = libc::pid_t::try_from(pid) else {
return ProcessLiveness::Unknown;
};
if native_pid <= 0 {
return ProcessLiveness::Unknown;
}
if unsafe { libc::kill(native_pid, 0) } == 0 {
return ProcessLiveness::Alive;
}
let error_code = io::Error::last_os_error().raw_os_error().map(i64::from);
match classify_probe_failure(error_code, i64::from(libc::ESRCH)) {
ProbeFailure::Absent => ProcessLiveness::Dead,
ProbeFailure::Ambiguous => ProcessLiveness::Unknown,
}
}
fn alive_with(
pid: u32,
pid_birth: u64,
read: impl FnOnce(u32) -> io::Result<Vec<u8>>,
probe_presence: impl FnOnce(u32) -> ProcessLiveness,
) -> ProcessLiveness {
if let Some(ticks) = read(pid).ok().and_then(|stat| start_ticks(&stat, pid)) {
if !has_exact_platform_tag(pid_birth, PID_BIRTH_PROCFS_TAG) {
return ProcessLiveness::Alive;
}
return if (ticks & !PID_BIRTH_PROCFS_TAG) == (pid_birth & !PID_BIRTH_PROCFS_TAG) {
ProcessLiveness::Alive
} else {
ProcessLiveness::Dead
};
}
match probe_presence(pid) {
ProcessLiveness::Dead => ProcessLiveness::Dead,
ProcessLiveness::Alive | ProcessLiveness::Unknown => ProcessLiveness::Unknown,
}
}
pub(super) fn alive(pid: u32, pid_birth: u64) -> ProcessLiveness {
alive_with(pid, pid_birth, read_stat, presence)
}
#[cfg(test)]
#[allow(clippy::decimal_bitwise_operands)]
mod tests {
use std::cell::Cell;
use super::*;
use crate::process::{PID_BIRTH_FILETIME_TAG, PID_BIRTH_SYSCTL_TAG};
fn stat(pid: u32, comm: &[u8], ticks: u64) -> Vec<u8> {
let mut bytes = format!("{pid} (").into_bytes();
bytes.extend_from_slice(comm);
bytes.extend_from_slice(b") R");
for field in 4..22 {
bytes.extend_from_slice(format!(" {field}").as_bytes());
}
bytes.extend_from_slice(format!(" {ticks} 23 24\n").as_bytes());
bytes
}
#[test]
fn proc_stat_parsing_ignores_arbitrary_comm_bytes() {
for comm in [
b"writer".as_slice(),
b"a (b) c)",
b"a\nb",
b"invalid\xff\xfe",
] {
for ticks in [0, 42, u64::MAX] {
assert_eq!(start_ticks(&stat(123, comm, ticks), 123), Some(ticks));
}
}
}
#[test]
fn malformed_stat_never_supplies_a_birth_token() {
let valid = stat(123, b"writer", 9876);
assert_eq!(start_ticks(&valid, 124), None);
for bytes in [
b"".as_slice(),
b"123 writer R",
b"bad (writer) R",
b"123 ) (",
b"123 (writer) R 1 2 3",
b"123 (writer) \xff",
] {
assert_eq!(start_ticks(bytes, 123), None);
}
let mut bad_ticks = b"123 (writer) R".to_vec();
for _ in 4..22 {
bad_ticks.extend_from_slice(b" 0");
}
bad_ticks.extend_from_slice(b" 18446744073709551616");
assert_eq!(start_ticks(&bad_ticks, 123), None);
}
#[test]
fn only_an_exact_linux_token_can_prove_pid_reuse() {
for (birth, expected) in [
(PID_BIRTH_PROCFS_TAG | 1234, ProcessLiveness::Alive),
(PID_BIRTH_PROCFS_TAG | 1235, ProcessLiveness::Dead),
(0, ProcessLiveness::Alive),
(1235, ProcessLiveness::Alive),
(PID_BIRTH_SYSCTL_TAG | 1235, ProcessLiveness::Alive),
(PID_BIRTH_FILETIME_TAG | 1235, ProcessLiveness::Alive),
(
PID_BIRTH_PROCFS_TAG | PID_BIRTH_SYSCTL_TAG | 1235,
ProcessLiveness::Alive,
),
(
PID_BIRTH_PROCFS_TAG | PID_BIRTH_FILETIME_TAG | 1235,
ProcessLiveness::Alive,
),
] {
assert_eq!(
alive_with(
123,
birth,
|pid| Ok(stat(pid, b"writer", 1234)),
|_| panic!("valid stat does not require a fallback probe"),
),
expected
);
}
}
#[test]
fn proc_read_failures_require_independent_proof_of_absence() {
for kind in [
io::ErrorKind::NotFound,
io::ErrorKind::PermissionDenied,
io::ErrorKind::Interrupted,
io::ErrorKind::InvalidData,
io::ErrorKind::Other,
] {
for presence in [
ProcessLiveness::Alive,
ProcessLiveness::Unknown,
ProcessLiveness::Dead,
] {
let probes = Cell::new(0);
let verdict = alive_with(
123,
PID_BIRTH_PROCFS_TAG | 1234,
|_| Err(kind.into()),
|pid| {
assert_eq!(pid, 123);
probes.set(probes.get() + 1);
presence
},
);
assert_eq!(probes.get(), 1);
assert_eq!(
verdict,
if presence == ProcessLiveness::Dead {
ProcessLiveness::Dead
} else {
ProcessLiveness::Unknown
}
);
}
}
}
#[test]
fn invalid_stat_keeps_a_possibly_live_owner() {
for bytes in [b"truncated".to_vec(), stat(124, b"wrong pid", 1234)] {
assert_eq!(
alive_with(
123,
PID_BIRTH_PROCFS_TAG | 1234,
|_| Ok(bytes),
|_| ProcessLiveness::Alive,
),
ProcessLiveness::Unknown
);
}
}
#[test]
fn signal_zero_never_uses_a_process_group_pid() {
assert_eq!(presence(0), ProcessLiveness::Unknown);
assert_eq!(presence(u32::MAX), ProcessLiveness::Unknown);
}
#[test]
fn signal_probe_requires_esrch_for_absence() {
assert_eq!(
classify_probe_failure(Some(i64::from(libc::ESRCH)), i64::from(libc::ESRCH)),
ProbeFailure::Absent
);
for code in [
None,
Some(0),
Some(libc::EPERM),
Some(libc::EACCES),
Some(libc::EINVAL),
] {
assert_eq!(
classify_probe_failure(code.map(i64::from), i64::from(libc::ESRCH)),
ProbeFailure::Ambiguous
);
}
}
}
}
#[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 = "linux", target_os = "macos", windows)))]
#[test]
fn unsupported_platform_returns_unknown() {
assert_eq!(
process_alive(std::process::id(), 0),
ProcessLiveness::Unknown
);
assert!(current_process_birth_token().is_none());
}
#[cfg(any(target_os = "linux", 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 = "linux", 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);
}
}