Skip to main content

git_policy_cli/
pid_namespace.rs

1//! What:
2//!  The PID namespace an owner record names, and the rule for judging an owner that lives in another one.
3//! Why:
4//!  A PID means something only inside its PID namespace. A wrapper in a container that probes a host owner's PID
5//!  finds no process (or an unrelated one), so it would judge a live owner dead and recover or retire what the owner
6//!  still holds (issues 1781 and 1784). A record therefore names its namespace, and an owner of another namespace is
7//!  judged by the age of its record instead of by the PID.
8//!
9//! In TS you'd write (pseudocode):
10//! ```ts
11//! const sameNamespace = readlinkSync('/proc/self/ns/pid') === record.ownerPidNamespace;
12//! ```
13
14use std::path::Path;
15use std::time::{Duration, SystemTime};
16
17/// How long an owner of another PID namespace counts as alive, measured from when its record was written.
18///  A commit or a lock hold lasts seconds to minutes; a crashed foreign owner stops blocking after this long.
19pub const FOREIGN_OWNER_PATIENCE: Duration = Duration::from_secs(3600);
20
21/// What:
22///  The identity of this process's PID namespace, such as `pid:[4026531836]`, or nothing where the system has none.
23/// Why:
24///   The kernel names a namespace by an inode; two processes share a PID space exactly when the names are equal.
25///
26/// In TS you'd write (pseudocode):
27/// ```ts
28/// readlinkSync('/proc/self/ns/pid')
29/// ```
30pub fn current_pid_namespace() -> Option<String> {
31    #[cfg(target_os = "linux")]
32    {
33        return std::fs::read_link("/proc/self/ns/pid")
34            .ok()
35            .and_then(|target| { return target.into_os_string().into_string().ok(); });
36    }
37    #[cfg(not(target_os = "linux"))]
38    {
39        return None;
40    }
41}
42
43/// What:
44///  Whether a recorded namespace and the reader's namespace are both known and differ.
45/// Why:
46///   Without both names nothing proves the PID space differs, so the PID probe keeps its old meaning; a record
47///  written before this field existed carries no name.
48///
49/// In TS you'd write (pseudocode):
50/// ```ts
51/// recorded !== undefined && current !== undefined && recorded !== current
52/// ```
53pub fn is_other_namespace(recorded: Option<&str>, current: Option<&str>) -> bool {
54    return match (recorded, current) {
55        (Some(recorded_name), Some(current_name)) => recorded_name != current_name,
56        _ => false,
57    };
58}
59
60/// What:
61///  Whether a record written by an owner of another namespace is old enough to stop counting as alive.
62/// Why:
63///   The record file's modification time stands for when the owner started; a record that cannot be measured keeps
64///  its owner alive, because the wrapper never retires what it cannot prove dead.
65///
66/// In TS you'd write (pseudocode):
67/// ```ts
68/// Date.now() - statSync(recordPath).mtimeMs > FOREIGN_OWNER_PATIENCE_MS
69/// ```
70pub fn foreign_record_has_expired(record_path: &Path) -> bool {
71    let Ok(modified) = std::fs::metadata(record_path).and_then(|metadata| { return metadata.modified(); }) else {
72        return false;
73    };
74    return match SystemTime::now().duration_since(modified) {
75        Ok(age) => age_outlives_patience(age),
76        Err(_) => false,
77    };
78}
79
80/// What:
81///  Whether a record of this age has outlived the patience; exactly the patience still counts as alive.
82/// Why:
83///   The boundary is a fact of the rule, so it is separated from the clock to be checked exactly.
84///
85/// In TS you'd write (pseudocode):
86/// ```ts
87/// age > FOREIGN_OWNER_PATIENCE_MS
88/// ```
89pub fn age_outlives_patience(age: Duration) -> bool {
90    return age > FOREIGN_OWNER_PATIENCE;
91}
92
93/// What:
94///  Whether an owner recorded in `recorded` counts as alive without any PID probe, or nothing when the PID decides.
95/// Why:
96///   Callers share one rule: an owner of another namespace is alive until its record outlives the patience.
97///
98/// In TS you'd write (pseudocode):
99/// ```ts
100/// isOtherNamespace(recorded, current) ? !hasExpired(recordPath) : undefined
101/// ```
102pub fn foreign_owner_standing(recorded: Option<&str>, record_path: &Path) -> Option<bool> {
103    if !is_other_namespace(recorded, current_pid_namespace().as_deref()) {
104        return None;
105    }
106    return Some(!foreign_record_has_expired(record_path));
107}
108
109/// Namespace controls stay out of the release executable.
110#[cfg(test)]
111#[path = "pid_namespace_tests.rs"]
112mod tests;