use common::Config;
use rlm_core::guard::history;
use rlm_core::guard::sampler::{live_cgroups, strip_cgroup_root, targets_from_procs};
use rlm_core::guard::{
cgfs, try_journal_path, Effector, Journal, PolicyEngine, Sampler, SystemdUser,
};
use rlm_core::rules::RulesEnforcer;
use rlm_core::CgroupManager;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
const EX_CONFIG: i32 = 78;
const EX_LOCKED: i32 = 75;
const RULES_INTERVAL_MS: u64 = 5_000;
#[derive(Debug, PartialEq, Eq)]
struct ScanPlan {
scan: bool,
rules: bool,
}
fn plan_scan(
wants_candidates: bool,
rules_configured: bool,
since_rules_ms: Option<u64>,
) -> ScanPlan {
let rules = rules_configured && since_rules_ms.is_none_or(|d| d >= RULES_INTERVAL_MS);
ScanPlan {
scan: wants_candidates || rules,
rules,
}
}
fn main() {
rlm_core::logging::init(tracing::Level::INFO);
let _instance_lock = match rlm_core::guard::lock_path() {
Some(lock_path) => match rlm_core::guard::lock::try_lock(&lock_path) {
Ok(Some(file)) => Some(file),
Ok(None) => {
tracing::error!(
"another rlm-guard is already running (lock {} is held); exiting with \
status {EX_LOCKED}. systemd will not restart this unit; once the other \
guard stops, run: systemctl --user restart rlm-guard",
lock_path.display()
);
std::process::exit(EX_LOCKED);
}
Err(e) => {
tracing::warn!(
"cannot take the lock {}: {e}; continuing without it",
lock_path.display()
);
None
}
},
None => {
tracing::warn!(
"no per-user state dir or XDG_RUNTIME_DIR; running without the single-instance lock"
);
None
}
};
let config = match Config::load_validated() {
Ok(c) => c,
Err(e) => {
tracing::error!(
"rlm-guard not started: {e}. Fix the file, then run: systemctl --user restart rlm-guard"
);
recover_only();
std::process::exit(EX_CONFIG);
}
};
if let Err(e) = run(config) {
tracing::error!("rlm-guard exiting: {e}");
std::process::exit(1);
}
}
fn recover_only() {
let Ok(manager) = CgroupManager::new() else {
return;
};
let Some(path) = try_journal_path() else {
return;
};
let Ok(journal) = Journal::open(path, cgfs::boot_id()) else {
return;
};
let systemd = SystemdUser::connect();
if let Err(e) = Effector::new(&manager, &journal, systemd.as_ref()).sweep_leftovers() {
tracing::warn!("journal recovery failed: {e}");
}
}
fn run(config: Config) -> common::Result<()> {
let gcfg = config.guard.clone();
let mut enforcer = RulesEnforcer::new(&config);
let self_pid = std::process::id();
let uid = unsafe { libc::getuid() };
let manager = CgroupManager::new()?;
if try_journal_path().is_none() && enforcer.rule_count() == 0 {
tracing::warn!(
"no per-user state dir or XDG_RUNTIME_DIR for the guard journal and no rules \
configured; nothing to do, exiting"
);
return Ok(());
}
let opened = match try_journal_path() {
Some(path) => Journal::open(path, cgfs::boot_id()),
None => Err(common::Error::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
"no per-user state dir or XDG_RUNTIME_DIR for the guard journal",
))),
};
let journal = match opened {
Ok(j) => Some(j),
Err(e) if enforcer.rule_count() > 0 => {
tracing::error!(
error = %e,
"failed to open guard journal; startup crash-recovery skipped and freeze/cap \
escalation disabled for this run (persistent rules are unaffected)"
);
None
}
Err(e) if !gcfg.enabled => {
tracing::warn!(
error = %e,
"guard disabled and no rules configured; journal unavailable and there is \
nothing to recover into; exiting cleanly instead of restart-looping"
);
return Ok(());
}
Err(e) => return Err(e),
};
let systemd = SystemdUser::connect();
if systemd.is_none() {
tracing::warn!("systemd user bus unavailable; using raw cgroupfs writes");
}
let effector = journal
.as_ref()
.map(|j| Effector::new(&manager, j, systemd.as_ref()));
if let Some(effector) = &effector {
if let Err(e) = effector.sweep_leftovers() {
tracing::warn!("startup sweep failed: {e}");
}
}
let escalation_enabled = gcfg.enabled && effector.is_some();
if !escalation_enabled && enforcer.rule_count() == 0 {
tracing::info!("guard disabled and no rules configured; exiting");
return Ok(());
}
let rlm_base = strip_cgroup_root(manager.base_path());
if rlm_base.is_none() {
tracing::error!(
"base_path {:?} isn't under /sys/fs/cgroup; disabling escalation target resolution (protect-matching and guard-status still work, but no freeze/cap victim will ever be selected)",
manager.base_path()
);
}
let sampler = Sampler::new(gcfg.clone(), self_pid, uid, rlm_base);
let mut engine = PolicyEngine::new(gcfg.clone());
let shutdown = Arc::new(AtomicBool::new(false));
{
let s = Arc::clone(&shutdown);
let _ = ctrlc::set_handler(move || s.store(true, Ordering::SeqCst));
}
let interval = Duration::from_millis(gcfg.timing.sample_interval_ms.max(100));
let start = Instant::now();
let mut warned_no_psi = false;
let mut last_rules_ms: Option<u64> = None;
let hist = history::try_history_path();
if hist.is_none() {
tracing::warn!("no per-user state dir or XDG_RUNTIME_DIR; guard history is not recorded");
}
tracing::info!(
uid,
interval_ms = interval.as_millis() as u64,
freeze_guard = escalation_enabled,
rules = enforcer.rule_count(),
"rlm-guard started"
);
while !shutdown.load(Ordering::SeqCst) {
let now_ms = start.elapsed().as_millis() as u64;
let guard_on = gcfg.enabled && effector.is_some();
let sample = if guard_on { sampler.sample() } else { None };
let wants = sample.is_some_and(|s| engine.wants_candidates(s));
let plan = plan_scan(
wants,
enforcer.rule_count() > 0,
last_rules_ms.map(|t| now_ms.saturating_sub(t)),
);
let snapshot = if plan.scan {
rlm_core::process::list_for_uid(uid).unwrap_or_else(|e| {
tracing::warn!("process scan failed: {e}");
Vec::new()
})
} else {
Vec::new()
};
match (&effector, sample) {
(Some(effector), Some(sample)) if gcfg.enabled => {
let procs = if wants {
sampler.candidates(&snapshot)
} else {
Vec::new()
};
let targets = targets_from_procs(&procs, &cgfs::current_bytes);
let live = live_cgroups(&engine.intervened_cgroups());
for action in engine.tick(now_ms, sample, &targets, &live) {
let result = effector.apply(&action).map_err(|e| e.to_string());
if let Err(e) = &result {
tracing::warn!(?action, "action failed: {e}");
}
let event = history::event_for(&action, &result, history::unix_now());
if let (Some(hist), Some(ev)) = (&hist, event) {
if let Err(e) = history::append(hist, &ev) {
tracing::debug!("history write failed: {e}");
}
}
}
}
_ if guard_on && !warned_no_psi => {
tracing::warn!("memory PSI unavailable; guard cannot act");
warned_no_psi = true;
}
_ => {}
}
if plan.rules {
enforcer.reconcile(&manager, &snapshot, &engine.intervened_cgroups());
last_rules_ms = Some(now_ms);
}
sleep_responsive(interval, &shutdown);
}
if let Some(effector) = &effector {
tracing::info!("rlm-guard shutting down; undoing all interventions");
if let Err(e) = effector.undo_all() {
tracing::warn!("undo_all failed: {e}");
}
}
Ok(())
}
fn sleep_responsive(total: Duration, shutdown: &AtomicBool) {
let step = Duration::from_millis(100);
let mut slept = Duration::ZERO;
while slept < total {
if shutdown.load(Ordering::SeqCst) {
break;
}
let chunk = step.min(total - slept);
std::thread::sleep(chunk);
slept += chunk;
}
}
#[cfg(test)]
mod tests {
const UNIT: &str = include_str!("../../assets/rlm-guard.service");
#[test]
fn idle_ticks_do_not_scan_proc() {
assert_eq!(
super::plan_scan(false, false, None),
super::ScanPlan {
scan: false,
rules: false
}
);
assert_eq!(
super::plan_scan(false, true, Some(2_000)),
super::ScanPlan {
scan: false,
rules: false
}
);
assert_eq!(
super::plan_scan(false, true, None),
super::ScanPlan {
scan: true,
rules: true
}
);
assert_eq!(
super::plan_scan(false, true, Some(5_000)),
super::ScanPlan {
scan: true,
rules: true
}
);
assert_eq!(
super::plan_scan(true, false, None),
super::ScanPlan {
scan: true,
rules: false
}
);
}
fn restart_prevent_codes() -> Vec<i32> {
UNIT.lines()
.filter_map(|l| l.strip_prefix("RestartPreventExitStatus="))
.flat_map(|v| v.split_whitespace())
.map(|c| c.parse().expect("numeric exit status"))
.collect()
}
#[test]
fn unit_does_not_restart_on_config_errors() {
assert!(restart_prevent_codes().contains(&super::EX_CONFIG));
}
#[test]
fn unit_does_not_restart_while_another_guard_holds_the_lock() {
assert!(restart_prevent_codes().contains(&super::EX_LOCKED));
}
#[test]
fn unit_has_no_directives_a_user_manager_ignores() {
assert!(
!UNIT.contains("OOMScoreAdjust"),
"a user unit cannot lower its OOM score"
);
assert!(
!UNIT.contains("MemoryMin"),
"MemoryMin is inert without an ancestor chain"
);
assert!(UNIT.contains("ExecStart=/usr/bin/rlm-guard"));
}
}