Skip to main content

running_process/broker/backend_lifecycle/
verify_pid.rs

1//! Process identity verification for backend handles.
2
3use std::io;
4use std::path::PathBuf;
5
6use crate::broker::backend_lifecycle::identity::{self, DaemonProcess};
7use crate::broker::host_identity;
8use crate::platform::process::{self, ProcessInspectError, ProcessInspectErrorKind};
9
10/// Verify a daemon process identity and return an OS liveness handle.
11pub fn verify_daemon_process(expected: &DaemonProcess) -> Result<ProcessHandle, VerifyPidError> {
12    verify_daemon_with_opener(expected, open_handle)
13}
14
15/// Verify identity while retaining rights to terminate that same process object.
16pub fn verify_daemon_process_for_control(
17    expected: &DaemonProcess,
18) -> Result<ProcessHandle, VerifyPidError> {
19    verify_daemon_with_opener(expected, |pid| {
20        ProcessHandle::open_for_control(pid).map_err(|error| VerifyPidError::Handle {
21            pid,
22            source: error.source,
23        })
24    })
25}
26
27fn verify_daemon_with_opener(
28    expected: &DaemonProcess,
29    open: impl FnOnce(u32) -> Result<ProcessHandle, VerifyPidError>,
30) -> Result<ProcessHandle, VerifyPidError> {
31    if expected.pid == 0 {
32        return Err(VerifyPidError::InvalidPid(expected.pid));
33    }
34
35    let current_boot_id = host_identity::current().boot_id;
36    if !expected.boot_id.is_empty()
37        && !current_boot_id.is_empty()
38        && expected.boot_id != current_boot_id
39    {
40        return Err(VerifyPidError::BootIdMismatch {
41            expected: expected.boot_id.clone(),
42            actual: current_boot_id,
43        });
44    }
45
46    let handle = open(expected.pid)?;
47    let exe_path =
48        process::executable_path(expected.pid).map_err(|source| VerifyPidError::ExePath {
49            pid: expected.pid,
50            source,
51        })?;
52    if !process::same_executable_path(&exe_path, &expected.exe_path) {
53        return Err(VerifyPidError::ExePathMismatch {
54            pid: expected.pid,
55            expected: expected.exe_path.clone(),
56            actual: exe_path,
57        });
58    }
59
60    let actual_hash =
61        identity::executable_hash_file(&exe_path).map_err(|source| VerifyPidError::ExeHash {
62            pid: expected.pid,
63            path: exe_path.clone(),
64            source,
65        })?;
66    if actual_hash != expected.exe_hash {
67        return Err(VerifyPidError::ExecutableHashMismatch { pid: expected.pid });
68    }
69
70    if !handle.is_alive() {
71        return Err(VerifyPidError::NotFound { pid: expected.pid });
72    }
73    Ok(handle)
74}
75
76/// Return whether a process ID currently resolves to a live process.
77pub fn process_is_alive(pid: u32) -> bool {
78    ProcessHandle::open(pid).is_ok_and(|handle| handle.is_alive())
79}
80
81/// Send a graceful terminate signal where the platform has one.
82pub fn signal_terminate(pid: u32) -> Result<(), VerifyPidError> {
83    process::signal_terminate(pid).map_err(|error| translate(pid, error))
84}
85
86/// Force-kill a process ID.
87pub fn force_kill_pid(pid: u32) -> Result<(), VerifyPidError> {
88    process::force_kill(pid).map_err(|error| translate(pid, error))
89}
90
91/// Force termination through a retained process object, never a reopened PID.
92pub fn force_kill_handle(handle: &ProcessHandle) -> Result<(), VerifyPidError> {
93    handle
94        .force_kill()
95        .map_err(|source| VerifyPidError::Handle {
96            pid: handle.pid(),
97            source,
98        })
99}
100
101/// Errors returned while verifying a daemon process.
102#[derive(Debug, thiserror::Error)]
103pub enum VerifyPidError {
104    /// PID zero or a value outside the native PID range is never valid.
105    #[error("invalid daemon pid: {0}")]
106    InvalidPid(u32),
107    /// The process is not currently alive.
108    #[error("process not found: {pid}")]
109    NotFound {
110        /// Process ID that could not be opened.
111        pid: u32,
112    },
113    /// The manifest was written during a prior host boot.
114    #[error("daemon boot id mismatch: expected {expected}, current {actual}")]
115    BootIdMismatch {
116        /// Boot ID stored with the daemon identity.
117        expected: String,
118        /// Current host boot ID.
119        actual: String,
120    },
121    /// The executable could not be hashed.
122    #[error("failed to hash executable for pid {pid} at {path:?}: {source}")]
123    ExeHash {
124        /// Process ID being verified.
125        pid: u32,
126        /// Executable path selected for hashing.
127        path: PathBuf,
128        /// Underlying I/O error.
129        source: io::Error,
130    },
131    /// The executable path for the process could not be read.
132    #[error("failed to resolve executable path for pid {pid}: {source}")]
133    ExePath {
134        /// Process ID being verified.
135        pid: u32,
136        /// Underlying platform error.
137        source: io::Error,
138    },
139    /// The executable path did not match the manifest identity.
140    #[error(
141        "daemon executable path mismatch for pid {pid}: expected {expected:?}, actual {actual:?}"
142    )]
143    ExePathMismatch {
144        /// Process ID being verified.
145        pid: u32,
146        /// Executable path stored with the daemon identity.
147        expected: PathBuf,
148        /// Executable path reported by the operating system.
149        actual: PathBuf,
150    },
151    /// The executable hash did not match the manifest identity.
152    #[error("daemon executable blake3 hash mismatch for pid {pid}")]
153    ExecutableHashMismatch {
154        /// Process ID being verified.
155        pid: u32,
156    },
157    /// A platform process-handle operation failed.
158    #[error("process handle operation failed for pid {pid}: {source}")]
159    Handle {
160        /// Process ID being opened or signalled.
161        pid: u32,
162        /// Underlying platform error.
163        source: io::Error,
164    },
165    /// The platform has no graceful shutdown primitive in this foundation.
166    #[error("graceful terminate is unsupported on this platform")]
167    GracefulTerminateUnsupported,
168}
169
170/// The OS liveness handle this module hands out.
171///
172/// It is the facade's handle: the ownership rules that make it trustworthy --
173/// a pidfd, a kqueue subscription, an open process handle -- belong to the
174/// host that issued it, not to this module's vocabulary.
175pub use crate::platform::process::ProcessLiveness as ProcessHandle;
176
177fn open_handle(pid: u32) -> Result<ProcessHandle, VerifyPidError> {
178    ProcessHandle::open(pid).map_err(|error| translate(pid, error))
179}
180
181/// Say a host's answer in this module's vocabulary.
182///
183/// The three named kinds each have a variant here that predates the facade
184/// and that callers already match on. `Host` has no such variant because it
185/// is not a classification -- it is the host's own error, and it is carried
186/// through whole rather than being given a name it does not have.
187fn translate(pid: u32, error: ProcessInspectError) -> VerifyPidError {
188    match error.kind {
189        ProcessInspectErrorKind::InvalidPid => VerifyPidError::InvalidPid(pid),
190        ProcessInspectErrorKind::NotFound => VerifyPidError::NotFound { pid },
191        ProcessInspectErrorKind::Unsupported => VerifyPidError::GracefulTerminateUnsupported,
192        ProcessInspectErrorKind::Host => VerifyPidError::Handle {
193            pid,
194            source: error.source,
195        },
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202
203    /// Every kind a host can report maps onto a variant callers already match.
204    ///
205    /// Walking the facade's own kinds rather than a local restatement of them
206    /// means a new kind stops this compiling until someone decides what this
207    /// module should call it.
208    #[test]
209    fn every_host_kind_has_a_name_here() {
210        let staged = |kind| ProcessInspectError {
211            kind,
212            source: io::Error::from_raw_os_error(1),
213        };
214        assert!(matches!(
215            translate(7, staged(ProcessInspectErrorKind::InvalidPid)),
216            VerifyPidError::InvalidPid(7)
217        ));
218        assert!(matches!(
219            translate(7, staged(ProcessInspectErrorKind::NotFound)),
220            VerifyPidError::NotFound { pid: 7 }
221        ));
222        assert!(matches!(
223            translate(7, staged(ProcessInspectErrorKind::Unsupported)),
224            VerifyPidError::GracefulTerminateUnsupported
225        ));
226        assert!(matches!(
227            translate(7, staged(ProcessInspectErrorKind::Host)),
228            VerifyPidError::Handle { pid: 7, .. }
229        ));
230    }
231
232    /// The host's error survives translation.
233    ///
234    /// `Handle` exists so an operator can read what the kernel actually said;
235    /// replacing it with a message composed here would defeat that.
236    #[test]
237    fn a_host_error_is_carried_through_whole() {
238        let error = translate(
239            7,
240            ProcessInspectError {
241                kind: ProcessInspectErrorKind::Host,
242                source: io::Error::from_raw_os_error(13),
243            },
244        );
245        let VerifyPidError::Handle { source, .. } = error else {
246            panic!("expected a handle error");
247        };
248        assert_eq!(source.raw_os_error(), Some(13));
249    }
250
251    /// This process is alive; a PID that names nothing is not.
252    #[test]
253    fn liveness_answers_for_this_process() {
254        assert!(process_is_alive(std::process::id()));
255        assert!(!process_is_alive(0));
256    }
257}