Skip to main content

aion_server/control/
incarnation.rs

1//! Incarnation identity: proving that the process wearing a recorded pid is
2//! the server that recorded it.
3//!
4//! A pid alone is a name the operating system recycles; signalling by bare
5//! pid is how a stop verb kills a stranger. The discriminator is the process
6//! START INSTANT, read through the same instrument ([`sysinfo`]) on both the
7//! write side (the server records its own start instant at boot) and the
8//! verify side (the stop/status verbs read the live process table), so the
9//! comparison is exact equality on one instrument's answer — never clock
10//! arithmetic between two instruments. A reused pid wears a different start
11//! instant; a dead pid wears none.
12
13use std::path::PathBuf;
14
15use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System};
16
17use super::pid_file::PidRecord;
18use crate::error::ServerError;
19
20/// What this process knows about itself at boot: the fields of the pid
21/// record that come from the process table and the binary on disk.
22#[derive(Clone, Debug)]
23pub struct SelfIdentity {
24    /// This process's id.
25    pub pid: u32,
26    /// This process's start instant in whole seconds since the Unix epoch,
27    /// as the process-table instrument reports it.
28    pub started_at_unix_secs: u64,
29    /// SHA-256 of the executable's bytes at the path the OS reports for
30    /// this process.
31    pub binary_sha256: String,
32}
33
34/// The live process table's answer about a recorded incarnation.
35#[derive(Clone, Debug, Eq, PartialEq)]
36pub enum IncarnationProbe {
37    /// No process wears the recorded pid: the recorded server is gone.
38    ProcessGone,
39    /// A process wears the pid and its start instant MATCHES the record:
40    /// this is the recorded server, still running.
41    Verified {
42        /// Executable path the process table reports for the live process,
43        /// carried for the report; `None` when the table does not know it.
44        exe: Option<PathBuf>,
45    },
46    /// A process wears the pid but its start instant does not match the
47    /// record: the pid was recycled onto a different process after the
48    /// recorded server died. Never signal it.
49    DifferentIncarnation {
50        /// Start instant of the live process now wearing the pid.
51        live_started_at_unix_secs: u64,
52        /// Executable path of the live process, for the refusal report.
53        exe: Option<PathBuf>,
54    },
55}
56
57/// Read this process's own identity at boot.
58///
59/// # Errors
60///
61/// Returns [`ServerError::Incarnation`] when the process table does not
62/// answer for our own pid (which would leave the pid record unable to carry
63/// the start instant every later verification depends on), when the
64/// executable path cannot be resolved, or when the binary cannot be read for
65/// hashing.
66pub fn self_identity() -> Result<SelfIdentity, ServerError> {
67    let pid = std::process::id();
68    let started_at_unix_secs =
69        process_start_instant(pid).ok_or_else(|| ServerError::Incarnation {
70            message: format!(
71                "the process table has no entry for this process (pid {pid}); \
72                 cannot record a verifiable incarnation"
73            ),
74        })?;
75    let exe = std::env::current_exe().map_err(|io_error| ServerError::Incarnation {
76        message: format!("cannot resolve this process's executable path: {io_error}"),
77    })?;
78    let bytes = std::fs::read(&exe).map_err(|io_error| ServerError::Incarnation {
79        message: format!(
80            "cannot read this process's executable `{}` for content hashing: {io_error}",
81            exe.display()
82        ),
83    })?;
84    Ok(SelfIdentity {
85        pid,
86        started_at_unix_secs,
87        binary_sha256: sha256_hex(&bytes),
88    })
89}
90
91/// Probe the live process table for the incarnation a record names.
92#[must_use]
93pub fn probe(record: &PidRecord) -> IncarnationProbe {
94    let mut system = System::new();
95    let pid = sysinfo::Pid::from_u32(record.pid);
96    system.refresh_processes_specifics(
97        ProcessesToUpdate::Some(&[pid]),
98        true,
99        ProcessRefreshKind::nothing().with_exe(sysinfo::UpdateKind::Always),
100    );
101    let Some(process) = system.process(pid) else {
102        return IncarnationProbe::ProcessGone;
103    };
104    let live_started_at_unix_secs = process.start_time();
105    let exe = process.exe().map(std::path::Path::to_path_buf);
106    if live_started_at_unix_secs == record.started_at_unix_secs {
107        IncarnationProbe::Verified { exe }
108    } else {
109        IncarnationProbe::DifferentIncarnation {
110            live_started_at_unix_secs,
111            exe,
112        }
113    }
114}
115
116/// The start instant the process table reports for `pid`, or `None` when no
117/// such process exists.
118fn process_start_instant(pid: u32) -> Option<u64> {
119    let mut system = System::new();
120    let pid = sysinfo::Pid::from_u32(pid);
121    system.refresh_processes_specifics(
122        ProcessesToUpdate::Some(&[pid]),
123        true,
124        ProcessRefreshKind::nothing(),
125    );
126    system.process(pid).map(sysinfo::Process::start_time)
127}
128
129/// Lowercase hex SHA-256 of `bytes`.
130fn sha256_hex(bytes: &[u8]) -> String {
131    use sha2::Digest as _;
132    const HEX: &[u8; 16] = b"0123456789abcdef";
133    let digest = sha2::Sha256::digest(bytes);
134    let mut hex = String::with_capacity(digest.len() * 2);
135    for byte in digest {
136        hex.push(char::from(HEX[usize::from(byte >> 4)]));
137        hex.push(char::from(HEX[usize::from(byte & 0x0f)]));
138    }
139    hex
140}
141
142#[cfg(test)]
143mod tests {
144    use super::{IncarnationProbe, probe, self_identity};
145    use crate::control::pid_file::PidRecord;
146
147    type TestResult = Result<(), Box<dyn std::error::Error>>;
148
149    fn record_with(
150        pid: u32,
151        started_at_unix_secs: u64,
152    ) -> Result<PidRecord, std::net::AddrParseError> {
153        Ok(PidRecord {
154            pid,
155            started_at_unix_secs,
156            binary_sha256: "0".repeat(64),
157            version: "0.0.0-test".to_owned(),
158            commit: "test".to_owned(),
159            http_address: "127.0.0.1:0".parse()?,
160            grpc_address: "127.0.0.1:0".parse()?,
161            drain_timeout_seconds: 30,
162        })
163    }
164
165    /// The green path proven against the ONLY process a test can trust to
166    /// exist: itself. Self-identity and a probe of the resulting record must
167    /// agree — the same-instrument property the module exists for.
168    #[test]
169    fn a_record_of_this_process_verifies() -> TestResult {
170        let me = self_identity()?;
171        let record = record_with(me.pid, me.started_at_unix_secs)?;
172        assert!(
173            matches!(probe(&record), IncarnationProbe::Verified { .. }),
174            "this process's own record must verify"
175        );
176        Ok(())
177    }
178
179    /// Red first: the SAME live pid with a different recorded start instant
180    /// is a different incarnation — the reused-pid face, which must never
181    /// verify and must carry the live instant for the refusal report.
182    #[test]
183    fn a_wrong_start_instant_on_a_live_pid_is_a_different_incarnation() -> TestResult {
184        let me = self_identity()?;
185        let record = record_with(me.pid, me.started_at_unix_secs.wrapping_add(7))?;
186        match probe(&record) {
187            IncarnationProbe::DifferentIncarnation {
188                live_started_at_unix_secs,
189                ..
190            } => {
191                assert_eq!(
192                    live_started_at_unix_secs, me.started_at_unix_secs,
193                    "the refusal must carry the LIVE process's instant"
194                );
195            }
196            other => {
197                return Err(format!(
198                    "a mismatched start instant must read as a different incarnation, got {other:?}"
199                )
200                .into());
201            }
202        }
203        Ok(())
204    }
205
206    /// Red first: a pid nothing wears is `ProcessGone`. Probing our own pid
207    /// space for a vacancy is inherently racy, so the specimen takes the
208    /// maximum pid-adjacent value a real allocator will not have handed out
209    /// during the test's lifetime — and asserts only the gone/alive axis.
210    #[test]
211    fn a_vacant_pid_is_process_gone() -> TestResult {
212        // Spawn a child and wait it to completion: its pid is proven
213        // vacated by the reap, not guessed.
214        let child = std::process::Command::new("true").spawn();
215        let mut child = match child {
216            Ok(child) => child,
217            Err(io_error) => return Err(format!("cannot spawn probe child: {io_error}").into()),
218        };
219        let pid = child.id();
220        child.wait()?;
221        let record = record_with(pid, 1)?;
222        // The reaped pid COULD have been recycled between wait and probe; a
223        // recycled wearer has a different start instant, so both honest
224        // answers are accepted — the specimen refuses only a false Verified.
225        match probe(&record) {
226            IncarnationProbe::ProcessGone | IncarnationProbe::DifferentIncarnation { .. } => Ok(()),
227            IncarnationProbe::Verified { .. } => {
228                Err("a reaped child's record must never verify".into())
229            }
230        }
231    }
232}