fix(watchers): exempt runaway shells from protection to prevent host OOM

- update is_protected() in box-stability-watcher.py to revoke immunity from bash/zsh processes with RSS >= 2048MB
- update watchers/README.md to document the 2048MB interactive shell threshold
- add unit tests verifying shell protection vs runaway exemption in test_box_stability_watcher.py
This commit is contained in:
operator
2026-10-07 21:09:45 +00:00
parent 0a45133d28
commit 2a2a808778
3 changed files with 13 additions and 4 deletions
+3
View File
@@ -37,6 +37,9 @@ class TestConfigAndSafety(unittest.TestCase):
self.assertTrue(w.is_protected(777, "tailscaled", "/usr/sbin/tailscaled", cfg))
self.assertFalse(w.is_protected(1234, "muse-bin", "/home/super/.local/bin/muse-bin-1.4.3 resume abc", cfg))
self.assertFalse(w.is_protected(5678, "chromium", "/usr/lib/chromium/chromium --type=renderer", cfg))
# Shell protection & runaway exemption
self.assertTrue(w.is_protected(9999, "bash", "/bin/bash", {"protected_commands": ["bash"]}, rss_mb=50))
self.assertFalse(w.is_protected(9999, "bash", "bash test_script.sh", {"protected_commands": ["bash"]}, rss_mb=2500))
class TestStabilityEvaluation(unittest.TestCase):
+4 -2
View File
@@ -37,8 +37,10 @@ When a process is paused:
## Protected Whitelist
The watcher will **never** terminate or renice:
`sshd`, `tailscaled`, `tailscale`, `systemd`, `dbus-broker`, `pipewire`, `wireplumber`, `tmux` (main server), `bash`, `zsh`, `ghostty`, `alacritty`.
The watcher will **never** terminate or renice core system services or interactive terminal sessions:
`sshd`, `tailscaled`, `tailscale`, `systemd`, `dbus-broker`, `pipewire`, `wireplumber`, `tmux` (main server), `ghostty`, `alacritty`.
Interactive shells (`bash`, `zsh`, `sh`) are protected while operating within normal memory bounds (<2048MB RSS). Runaway scripts or test jobs executing under `bash`/`zsh` that exceed 2048MB RSS automatically lose whitelist immunity and are subjected to the standard ORANGE pause/cull lifecycle to protect the host against OOM crashes.
## Unified Box CLI Integration
+6 -2
View File
@@ -227,11 +227,15 @@ def get_system_metrics() -> Dict[str, Any]:
}
def is_protected(pid: int, name: str, cmdline: str, config: Dict[str, Any]) -> bool:
def is_protected(pid: int, name: str, cmdline: str, config: Dict[str, Any], rss_mb: int = 0) -> bool:
if pid in (os.getpid(), os.getppid(), 1):
return True
low_name = name.lower()
low_cmd = cmdline.lower()
# Shells (bash/zsh) are only protected while of reasonable memory size.
# A shell consuming >= 2048 MB RSS is a runaway script/test or memory leak, not an interactive shell.
if low_name in ("bash", "zsh", "sh") and rss_mb >= 2048:
return False
for prot in config.get("protected_commands", []):
p_low = prot.lower()
if p_low == low_name or p_low in low_cmd.split():
@@ -289,7 +293,7 @@ def inspect_processes(config: Dict[str, Any]) -> List[Dict[str, Any]]:
"nice": nice,
"matches_target": matches_target,
"tmux_sock": tmux_sock,
"is_protected": is_protected(pid, name, cmdline, config),
"is_protected": is_protected(pid, name, cmdline, config, rss_mb=rss_mb),
})
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue