Skip to content
Jashk120Public

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Rambo

Rambo is an event-driven, kernel-backed system monitor for Linux. It watches RAM (via cgroup v2 limits), temperature (hwmon), pressure (PSI), network (netlink), CPU and disk, then acts on configurable policies: desktop notifications, graceful SIGTERM to a score-picked process, or suspending heavy jobs.

Detection (watchers) is separated from policy (a config-driven engine), so adding a sensor or changing behavior never requires touching daemon logic.

Status: experimental (0.x). Rambo attempts staged, policy-driven intervention before the kernel OOM killer under the configured conditions. It is not a guarantee that a process, or the system, survives memory exhaustion, and it does not replace the kernel OOM killer. Behavior, config keys and defaults may change between 0.x releases. Preview what it would do with rambo daemon --dry-run before relying on it.

Automatic killing is experimental and fail-closed. Automatic thermal killing is DISABLED by default and must not be enabled on a primary desktop: enabling [temperature] allow_kill = true with a wrong [temperature] kill_allowlist can terminate the graphical session. Memory and thermal termination only target explicitly allowlisted workloads (see [kill] allowlist, [kill] allow_exe, [kill] allow_cgroup); anything else is never killed automatically unless [kill] allow_unknown = true is set.

Architecture

Watcher ── Event{Severity, Source} ──▶ Policy Engine ──▶ Action
                                      (config.toml)      notify / kill / suspend

All watchers are kernel-backed or kernel-accounted:

