git_policy_cli/process/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.
19/// Three kinds of record are judged by this age. A merge's intent record (`merge_isolation_journal.rs`, `write_merge_intent`,
20/// written once and synced per merge) and a claim file (`single_owner_claim.rs`, `write_private`, written once) are not
21/// rewritten while the owner works, so the age is the owner's whole hold; the item path of a worktree add is judged by its own
22/// modification time (`worktree_copy_owner.rs`, `is_alive`), which the add's work can move, so its age is at most the add's
23/// length. The patience must
24/// outlast the longest such hold. Measured: the longest worktree add in the add timing log (48 adds, 2026-11-03T08:55Z to
25/// 15:06Z, `totalMs`) took 539.7 s, and the nearest log of a command that waits, the commit timing log (1,051 commits,
26/// 2026-11-02T09:01:37Z to 2026-11-03T15:10:12Z), has a longest of 750.6 s. No log records the length of a merge, which waits up
27/// to the 1,800 s live lock wait (`wait_limit.rs`, `DEFAULT_LIVE_WAIT`) with its record already written, so an hour is twice the
28/// longest wait the code allows and 4.8 times the longest command measured. If that wait is lengthened or removed, this value
29/// must be revisited with it. The logs hold commands of the host's own PID namespace, so no foreign-namespace hold was observed
30/// directly. Kept at an hour as the coordinator's pick under the owner's delegation, open to the owner's veto
31/// (`doc/planning/cli-git-shipped-limits-audit.md`, "Foreign-owner patience").
32pub const FOREIGN_OWNER_PATIENCE: Duration = Duration::from_secs(3600);
33
34/// What:
35/// The identity of this process's PID namespace, such as `pid:[4026531836]`, or nothing where the system has none.
36/// Why:
37/// The kernel names a namespace by an inode; two processes share a PID space exactly when the names are equal.
38///
39/// In TS you'd write (pseudocode):
40/// ```ts
41/// readlinkSync('/proc/self/ns/pid')
42/// ```
43pub fn current_pid_namespace() -> Option<String> {
44 #[cfg(target_os = "linux")]
45 {
46 return std::fs::read_link("/proc/self/ns/pid")
47 .ok()
48 .and_then(|target| { return target.into_os_string().into_string().ok(); });
49 }
50 #[cfg(not(target_os = "linux"))]
51 {
52 return None;
53 }
54}
55
56/// What:
57/// Whether a recorded namespace and the reader's namespace are both known and differ.
58/// Why:
59/// Without both names nothing proves the PID space differs, so the PID probe keeps its old meaning; a record
60/// written before this field existed carries no name.
61///
62/// In TS you'd write (pseudocode):
63/// ```ts
64/// recorded !== undefined && current !== undefined && recorded !== current
65/// ```
66pub fn is_other_namespace(recorded: Option<&str>, current: Option<&str>) -> bool {
67 return match (recorded, current) {
68 (Some(recorded_name), Some(current_name)) => recorded_name != current_name,
69 _ => false,
70 };
71}
72
73/// What:
74/// Whether a record written by an owner of another namespace is old enough to stop counting as alive.
75/// Why:
76/// The record file's modification time stands for when the owner started; a record that cannot be measured keeps
77/// its owner alive, because the wrapper never retires what it cannot prove dead.
78///
79/// In TS you'd write (pseudocode):
80/// ```ts
81/// Date.now() - statSync(recordPath).mtimeMs > FOREIGN_OWNER_PATIENCE_MS
82/// ```
83pub fn foreign_record_has_expired(record_path: &Path) -> bool {
84 let Ok(modified) = std::fs::metadata(record_path).and_then(|metadata| { return metadata.modified(); }) else {
85 return false;
86 };
87 return match SystemTime::now().duration_since(modified) {
88 Ok(age) => age_outlives_patience(age),
89 Err(_) => false,
90 };
91}
92
93/// What:
94/// Whether a record of this age has outlived the patience; exactly the patience still counts as alive.
95/// Why:
96/// The boundary is a fact of the rule, so it is separated from the clock to be checked exactly.
97///
98/// In TS you'd write (pseudocode):
99/// ```ts
100/// age > FOREIGN_OWNER_PATIENCE_MS
101/// ```
102pub fn age_outlives_patience(age: Duration) -> bool {
103 return age > FOREIGN_OWNER_PATIENCE;
104}
105
106/// What:
107/// Whether an owner recorded in `recorded` counts as alive without any PID probe, or nothing when the PID decides.
108/// Why:
109/// Callers share one rule: an owner of another namespace is alive until its record outlives the patience.
110///
111/// In TS you'd write (pseudocode):
112/// ```ts
113/// isOtherNamespace(recorded, current) ? !hasExpired(recordPath) : undefined
114/// ```
115pub fn foreign_owner_standing(recorded: Option<&str>, record_path: &Path) -> Option<bool> {
116 if !is_other_namespace(recorded, current_pid_namespace().as_deref()) {
117 return None;
118 }
119 return Some(!foreign_record_has_expired(record_path));
120}
121
122/// Namespace controls stay out of the release executable.
123#[cfg(test)]
124#[path = "pid_namespace_tests.rs"]
125mod tests;