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:
+4
-2
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user