2026-07-16 00:52:21 +01:00
use anyhow ::{ Context , Result , bail } ;
2026-07-13 18:25:19 +01:00
use std ::path ::PathBuf ;
2026-07-12 13:32:47 +01:00
2026-07-16 00:52:21 +01:00
/// The immutable operating-system identity of one process incarnation.
///
/// A PID alone is an address which the OS is free to reuse. `creation_token`
/// is the native, monotonic-within-boot incarnation token (Windows FILETIME or
/// Linux `/proc/<pid>/stat` starttime), and the executable path prevents a
/// different image from inheriting authority through an erroneous launch.
#[ derive(Clone, Debug, PartialEq, Eq) ]
pub struct ProcessInstanceIdentity {
pub process_id : u32 ,
pub creation_token : u64 ,
pub created_at_rfc3339 : Option < String > ,
pub executable_path : PathBuf ,
}
#[ derive(Clone, Debug, PartialEq, Eq) ]
pub enum ProcessInstanceObservation {
ExactAlive ,
ExactExited { exit_code : Option < u32 > } ,
Replaced { observed : ProcessInstanceIdentity } ,
Missing ,
Inaccessible ,
Indeterminate { reason : String } ,
}
/// Compatibility projection for old operator displays. It is deliberately
/// incapable of calling access denial or PID absence "dead".
2026-07-12 13:32:47 +01:00
#[ derive(Clone, Copy, Debug, PartialEq, Eq) ]
pub enum EpiphanyProcessObservation {
Alive ,
Missing ,
}
2026-07-16 00:52:21 +01:00
impl EpiphanyProcessObservation {
pub fn label ( self ) -> & 'static str {
match self {
Self ::Alive = > " alive " ,
Self ::Missing = > " missing " ,
2026-07-13 18:25:19 +01:00
}
}
}
2026-07-16 00:52:21 +01:00
pub fn capture_process_instance ( process_id : u32 ) -> Result < ProcessInstanceIdentity > {
if process_id = = 0 {
bail! ( " process-instance identity requires a nonzero PID " ) ;
}
platform ::capture ( process_id )
}
pub fn observe_process_instance ( expected : & ProcessInstanceIdentity ) -> ProcessInstanceObservation {
if expected . process_id = = 0 | | expected . creation_token = = 0 {
return ProcessInstanceObservation ::Indeterminate {
reason : " invalid expected process-instance identity " . to_string ( ) ,
} ;
}
platform ::observe ( expected )
}
/// Returns a boot-incarnation token only when the platform can prove it.
pub fn native_boot_identity ( ) -> Option < String > {
platform ::boot_identity ( )
}
2026-07-13 18:25:19 +01:00
pub fn native_process_executable_path ( process_id : u32 ) -> Result < Option < PathBuf > > {
2026-07-16 00:52:21 +01:00
match capture_process_instance ( process_id ) {
Ok ( identity ) = > Ok ( Some ( identity . executable_path ) ) ,
Err ( error ) if platform ::is_missing_error ( & error ) = > Ok ( None ) ,
Err ( error ) = > Err ( error ) ,
2026-07-13 18:25:19 +01:00
}
}
2026-07-16 00:52:21 +01:00
pub fn observe_native_process ( process_id : u32 ) -> Result < EpiphanyProcessObservation > {
// This legacy API has no expected incarnation and therefore cannot prove
// termination. It remains useful only as a conservative liveness display.
match capture_process_instance ( process_id ) {
Ok ( _ ) = > Ok ( EpiphanyProcessObservation ::Alive ) ,
Err ( error ) if platform ::is_missing_error ( & error ) = > Ok ( EpiphanyProcessObservation ::Missing ) ,
Err ( error ) = > Err ( error ) ,
2026-07-12 13:32:47 +01:00
}
}
#[ cfg(windows) ]
2026-07-16 00:52:21 +01:00
mod platform {
use super ::* ;
use chrono ::{ DateTime , SecondsFormat , Utc } ;
use std ::os ::windows ::ffi ::OsStringExt ;
use windows_sys ::Win32 ::Foundation ::{
CloseHandle , ERROR_ACCESS_DENIED , ERROR_INVALID_PARAMETER , ERROR_NO_MORE_FILES , FILETIME ,
GetLastError , INVALID_HANDLE_VALUE , WAIT_FAILED , WAIT_OBJECT_0 , WAIT_TIMEOUT ,
} ;
use windows_sys ::Win32 ::System ::Diagnostics ::ToolHelp ::{
CreateToolhelp32Snapshot , PROCESSENTRY32W , Process32FirstW , Process32NextW ,
TH32CS_SNAPPROCESS ,
} ;
2026-07-12 13:32:47 +01:00
use windows_sys ::Win32 ::System ::Threading ::{
2026-07-16 00:52:21 +01:00
GetExitCodeProcess , GetProcessTimes , OpenProcess , PROCESS_QUERY_LIMITED_INFORMATION ,
QueryFullProcessImageNameW , WaitForSingleObject ,
2026-07-12 13:32:47 +01:00
} ;
2026-07-16 00:52:21 +01:00
2026-07-16 03:24:54 +01:00
#[ repr(C) ]
struct SystemTimeOfDayInformation {
boot_time : i64 ,
current_time : i64 ,
time_zone_bias : i64 ,
current_time_zone_id : u32 ,
reserved : u32 ,
boot_time_bias : u64 ,
sleep_time_bias : u64 ,
}
#[ link(name = " ntdll " ) ]
unsafe extern " system " {
fn NtQuerySystemInformation (
system_information_class : u32 ,
system_information : * mut core ::ffi ::c_void ,
system_information_length : u32 ,
return_length : * mut u32 ,
) -> i32 ;
}
2026-07-16 00:52:21 +01:00
const WINDOWS_TO_UNIX_100NS : u64 = 116_444_736_000_000_000 ;
const SYNCHRONIZE_ACCESS : u32 = 0x0010_0000 ;
struct OwnedHandle ( windows_sys ::Win32 ::Foundation ::HANDLE ) ;
impl Drop for OwnedHandle {
fn drop ( & mut self ) {
unsafe { CloseHandle ( self . 0 ) } ;
2026-07-12 13:32:47 +01:00
}
2026-07-16 00:52:21 +01:00
}
fn filetime_token ( value : FILETIME ) -> u64 {
( ( value . dwHighDateTime as u64 ) < < 32 ) | value . dwLowDateTime as u64
}
fn rfc3339_from_filetime ( token : u64 ) -> Result < String > {
let unix_100ns = token
. checked_sub ( WINDOWS_TO_UNIX_100NS )
. context ( " process creation FILETIME predates Unix epoch " ) ? ;
let seconds = ( unix_100ns / 10_000_000 ) as i64 ;
let nanos = ( ( unix_100ns % 10_000_000 ) * 100 ) as u32 ;
let value = DateTime ::< Utc > ::from_timestamp ( seconds , nanos )
. context ( " process creation FILETIME is out of range " ) ? ;
Ok ( value . to_rfc3339_opts ( SecondsFormat ::Nanos , true ) )
}
fn pid_in_snapshot ( process_id : u32 ) -> Result < bool > {
unsafe {
let snapshot = CreateToolhelp32Snapshot ( TH32CS_SNAPPROCESS , 0 ) ;
if snapshot = = INVALID_HANDLE_VALUE {
return Err ( std ::io ::Error ::last_os_error ( ) ) . context ( " create process snapshot " ) ;
}
let snapshot = OwnedHandle ( snapshot ) ;
let mut entry : PROCESSENTRY32W = std ::mem ::zeroed ( ) ;
entry . dwSize = std ::mem ::size_of ::< PROCESSENTRY32W > ( ) as u32 ;
if Process32FirstW ( snapshot . 0 , & mut entry ) = = 0 {
return Err ( std ::io ::Error ::last_os_error ( ) ) . context ( " read process snapshot " ) ;
}
loop {
if entry . th32ProcessID = = process_id {
return Ok ( true ) ;
}
if Process32NextW ( snapshot . 0 , & mut entry ) = = 0 {
let error = GetLastError ( ) ;
if error = = ERROR_NO_MORE_FILES {
return Ok ( false ) ;
}
return Err ( std ::io ::Error ::from_raw_os_error ( error as i32 ) )
. context ( " advance process snapshot " ) ;
}
}
2026-07-12 13:32:47 +01:00
}
2026-07-16 00:52:21 +01:00
}
fn open ( process_id : u32 ) -> Result < OwnedHandle > {
unsafe {
let handle = OpenProcess (
PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE_ACCESS ,
0 ,
process_id ,
) ;
if handle . is_null ( ) {
return Err ( std ::io ::Error ::last_os_error ( ) )
. with_context ( | | format! ( " open process {process_id} " ) ) ;
}
Ok ( OwnedHandle ( handle ) )
}
}
fn identity_from_handle (
process_id : u32 ,
handle : & OwnedHandle ,
) -> Result < ProcessInstanceIdentity > {
unsafe {
let mut creation : FILETIME = std ::mem ::zeroed ( ) ;
let mut exit : FILETIME = std ::mem ::zeroed ( ) ;
let mut kernel : FILETIME = std ::mem ::zeroed ( ) ;
let mut user : FILETIME = std ::mem ::zeroed ( ) ;
if GetProcessTimes ( handle . 0 , & mut creation , & mut exit , & mut kernel , & mut user ) = = 0 {
return Err ( std ::io ::Error ::last_os_error ( ) ) . context ( " query process creation time " ) ;
}
let creation_token = filetime_token ( creation ) ;
if creation_token = = 0 {
bail! ( " operating system returned a zero process creation token " ) ;
}
let mut buffer = vec! [ 0_ u16 ; 32_768 ] ;
let mut length = buffer . len ( ) as u32 ;
if QueryFullProcessImageNameW ( handle . 0 , 0 , buffer . as_mut_ptr ( ) , & mut length ) = = 0 {
return Err ( std ::io ::Error ::last_os_error ( ) )
. context ( " query process executable path " ) ;
}
buffer . truncate ( length as usize ) ;
let raw = PathBuf ::from ( std ::ffi ::OsString ::from_wide ( & buffer ) ) ;
let executable_path = std ::fs ::canonicalize ( & raw )
. with_context ( | | format! ( " canonicalize process executable {} " , raw . display ( ) ) ) ? ;
Ok ( ProcessInstanceIdentity {
process_id ,
creation_token ,
created_at_rfc3339 : Some ( rfc3339_from_filetime ( creation_token ) ? ) ,
executable_path ,
} )
}
}
pub ( super ) fn capture ( process_id : u32 ) -> Result < ProcessInstanceIdentity > {
identity_from_handle ( process_id , & open ( process_id ) ? )
}
pub ( super ) fn observe ( expected : & ProcessInstanceIdentity ) -> ProcessInstanceObservation {
let present = match pid_in_snapshot ( expected . process_id ) {
Ok ( value ) = > value ,
Err ( error ) = > {
return ProcessInstanceObservation ::Indeterminate {
reason : error . to_string ( ) ,
} ;
}
} ;
if ! present {
return ProcessInstanceObservation ::Missing ;
}
let handle = match open ( expected . process_id ) {
Ok ( handle ) = > handle ,
Err ( error ) = > {
return if error
. downcast_ref ::< std ::io ::Error > ( )
. is_some_and ( | e | e . raw_os_error ( ) = = Some ( ERROR_ACCESS_DENIED as i32 ) )
{
ProcessInstanceObservation ::Inaccessible
} else {
ProcessInstanceObservation ::Indeterminate {
reason : error . to_string ( ) ,
}
} ;
}
} ;
let observed = match identity_from_handle ( expected . process_id , & handle ) {
Ok ( observed ) if observed ! = * expected = > {
return ProcessInstanceObservation ::Replaced { observed } ;
}
Ok ( observed ) = > observed ,
Err ( error ) = > {
return ProcessInstanceObservation ::Indeterminate {
reason : error . to_string ( ) ,
} ;
}
} ;
debug_assert_eq! ( observed , * expected ) ;
unsafe {
match WaitForSingleObject ( handle . 0 , 0 ) {
WAIT_OBJECT_0 = > {
let mut code = 0 ;
let exit_code = ( GetExitCodeProcess ( handle . 0 , & mut code ) ! = 0 ) . then_some ( code ) ;
ProcessInstanceObservation ::ExactExited { exit_code }
}
WAIT_TIMEOUT = > ProcessInstanceObservation ::ExactAlive ,
WAIT_FAILED = > ProcessInstanceObservation ::Indeterminate {
reason : format ! (
" wait for process failed with Windows error {} " ,
GetLastError ( )
) ,
} ,
other = > ProcessInstanceObservation ::Indeterminate {
reason : format ! ( " unexpected process wait result {other} " ) ,
} ,
}
}
}
pub ( super ) fn boot_identity ( ) -> Option < String > {
2026-07-16 03:24:54 +01:00
let mut information = SystemTimeOfDayInformation {
boot_time : 0 ,
current_time : 0 ,
time_zone_bias : 0 ,
current_time_zone_id : 0 ,
reserved : 0 ,
boot_time_bias : 0 ,
sleep_time_bias : 0 ,
} ;
let status = unsafe {
NtQuerySystemInformation (
3 ,
( & mut information as * mut SystemTimeOfDayInformation ) . cast ( ) ,
std ::mem ::size_of ::< SystemTimeOfDayInformation > ( ) as u32 ,
std ::ptr ::null_mut ( ) ,
2026-07-16 00:52:21 +01:00
)
2026-07-16 03:24:54 +01:00
} ;
( status > = 0 & & information . boot_time > 0 )
. then ( | | format! ( " windows-kernel-boot-filetime: {} " , information . boot_time ) )
2026-07-16 00:52:21 +01:00
}
pub ( super ) fn is_missing_error ( error : & anyhow ::Error ) -> bool {
error . chain ( ) . any ( | source | {
source . downcast_ref ::< std ::io ::Error > ( ) . is_some_and ( | e | {
matches! ( e . raw_os_error ( ) , Some ( code ) if code = = ERROR_INVALID_PARAMETER as i32 )
} )
2026-07-12 13:32:47 +01:00
} )
}
}
2026-07-16 00:52:21 +01:00
#[ cfg(target_os = " linux " ) ]
mod platform {
use super ::* ;
fn stat ( process_id : u32 ) -> Result < ( u64 , char ) > {
let raw = std ::fs ::read_to_string ( format! ( " /proc/ {process_id} /stat " ) )
. with_context ( | | format! ( " read /proc/ {process_id} /stat " ) ) ? ;
let close = raw
. rfind ( " ) " )
. context ( " malformed proc stat command field " ) ? ;
let fields : Vec < & str > = raw [ close + 2 .. ] . split_whitespace ( ) . collect ( ) ;
let state = fields
. first ( )
. and_then ( | value | value . chars ( ) . next ( ) )
. context ( " missing proc state " ) ? ;
// starttime is field 22; `fields[0]` is original field 3.
let starttime = fields . get ( 19 ) . context ( " missing proc starttime " ) ? . parse ( ) ? ;
Ok ( ( starttime , state ) )
}
pub ( super ) fn capture ( process_id : u32 ) -> Result < ProcessInstanceIdentity > {
let ( creation_token , _ ) = stat ( process_id ) ? ;
if creation_token = = 0 {
bail! ( " operating system returned a zero process creation token " ) ;
}
let executable_path = std ::fs ::canonicalize ( format! ( " /proc/ {process_id} /exe " ) )
. with_context ( | | format! ( " read process {process_id} executable " ) ) ? ;
Ok ( ProcessInstanceIdentity {
process_id ,
creation_token ,
created_at_rfc3339 : None ,
executable_path ,
} )
}
pub ( super ) fn observe ( expected : & ProcessInstanceIdentity ) -> ProcessInstanceObservation {
let ( creation_token , state ) = match stat ( expected . process_id ) {
Ok ( observed ) = > observed ,
Err ( error ) if is_missing_error ( & error ) = > return ProcessInstanceObservation ::Missing ,
Err ( error ) = > {
return ProcessInstanceObservation ::Indeterminate {
reason : error . to_string ( ) ,
} ;
}
} ;
if creation_token = = expected . creation_token & & matches! ( state , 'Z' | 'X' ) {
return ProcessInstanceObservation ::ExactExited { exit_code : None } ;
}
match capture ( expected . process_id ) {
Ok ( observed ) if observed ! = * expected = > {
ProcessInstanceObservation ::Replaced { observed }
}
Ok ( _ ) = > ProcessInstanceObservation ::ExactAlive ,
Err ( error ) = > ProcessInstanceObservation ::Indeterminate {
reason : error . to_string ( ) ,
} ,
2026-07-12 13:32:47 +01:00
}
}
2026-07-16 00:52:21 +01:00
pub ( super ) fn boot_identity ( ) -> Option < String > {
std ::fs ::read_to_string ( " /proc/sys/kernel/random/boot_id " )
. ok ( )
. map ( | s | s . trim ( ) . to_string ( ) )
. filter ( | s | ! s . is_empty ( ) )
}
pub ( super ) fn is_missing_error ( error : & anyhow ::Error ) -> bool {
error . chain ( ) . any ( | source | {
source
. downcast_ref ::< std ::io ::Error > ( )
. is_some_and ( | e | e . kind ( ) = = std ::io ::ErrorKind ::NotFound )
} )
}
}
#[ cfg(not(any(windows, target_os = " linux " ))) ]
mod platform {
use super ::* ;
pub ( super ) fn capture ( _ : u32 ) -> Result < ProcessInstanceIdentity > {
bail! ( " native process-instance observation is unsupported on this platform " )
}
pub ( super ) fn observe ( _ : & ProcessInstanceIdentity ) -> ProcessInstanceObservation {
ProcessInstanceObservation ::Indeterminate {
reason : " native process-instance observation is unsupported on this platform "
. to_string ( ) ,
}
}
pub ( super ) fn boot_identity ( ) -> Option < String > {
None
}
pub ( super ) fn is_missing_error ( _ : & anyhow ::Error ) -> bool {
false
}
}
#[ cfg(test) ]
mod tests {
use super ::* ;
#[ test ]
fn rejects_zero_pid ( ) {
assert! ( capture_process_instance ( 0 ) . is_err ( ) ) ;
}
2026-07-16 03:24:54 +01:00
#[ test ]
fn native_boot_identity_is_available_and_stable ( ) {
let first = native_boot_identity ( ) . expect ( " native boot identity unavailable " ) ;
let second = native_boot_identity ( ) . expect ( " native boot identity disappeared " ) ;
assert_eq! ( first , second ) ;
}
2026-07-16 00:52:21 +01:00
#[ test ]
fn live_current_process_has_stable_exact_identity ( ) {
let identity = capture_process_instance ( std ::process ::id ( ) ) . unwrap ( ) ;
assert_ne! ( identity . creation_token , 0 ) ;
assert! ( identity . executable_path . is_absolute ( ) ) ;
assert_eq! (
observe_process_instance ( & identity ) ,
ProcessInstanceObservation ::ExactAlive
) ;
}
#[ test ]
fn forged_incarnation_is_replaced_not_alive ( ) {
let mut identity = capture_process_instance ( std ::process ::id ( ) ) . unwrap ( ) ;
identity . creation_token = identity . creation_token . saturating_add ( 1 ) ;
assert! ( matches! (
observe_process_instance ( & identity ) ,
ProcessInstanceObservation ::Replaced { .. }
) ) ;
}
#[ test ]
fn child_exit_is_observed_for_the_exact_instance ( ) {
let mut child = if cfg! ( windows ) {
std ::process ::Command ::new ( " powershell.exe " )
. args ( [
" -NoProfile " ,
" -NonInteractive " ,
" -Command " ,
" Start-Sleep -Milliseconds 500; exit 17 " ,
] )
. spawn ( )
. unwrap ( )
} else {
std ::process ::Command ::new ( " sh " )
. args ( [ " -c " , " exit 17 " ] )
. spawn ( )
. unwrap ( )
} ;
let identity = capture_process_instance ( child . id ( ) ) . unwrap ( ) ;
let _ = child . wait ( ) . unwrap ( ) ;
let observed = observe_process_instance ( & identity ) ;
assert! ( matches! (
observed ,
ProcessInstanceObservation ::ExactExited {
exit_code : Some ( 17 )
} | ProcessInstanceObservation ::Missing
) ) ;
}
2026-07-12 13:32:47 +01:00
}