Watcher Kernel source Triggers
Memory cgroup v2 memory.high/memory.max + memory.events soft → warn, hard → kill, max/oom_kill → emergency
Temperature hwmon temp*_input (CPU/GPU) ≥ warning → warn, ≥ critical → throttle eligible only, ≥ emergency → throttle + notify (kill only with allow_kill + kill_allowlist)
Pressure /proc/pressure/* (PSI) sustained some/full → warn / escalate
Network netlink (NETLINK_ROUTE) link down, throughput above alert
CPU /proc/stat busy% sustained above alert
Disk statfs + /proc/diskstats space%, pathological I/O
Battery power_supply (laptops only) low charge → suspend heavy jobs

Install

RAMBo is a systemd user service. The daemon runs as your user and never needs root; only the narrow oom-protect helper runs through pkexec, and only when the daemon is the canonical /usr/bin/rambo. The polkit rule, the systemd unit and the daemon's path guard all agree on exactly /usr/bin/rambo, so install to /usr (the default).

Support status

Target Status Notes
Generic Linux tarball (amd64/arm64) Built + validated in CI Static binary, no distro assumptions
Arch Linux (PKGBUILD) Built + validated in CI Not published to the AUR
Debian / Ubuntu (.deb) Built + validated in CI Contents checked with dpkg-deb --contents
Fedora / RHEL-compatible (.rpm) Built + validated in CI Contents checked with rpm -qpl
openSUSE Manual (generic tarball) No openSUSE package target yet; the installer detects it and falls back to generic behavior
Alpine .apk, Homebrew Future / not published Not claimed as available

Distro packages are experimental and may lag the source tree. The presence of a generic binary does not imply support for every distribution: only the targets marked above are built and validated by CI. Where a distro-specific package is missing, use the generic tarball or build from source.

From a release artifact

Download the artifacts attached to the v0.1.0 release and verify them with the published sha256sums.txt:

sha256sum -c sha256sums.txt

Generic tarball (rambo_0.1.0_linux_amd64.tar.gz, …_arm64.tar.gz):

tar xzf rambo_0.1.0_linux_amd64.tar.gz
cd rambo_0.1.0_linux_amd64
sudo install -Dm755 rambo /usr/bin/rambo
sudo install -Dm644 polkit/99-rambo.rules /usr/share/polkit-1/rules.d/99-rambo.rules
sudo install -Dm644 systemd/rambo.service /usr/lib/systemd/user/rambo.service

Debian / Ubuntu:

sudo apt install ./rambo_0.1.0_amd64.deb

Fedora / RHEL-compatible:

sudo dnf install ./rambo-0.1.0-1.*.rpm

Arch Linux (build the package from the repository root; it uses the deterministic tagged source and a real checksum):

git clone https://github.com/Jashk120/Rambo.git
cd Rambo
makepkg -si

From source

git clone https://github.com/Jashk120/Rambo.git
cd Rambo
make build          # ./rambo reports 0.1.0
make install-check  # report distro, arch, paths, systemd/polkit, consistency
sudo make install   # files only
make activate       # explicit, separate service activation

make install is distro-aware: it reads /etc/os-release (with a generic Linux fallback and a warning when the metadata is missing or unrecognized), detects the architecture using Go's naming (amd64, arm64, 386, arm, …), and installs the same canonical paths everywhere. It never runs systemctl, pkexec, or any privileged command, and it honors DESTDIR for staged package builds. Preview it with:

make install-check

It prints the detected distro and architecture, the selected install paths, whether systemd and polkit are available, and whether the executable, systemd ExecStart and polkit rule paths agree. All install paths are overridable Make variables: PREFIX, BINDIR, SYSTEMD_USER_DIR, POLKIT_RULE_DIR, DOCDIR, MANDIR, LICENSEDIR, the completion dirs, and the reported CONFIG_DIR/STATE_DIR. The polkit rule is installed to the single canonical location, /usr/share/polkit-1/rules.d — never /etc. The helper and the shipped unit only authorize /usr/bin/rambo, so a non-/usr prefix yields a working unprivileged daemon but no passwordless oom-protect; make install-check reports that mismatch rather than hiding it.

Run unprivileged without installing:

go run . daemon --dry-run

Version overrides

The default version (0.1.0) lives in internal/version/version.go. CI and release builds override it reproducibly:

make build VERSION=0.1.0 COMMIT="$(git rev-parse --short HEAD)" \
  BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"

Commands

Command Description
rambo daemon Run the monitor (event loop) in the foreground
rambo top One-shot view of system stats and top RAM consumers
rambo stats Live dashboard (Ctrl+C to exit)
rambo threshold set/status Set memory thresholds and alert levels
rambo protect add/remove/list Protect processes from being killed
rambo oom-protect Privileged helper (via pkexec): set oom_score_adj=-1000 on protected processes
rambo history Show past events and kills with reasons
rambo clean Delete the log file

Dry run

rambo daemon --dry-run runs the full decision path — the same watcher classification, victim scoring, protection/exclusion, cooldown/rate limiter, pending/deferred kills, suspend targets and resume reasons — but performs no signals, no cgroup or oom_score_adj writes, no notifications, and no state/journal writes. The limiter and pending state are simulated in memory so previews stay accurate, and every line is prefixed [rambo][dry-run] and worded "would ...". A preview never claims an action was completed and never claims memory was freed.

Configuration

Config lives at ~/.config/rambo/config.toml (migrated automatically from the old config.json on first run). Defaults:

protect    = ["nvim", "gnome-shell"]   # never killed (session/system-critical gate applies too)
expendable = ["steam"]                 # killed first (score penalty)

[memory]
soft_pct = 90          # memory.high — kernel throttles here (soft)
hard_pct = 96          # graceful SIGTERM line
max_pct  = 99          # kernel safety-net limit
reserve_mb = 256       # absolute emergency reserve on MemAvailable (0 = off)
available_warn_pct = 15  # MemAvailable <= 15% of total → warn
available_crit_pct = 5   # MemAvailable <= 5% of total → kill
swap_warn_pct = 50     # swap in use >= 50% → warn (0 = off)
swap_crit_pct = 95     # swap in use >= 95% → kill (0 = off)
velocity_mbps = 0      # MemAvailable decline rate triggering warn (0 = off)

[memory.repeat]
interval = "20s"       # repeat cadence while hard/critical is sustained
backoff  = 2.0         # exponential backoff per repeat (>= 1)
max      = "5m"        # cap for the backed-off interval

[memory_pressure]
some_pct = 60          # PSI some → warn when sustained
full_pct = 20          # PSI full  → escalate (kill) when sustained
window   = "10s"
action   = "escalate"

[memory_pressure.repeat]
interval = "20s"       # repeat cadence while full/critical is sustained
backoff  = 2.0         # exponential backoff per repeat (>= 1)
max      = "5m"        # cap for the backed-off interval

[temperature]
warning = 85           # log/notify only, never signals
critical = 90          # throttle (SIGSTOP) eligible processes only, never kills
emergency = 95         # throttle + notify; kills only when allow_kill is set
action  = "throttle"   # retained for compatibility; alone it never arms killing
throttle = true        # SIGSTOP eligible heavy processes at throttle_at
throttle_at = 87       # resume 3°C below
allow_kill = false     # DISABLED by default; the only thermal kill switch
kill_allowlist = []    # only these names may be thermally killed (when allow_kill)
sensors = []           # empty = auto-detect CPU/GPU sensors

[network]
alert_mbps = 900
action     = "notify"

[cpu]
alert_pct = 90
action    = "notify"

[disk]
space_alert_pct = 90
io_alert        = true
action          = "notify"

[kill]
policy      = "score"      # "score" or "rss"
cooldown    = "30s"        # min interval between kills
max_per_min = 3            # rate limit
oom_prefer  = true         # steer the kernel OOM killer toward rambo's picks
oom_protect = true         # shield protected processes from the kernel OOM killer
grace       = "5s"         # wait after SIGTERM for exit before SIGKILL
escalate    = true         # SIGKILL survivors of SIGTERM (identity-checked)
allowlist   = []           # names automatic termination may target
allow_exe   = []           # executable paths automatic termination may target
allow_cgroup = []          # cgroup substrings automatic termination may target
allow_unknown = false      # escape hatch; false = unclassified processes never killed

[kill.weights]
rss     = 0.6              # kill score weights
cpu     = 0.3
runtime = 0.1

[battery]
low_pct = 20
action  = "suspend"        # SIGSTOP heavy jobs at low charge (laptops only)

Thermal killing is fail-closed: setting [temperature] action = "kill" alone does not arm it. A thermal kill happens only when [temperature] allow_kill = true is set, and even then only processes on [temperature] kill_allowlist (or an allowlisted cgroup) are eligible. Likewise, [kill] allowlist, [kill] allow_exe and [kill] allow_cgroup restrict automatic termination from all sources; [kill] allow_unknown (default false) must be set to re-enable killing of processes that are neither session/system-critical nor allowlisted.

Thresholds — percent of total RAM, with GB overrides

rambo threshold set --soft-pct 90 --hard-pct 96 --max-pct 99
rambo threshold set --soft 14 --hard 14.5 --max 15        # GB equivalents
rambo threshold set --temp-kill 90 --net-alert 900 --cpu-alert 90 --disk-alert 90
rambo threshold status

Thresholds are relative to total RAM, so the same config works on 16/32/64 GB machines. The soft value must be below the hard value, which must be below the max.

Sustained-pressure repeats

Memory (hard) and PSI (full) are level-triggered: entry emits once, a repeat re-emits while the critical level is sustained (base repeat.interval, times repeat.backoff per repeat, capped at repeat.max), and recovery emits once when pressure returns to normal. Soft/some warnings emit on entry and recovery only. Memory exits with 1% of total RAM hysteresis; PSI exits 2.0 points below its threshold. Missing [memory.repeat] or [memory_pressure.repeat] keys fall back to 20s / 2.0 / 5m.

How RAM enforcement works

On start, rambo finds its session cgroup and applies the limits as kernel enforcement:

  • memory.high = soft% — the kernel throttles allocations in the slice at the soft line (it does not guarantee the line is never crossed), and rambo notifies.
  • memory.max = max% — kernel safety net. If rambo's graceful kill is missed, the kernel protects the session.
  • At the hard line, rambo SIGTERMs the best kill candidate (see below).

Limits are applied only to app.slice and session.slice (the desktop slices the user owns). They are restored to max when the daemon stops. On non-systemd systems, or if the cgroups aren't writable, rambo degrades to meminfo-based monitoring.

The cgroup files are in bytes. Rambo writes byte values and refuses to write any limit below 512 MiB / 1 GiB as a safety floor.

Global memory-pressure signals

The cgroup soft/hard/max levels only see the session cgroup, so rambo layers system-global signals from /proc/meminfo on top: MemAvailable percentage, an absolute emergency reserve (reserve_mb), swap usage percentage (swap_warn_pct/swap_crit_pct, via SwapTotal/SwapFree so zram works), and the MemAvailable decline rate (velocity_mbps). The combined level is the max of the cgroup level and the global level — the global signals can only raise it, never lower it. Systems without swap skip the swap signals, and a missing MemAvailable key skips the availability/reserve/velocity signals; neither case can false-trigger.

These are conservative starting points; tune per machine. The reserve is small against a 16 GB desktop but large on a 2 GB VM (lower it there). Velocity is off by default because the decline rate is noisy — enable it explicitly once a baseline for the machine is known.

Kill policy

Killing is fail-closed: automatic termination only targets explicitly allowlisted workloads, for all sources (memory and thermal). A candidate must match [kill] allowlist, [kill] allow_exe or [kill] allow_cgroup (or [temperature] kill_allowlist for thermal events when [temperature] allow_kill = true). Session/system-critical and non-allowlisted (unknown) processes are excluded from automatic termination; [kill] allow_unknown (default false) is the escape hatch that re-enables killing of unknown processes from non-thermal sources. Thermal events never kill unknown processes regardless of that flag.

The default score policy ranks eligible candidates by:

score = 0.6·rss_norm + 0.3·cpu_norm + 0.1·runtime_norm
      + 0.2 if expendable

RSS and CPU are normalized against total RAM / 100%; younger processes outrank long-running ones.

A classification gate protects processes from any rambo signal (SIGTERM/SIGKILL/SIGSTOP) and from oom_score_adj changes: rambo itself; the session leader; startplasma-wayland/startplasma-x11; plasmashell; kwin_wayland/kwin_x11; systemd --user; dbus-daemon/dbus-broker; the active terminal and shell (any process with a controlling TTY); anything in the user session.slice; plus system-critical processes (PID 1/2, root-owned, system.slice). Classification uses executable path, argv, PID + start time, parent/session, UID and cgroup, not process names alone. Rambo's own signal safety never depends on oom_score_adj; oom_score_adj only steers the kernel OOM killer. The protect list and protect_tree add operator-chosen processes on top of this gate and are never killed.

Cooldowns prevent thrash: kill.cooldown (default 30s) and kill.max_per_min (default 3) — a kill attempt inside the cooldown window notifies "kill throttled" instead of acting.

Set policy = "rss" for the old "biggest process" behavior.

Verifying kills

A sent SIGTERM is not a kill. After signalling, the daemon records a kill_requested entry and verifies asynchronously (the event loop never blocks): it polls for process exit for kill.grace (default 5s), then — if the process is still the same process (PID + start time + name) and kill.escalate is true — sends SIGKILL and re-checks. Only a verified exit notifies "Killed … (freed X MB)" with a kill history record; survivors are marked ineffective (skipped by future picks) with a kill_ineffective record. SIGKILL is never sent to a reused PID whose identity no longer matches the victim.

Steering the kernel OOM killer

The kernel picks OOM victims by oom_score_adj. Rambo cannot replace the kernel OOM killer, but with kill.oom_prefer = true it steers it:

  • Preferred victims: each process rambo chooses to kill is marked oom_score_adj = 1000 (the kernel's #1 choice) as soon as rambo signals it, and every process in the expendable list is kept marked — so if the kernel OOM-kills anyway, it takes rambo's pick first. Raising oom_score_adj needs no privileges.
  • Protecting processes from the kernel: with kill.oom_protect = true (default), the daemon asks a root helper (rambo oom-protect, run through pkexec) to lower oom_score_adj to -1000 on session/system-critical infrastructure (session leader, startplasma-wayland/startplasma-x11, plasmashell, kwin_wayland/kwin_x11, systemd --user, dbus-daemon/dbus-broker, processes in session.slice, PID 1/2, root-owned and system.slice processes), the protect list, and rambo itself — such processes are excluded from the kernel's OOM victim selection. Lowering below 0 requires CAP_SYS_RESOURCE, which is why this runs as root. See Hardening to install the polkit rule; without it the daemon simply logs a warning and keeps running unprivileged. Rambo's own signal safety never depends on this marking; oom_score_adj only steers the kernel OOM killer.

Caveat: once a process is marked 1000 it stays marked for the process lifetime (only root, a restart, or the process exiting clears it) — which is why only expendable processes and active kill victims are ever marked. Likewise, a -1000 protection persists until the process exits or root clears it.

Hardening

The daemon itself stays unprivileged — only a narrow helper runs as root. With kill.oom_protect = true (default), the daemon invokes pkexec rambo oom-protect on start and every 5 minutes (invocations are coalesced and spaced at least 30s apart). That helper sets oom_score_adj = -1000 (excluded from the kernel's OOM victim selection) on session/system-critical infrastructure, the protect list, and rambo itself. Session/system-critical and non-allowlisted processes are never automatically killed regardless: killing additionally requires an [kill] allowlist/allow_exe/allow_cgroup match (or [temperature] kill_allowlist with [temperature] allow_kill = true for thermal). It only writes oom_score_adj; it never kills, so running it as root carries no kill blast radius.

To let the passwordless helper through, install once (as root):

sudo make install

This installs the binary to /usr/bin/rambo, the polkit rule to /usr/share/polkit-1/rules.d/99-rambo.rules, and the systemd user service to /usr/lib/systemd/user/rambo.service. On Arch, makepkg -si (or the AUR) does the same. The rule is machine-wide — every user on the system gets passwordless rambo oom-protect with no per-user step.

systemctl --user enable --now rambo.service

The rule matches only the exact installed invocation — program is /usr/bin/rambo and the full command line is /usr/bin/rambo oom-protect — and grants nothing else. In particular it never matches by basename, so a copy at any other path (e.g. /tmp/evil/rambo) gets no passwordless grant. The daemon additionally only invokes pkexec when it is itself the installed /usr/bin/rambo binary; a dev checkout, a temporary Go test binary (/tmp/go-build…/cmd.test), ~/.local/bin/rambo, or any other path skips the helper with a log line instead of prompting.

Run the canonical binary. The polkit rule, the daemon's path guard and the systemd unit all agree on exactly /usr/bin/rambo. A user-local copy is dangerous because ~/.local/bin usually precedes /usr/bin in PATH, and a unit in ~/.config/systemd/user/rambo.service overrides the packaged /usr/lib/systemd/user/rambo.service — either silently runs a stale or dev build that the rule does not grant (protection off, or an auth prompt). make install warns when either shadow exists; remove it so rambo resolves to /usr/bin/rambo.

Verify protected processes are now -1000 (rambo itself may show up as the daemon):

cat /proc/$(pgrep -x kwin_wayland)/oom_score_adj   # → -1000
cat /proc/$(pgrep -x rambo)/oom_score_adj          # → -1000

Without the rule, the daemon logs oom-protect: helper failed and keeps running — kernel protection is simply off. Protection persists for each protected process's lifetime and is re-applied on the 5-minute cadence to catch newly started ones.

Protect / expendable

rambo protect add --name nvim
rambo protect remove --name nvim
rambo protect list

The old rambo whitelist command still works as an alias.

protect adds to the never-kill gate on top of the built-in session/system-critical set; expendable only reorders eligible, allowlisted candidates. Neither list bypasses the allowlist requirement: a process is automatically killed only if it matches [kill] allowlist, [kill] allow_exe or [kill] allow_cgroup (thermal additionally needs [temperature] allow_kill = true plus [temperature] kill_allowlist), or the operator sets [kill] allow_unknown = true.

History

Events and kills are recorded to ~/.local/state/rambo/state.json (last 500):

rambo history
rambo history --source temperature --n 50

Run with systemd

A user service template is included at systemd/rambo.service. It is installed automatically to /usr/lib/systemd/user/rambo.service by sudo make install (or the package); enable it with:

systemctl --user enable --now rambo.service

For a manual install without make install, copy it to your user unit dir:

mkdir -p ~/.config/systemd/user
cp systemd/rambo.service ~/.config/systemd/user/rambo.service
systemctl --user daemon-reload
systemctl --user enable --now rambo.service

The daemon prints a status line each second (visible with journalctl --user -u rambo -f) and restores cgroup limits on a clean stop.

Logs

~/.local/share/rambo/rambo.log records kills, and desktop notifications fall back to it when no notification daemon is available. Delete it with rambo clean.

Notes

  • Linux-only; reads kernel data from cgroup v2, hwmon, /proc, and netlink.
  • The hard action sends SIGTERM, verifies the exit, and escalates to SIGKILL when kill.escalate is true. The kernel safety net may SIGKILL if the max limit is truly reached.
  • Battery protection is dormant on desktops (no BAT* in power_supply).
  • Suspended processes are tracked by identity (PID + start time + name) with a set of suspend reasons (battery, thermal): a process suspended for both stays stopped until every reason clears, so battery recovery never resumes a process thermal throttling still needs stopped. Battery recovery resumes only battery suspensions, thermal recovery only thermal ones; a reused PID is never resumed, and processes rambo killed are never resumed. Every change to the suspended set is journaled to ~/.local/state/rambo/suspended.json (atomic write) before SIGSTOP is delivered, and the daemon reconciles that journal at startup — resuming only identities that still validate — so a crash no longer leaves processes stopped forever. A journal write failure is logged while the daemon keeps running from memory (a later crash may then strand processes), and processes stopped by anyone other than rambo are never resumed by it.
  • A config reload (SIGHUP) preserves runtime state — suspended processes, the kill cooldown/rate window, the deferred kill, and the ineffective set — so a reload never strands a suspended process. The daemon resumes any still-suspended processes on shutdown, and after a kernel OOM event it immediately re-evaluates pending/suspended state before acting.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages