Skip to main content

rlmctl_core/guard/
mod.rs

1//! Freeze-guard engine: watch memory pressure and proactively freeze/soft-cap
2//! the non-protected app driving the pressure before the system locks up, healing
3//! itself once pressure clears. Pure engine + sampler live here; the daemon loop
4//! lives in the `rlm-guard` binary.
5
6use std::path::PathBuf;
7
8pub mod cgfs;
9pub mod effector;
10pub mod history;
11pub mod journal;
12pub mod lock;
13pub mod notify;
14pub mod policy;
15pub mod report;
16pub mod resolve;
17pub mod sampler;
18pub mod service;
19pub mod systemd;
20pub mod types;
21
22pub use effector::{Applied, Effector};
23pub use journal::Journal;
24pub use policy::PolicyEngine;
25pub use sampler::Sampler;
26pub use systemd::SystemdUser;
27pub use types::{Action, Intervention, Level, ProcInfo, PsiSource, Sample, Target};
28
29/// Path of a guard state file named `name`, in a per-user dir.
30///
31/// Prefers `$XDG_STATE_HOME/rlm` (`~/.local/state/rlm` by default), matching
32/// the XDG-first convention `common::Config` uses for `config_dir()`. When no
33/// state dir can be resolved (e.g. `$HOME` unset) it falls back to
34/// `$XDG_RUNTIME_DIR/rlm`, but only when that dir is owned by us and not group
35/// or world writable (see [`private_runtime_dir`]). The journal is
36/// boot_id-guarded, so losing it at reboot costs nothing. It never falls back
37/// to a shared, world-writable dir such as `/tmp`, where another user could
38/// pre-create or read the files. `None` means neither dir is usable.
39pub fn guard_file(name: &str) -> Option<PathBuf> {
40    let state = dirs::state_dir();
41    let runtime = if state.is_none() {
42        private_runtime_dir()
43    } else {
44        None
45    };
46    guard_file_from(state, runtime, name)
47}
48
49/// `$XDG_RUNTIME_DIR`, or `None` when it is unset or not private to us: not
50/// owned by our uid, or group or world writable. A rejected dir is logged once.
51fn private_runtime_dir() -> Option<PathBuf> {
52    use std::os::unix::fs::MetadataExt;
53    static WARNED: std::sync::Once = std::sync::Once::new();
54    let dir = dirs::runtime_dir()?;
55    let uid = crate::process::current_uid();
56    match std::fs::metadata(&dir) {
57        Ok(m) if runtime_dir_is_private(m.uid(), m.mode(), uid) => Some(dir),
58        Ok(m) => {
59            WARNED.call_once(|| {
60                tracing::warn!(
61                    "ignoring XDG_RUNTIME_DIR {}: owner uid {} mode {:o}, expected owner {} \
62                     and not group or world writable",
63                    dir.display(),
64                    m.uid(),
65                    m.mode() & 0o7777,
66                    uid
67                )
68            });
69            None
70        }
71        Err(e) => {
72            WARNED.call_once(|| tracing::warn!("ignoring XDG_RUNTIME_DIR {}: {e}", dir.display()));
73            None
74        }
75    }
76}
77
78/// Whether a runtime dir with this owner and mode is safe for guard files.
79fn runtime_dir_is_private(owner: u32, mode: u32, uid: u32) -> bool {
80    owner == uid && mode & 0o022 == 0
81}
82
83fn guard_file_from(
84    state: Option<PathBuf>,
85    runtime: Option<PathBuf>,
86    name: &str,
87) -> Option<PathBuf> {
88    state.or(runtime).map(|d| d.join("rlm").join(name))
89}
90
91/// Path of the guard's write-ahead restore journal, or `None` when no
92/// per-user dir is known (see [`guard_file`]). Without it the guard must not
93/// freeze or cap, since it could not guarantee a restore.
94pub fn try_journal_path() -> Option<PathBuf> {
95    guard_file("guard-journal.jsonl")
96}
97
98/// [`try_journal_path`] for read-only callers such as `rlm guard status`.
99/// Returns an empty path when no per-user dir is known; reading it finds
100/// nothing, which is the right answer since no guard can have written it.
101pub fn journal_path() -> PathBuf {
102    try_journal_path().unwrap_or_default()
103}
104
105/// Path of the lock file that keeps a second `rlm-guard` from running. It
106/// sits next to the journal it protects. `None` means no per-user dir is
107/// known (see [`guard_file`]); the caller then runs without the lock.
108pub fn lock_path() -> Option<PathBuf> {
109    guard_file("rlm-guard.lock")
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn guard_files_prefer_the_state_dir() {
118        assert_eq!(
119            guard_file_from(Some("/s".into()), Some("/r".into()), "rlm-guard.lock"),
120            Some(PathBuf::from("/s/rlm/rlm-guard.lock"))
121        );
122    }
123
124    #[test]
125    fn guard_files_fall_back_to_the_runtime_dir_not_tmp() {
126        assert_eq!(
127            guard_file_from(None, Some("/run/user/1000".into()), "guard-journal.jsonl"),
128            Some(PathBuf::from("/run/user/1000/rlm/guard-journal.jsonl"))
129        );
130        assert_eq!(guard_file_from(None, None, "guard-journal.jsonl"), None);
131    }
132
133    #[test]
134    fn runtime_dir_is_used_only_when_private_to_us() {
135        assert!(runtime_dir_is_private(1000, 0o40700, 1000));
136        assert!(runtime_dir_is_private(1000, 0o40750, 1000));
137        // Owned by someone else.
138        assert!(!runtime_dir_is_private(0, 0o40700, 1000));
139        // Group or world writable.
140        assert!(!runtime_dir_is_private(1000, 0o40770, 1000));
141        assert!(!runtime_dir_is_private(1000, 0o40702, 1000));
142        assert!(!runtime_dir_is_private(1000, 0o41777, 1000));
143    }
144
145    #[test]
146    fn journal_reader_path_is_empty_without_a_dir() {
147        // An empty path reads as missing; it is never under /tmp.
148        let p = try_journal_path().unwrap_or_default();
149        assert!(!p.starts_with("/tmp"), "{p:?}");
150        assert!(Journal::read_entries(&PathBuf::new(), "boot").is_empty());
151    }
152}