Skip to main content

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