Skip to content

feat(ops): add rw_cpu_guard.sh — nuclear CPU governor for KVM1 - #116

Merged
gamblecodezcom merged 1 commit into
mainfrom
claude/cpu-guard-script-DnD8u
Mar 1, 2026
Merged

feat(ops): add rw_cpu_guard.sh — nuclear CPU governor for KVM1#116
gamblecodezcom merged 1 commit into
mainfrom
claude/cpu-guard-script-DnD8u

Conversation

@gamblecodezcom

@gamblecodezcom gamblecodezcom commented Mar 1, 2026

Copy link
Copy Markdown
Owner

User description

Prevents Runewager from being starved by CPU hogs on Hostinger KVM1.

Features:

  • Load-aware 3-zone response (none / soft / hard) with per-zone
    %CPU thresholds so the guard is only aggressive when the box is hot
  • Kill ladder: renice+ionice → cgroup-quota → STOP/CONT (3 s) → SIGTERM → SIGKILL
  • cgroup v1 + v2 auto-detection; configurable CPU quota (default 70%)
  • Fork-bomb detection via total-process-count ceiling (default 200)
  • Telegram admin alerts on STOP/CONT and kills (5-min cooldown,
    reads BOT_TOKEN + ADMIN_IDS from .env automatically)
  • Duplicate-instance guard via PID file
  • Atomic state-file updates (mktemp + mv) — no corruption under load
  • Comprehensive whitelist covering Runewager (all launch forms), PM2,
    systemd, sshd, cron, fail2ban, interactive shells, and guard itself;
    children of whitelisted parents are also protected
  • Modes: daemon (default), --install (writes systemd unit + enables),
    --status, --dry-run, --once (single scan, great for cron testing)
  • No set -e in daemon loop — a subcommand failure never kills the guard
  • Falls back to /tmp paths if /var/log isn't writable (non-root runs)

https://claude.ai/code/session_01WpEqAiDbpqnBywMjYXJHf4

Summary by Sourcery

Add a dedicated CPU guard daemon script to protect the Runewager bot from CPU starvation on Hostinger KVM1 by monitoring load and throttling or killing non-whitelisted hog processes.

New Features:

  • Introduce rw_cpu_guard.sh bash daemon providing load-aware CPU hog detection and mitigation with configurable thresholds and strike-based kill ladder.
  • Add automatic cgroup v1/v2 CPU quota management for throttled processes with configurable quota percentage.
  • Implement fork-bomb protection via a global process-count ceiling and targeted cleanup of recent non-whitelisted processes.
  • Integrate Telegram admin alerting for severe actions (STOP/CONT and kills) with cooldown and .env-based configuration.
  • Provide multiple operation modes including foreground daemon, one-shot scan, dry-run simulation, status reporting, and systemd-based installation.

Enhancements:

  • Define an extensive whitelist and inheritance rules to ensure Runewager, its infrastructure, critical system services, shells, and the guard itself are never targeted.
  • Add atomic, log-backed strike tracking and PID-guarding to avoid state corruption and duplicate daemon instances, with safe fallbacks when paths are not writable.

Deployment:

  • Add a systemd service definition generated by the script to install and manage the CPU guard as a persistent service on compatible hosts.

CodeAnt-AI Description

Protect Runewager bot from CPU starvation with a dedicated rw_cpu_guard daemon

What Changed

  • New rw_cpu_guard.sh daemon that continuously monitors system load and intervenes when CPU usage is high.
  • Three load zones (normal / soft / hard) with per-zone CPU thresholds that throttle or terminate non-whitelisted processes using a staged ladder: renice/ionice → cgroup throttle → STOP/CONT pause → SIGTERM → SIGKILL.
  • Applies cgroup v1/v2 CPU quotas to throttle offenders, and detects/mitigates fork-bomb situations by killing recent non-whitelisted processes when total process count is excessive.
  • Protects critical processes via a comprehensive whitelist (Runewager, PM2, systemd, sshd, cron, shells, guard itself); children of whitelisted parents are also preserved.
  • Optional Telegram alerts for STOP/CONT and kills (cooldown applied), and operational modes: daemon (default), --install (systemd service), --status, --dry-run, and --once for single-scan testing.

Impact

✅ Fewer Runewager CPU starvations
✅ Clearer admin alerts on process throttles and kills
✅ Shorter recovery time after fork-bomb events

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

Summary by CodeRabbit

  • New Features
    • Added CPU protection daemon with multi-level resource throttling
    • Includes fork-bomb detection, process whitelisting, and cgroup quota management
    • Telegram alerts for critical events and dry-run mode for testing
    • System utilities for daemon installation and status monitoring

Prevents Runewager from being starved by CPU hogs on Hostinger KVM1.

Features:
- Load-aware 3-zone response (none / soft / hard) with per-zone
  %CPU thresholds so the guard is only aggressive when the box is hot
- Kill ladder: renice+ionice → cgroup-quota → STOP/CONT (3 s) → SIGTERM → SIGKILL
- cgroup v1 + v2 auto-detection; configurable CPU quota (default 70%)
- Fork-bomb detection via total-process-count ceiling (default 200)
- Telegram admin alerts on STOP/CONT and kills (5-min cooldown,
  reads BOT_TOKEN + ADMIN_IDS from .env automatically)
- Duplicate-instance guard via PID file
- Atomic state-file updates (mktemp + mv) — no corruption under load
- Comprehensive whitelist covering Runewager (all launch forms), PM2,
  systemd, sshd, cron, fail2ban, interactive shells, and guard itself;
  children of whitelisted parents are also protected
- Modes: daemon (default), --install (writes systemd unit + enables),
  --status, --dry-run, --once (single scan, great for cron testing)
- No `set -e` in daemon loop — a subcommand failure never kills the guard
- Falls back to /tmp paths if /var/log isn't writable (non-root runs)

https://claude.ai/code/session_01WpEqAiDbpqnBywMjYXJHf4
@codeant-ai

codeant-ai Bot commented Mar 1, 2026

Copy link
Copy Markdown

CodeAnt AI is reviewing your PR.


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@sourcery-ai

sourcery-ai Bot commented Mar 1, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds a standalone bash-based CPU guard daemon (rw_cpu_guard.sh) for Hostinger KVM1 that monitors system load, identifies non-whitelisted CPU hogs, and throttles or kills them via a multi-step kill ladder, with cgroup-based quotas, fork-bomb protection, Telegram alerts, and systemd integration.

Sequence diagram for rw_cpu_guard daemon scan and kill ladder

sequenceDiagram
    participant Guard as rw_cpu_guard_daemon
    participant ProcFS as procfs_ps
    participant CGroup as cgroup_cpu_controller
    participant Hog as Target_process
    participant Telegram as Telegram_API

    Guard->>ProcFS: Read /proc/loadavg
    ProcFS-->>Guard: Current_load
    Guard->>Guard: Determine load_zone (none|soft|hard)
    Guard->>ProcFS: List /proc PIDs
    ProcFS-->>Guard: PIDs
    Guard->>Guard: _check_fork_bomb (kill recent non-whitelisted if over limit)
    Guard->>ProcFS: ps -eo pid,pcpu,cmd --sort=-pcpu
    ProcFS-->>Guard: Process_list

    loop For each process
        Guard->>ProcFS: Read /proc/pid/cmdline
        ProcFS-->>Guard: full_cmd
        Guard->>Guard: _is_whitelisted(full_cmd, pid)?
        alt Whitelisted
            Guard->>Guard: Skip process
        else Not whitelisted and cpu >= threshold
            Guard->>Guard: strikes = _get_strikes(pid) + 1
            Guard->>Guard: _set_strikes(pid, strikes)
            Guard->>CGroup: _apply_cgroup(pid)

            alt strikes == 1 (strike1)
                Guard->>Hog: renice +15
                Guard->>Hog: ionice class 3
                Guard->>Guard: Log STRIKE1
            else strikes == 2 (strike2)
                Guard->>Hog: kill -STOP
                Guard->>Guard: sleep 3s
                Guard->>Hog: kill -CONT
                Guard->>Guard: Log STRIKE2 STOP/CONT
                Guard->>Telegram: send STOP/CONT alert
                Telegram-->>Guard: HTTP 200 (ignored on failure)
            else strikes >= 3 (termination)
                Guard->>Hog: kill -TERM
                Guard->>Guard: sleep 2s
                Guard->>Hog: kill -KILL (if still alive)
                Guard->>Guard: Log KILLED
                Guard->>Telegram: send SIGTERM/SIGKILL alert
                Telegram-->>Guard: HTTP 200 (ignored on failure)
            end
        end
    end

    Guard->>Guard: sleep SCAN_INTERVAL
    Guard->>Guard: Next _scan_once() iteration
Loading

Flow diagram for rw_cpu_guard main modes and daemon scan loop

flowchart TD
    A[Start rw_cpu_guard.sh] --> B[Parse CLI args]

    B -->|--install| M[Mode install]
    B -->|--status| N[Mode status]
    B -->|--once| O[Mode once]
    B -->|no special mode| P[Mode daemon]

    M --> M1[Check root user]
    M1 -->|not root| M2[Print error and exit]
    M1 -->|root| M3[Write systemd unit file]
    M3 --> M4[systemctl daemon-reload, enable, restart]
    M4 --> Z[Exit]

    N --> N1[Print load and config]
    N1 --> N2[Show PID file status]
    N2 --> N3[Show systemd state and strikes]
    N3 --> N4[Show recent log lines]
    N4 --> Z

    subgraph Daemon_Setup
        D1[_ensure_log]
        D2[Ensure STATE_FILE exists]
        D3[Duplicate-instance check via PID_FILE]
        D4[trap _cleanup on EXIT/INT/TERM]
        D5[_load_env from .env]
        D6[_setup_cgroup]
        D7[Log startup banner]
    end

    O --> Daemon_Setup
    P --> Daemon_Setup

    D7 -->|mode once| S1[_scan_once]
    S1 --> Z

    D7 -->|mode daemon| L1[Daemon loop]

    subgraph Scan_Once
        S2[Read /proc/loadavg]
        S2 --> S3[Compute load zone none/soft/hard]
        S3 --> S4[_clear_dead_pids]
        S4 --> S5[_check_fork_bomb]
        S5 --> S6[Select CPU threshold by zone]
        S6 --> S7[ps -eo pid,pcpu,cmd --sort=-pcpu]
        S7 --> S8{For each process}

        S8 -->|cpu < threshold or <1%| S9[Skip process]
        S9 --> S8

        S8 -->|candidate| S10[Read full cmdline from /proc/pid/cmdline]
        S10 --> S11{_is_whitelisted?}
        S11 -->|yes| S12[Log PROTECTED and skip]
        S12 --> S8

        S11 -->|no| S13[_get_strikes + 1, _set_strikes]
        S13 --> S14[_apply_cgroup pid]
        S14 --> S15{Strike count}

        S15 -->|1| S16[renice +15, ionice class 3]
        S16 --> S8

        S15 -->|2| S17[STOP 3s then CONT, log]
        S17 --> S18[Optional Telegram STOP/CONT alert]
        S18 --> S8

        S15 -->|>=3| S19[TERM, wait, then KILL if needed]
        S19 --> S20[Log and Telegram kill alert]
        S20 --> S8

        S8 -->|done| S21[Return from _scan_once]
    end

    L1 --> S2
    S21 --> L2[sleep SCAN_INTERVAL]
    L2 --> L1

    Z[End]
Loading

File-Level Changes

Change Details Files
Introduce rw_cpu_guard.sh bash daemon that monitors load and CPU usage and enforces a 3-zone throttling/kill strategy for non-whitelisted processes.
  • Defines configuration options for load thresholds, CPU usage thresholds per zone, scan interval, strike count, process-count ceiling, cgroup name/quota, and file paths with sensible defaults for KVM1.
  • Implements whitelisting based on command-line substrings/regex and parent process, protecting Runewager, PM2, systemd, SSH, cron, common daemons, interactive shells, and utilities used by the script.
  • Implements a scan loop that computes current load, selects a load zone, filters processes by per-zone CPU threshold using ps, resolves full command via /proc, and dispatches non-whitelisted hogs to a strike handler while updating a persistent strike state file atomically.
  • Implements a multi-strike kill ladder that progressively applies renice/ionice, cgroup throttling, STOP/CONT pause, then SIGTERM/SIGKILL, with dry-run support and structured logging.
  • Adds fork-bomb detection based on total /proc PID count and an emergency cleanup that KILLs recent non-whitelisted PIDs when the threshold is exceeded.
scripts/rw_cpu_guard.sh
Add cgroup v1/v2 CPU quota integration and process assignment for throttling non-whitelisted processes.
  • Detects presence of cgroup v2 or v1 CPU controller under /sys/fs/cgroup and sets internal flags, logging when quota enforcement is unavailable.
  • Creates a cgroup for the guard (rw_cpu_guard) in v1 or v2, computes and writes cpu quota/period based on a configurable percentage of one CPU, and logs configuration.
  • Provides a helper to assign target PIDs into the configured cgroup best-effort during hog handling.
scripts/rw_cpu_guard.sh
Add logging, environment loading, Telegram alerting, and fork-safe state management utilities.
  • Implements a logging layer with INFO/WARN/ERROR/ACT levels that falls back to /tmp if /var/log is not writable and mirrors to stdout in interactive or dry-run modes.
  • Adds an .env loader that reads BOT_TOKEN/TELEGRAM_BOT_TOKEN and ADMIN_IDS into TG_BOT_TOKEN/TG_ADMIN_IDS if unset in the environment.
  • Implements Telegram alert sending with configurable 5-minute cooldown and multi-admin support, using curl and defensive error handling.
  • Stores per-PID strike counts in a state file with atomic mktemp+mv updates and periodic cleanup of dead PIDs to avoid unbounded growth.
scripts/rw_cpu_guard.sh
Provide multiple operating modes, systemd service installation, duplicate-instance guarding, and status reporting.
  • Parses CLI flags to support daemon (default), --install, --status, --dry-run, --once, and --help modes without using set -e in the loop context.
  • Implements --install to write a hardened systemd unit (with capabilities, restart policy, and documentation), enable it, and start the service.
  • Adds a PID file guard to prevent multiple daemon instances and ensures cleanup of the PID file on exit via trap.
  • Implements --status to display current load, thresholds, scan interval, dry-run flag, log/state paths, guard PID liveness, systemd service status, current strike records, and a tail of recent log lines.
scripts/rw_cpu_guard.sh

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@gamblecodezcom
gamblecodezcom merged commit 86877c6 into main Mar 1, 2026
4 of 5 checks passed
@gamblecodezcom
gamblecodezcom deleted the claude/cpu-guard-script-DnD8u branch March 1, 2026 11:27
@coderabbitai

coderabbitai Bot commented Mar 1, 2026

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f67adc2 and 2193075.

📒 Files selected for processing (1)
  • scripts/rw_cpu_guard.sh

📝 Walkthrough

Walkthrough

A new Bash daemon script is introduced to monitor and regulate CPU load on a VPS by protecting a Runewager bot through multi-zone load classification, process throttling, and forceful termination when necessary, with cgroup enforcement, Telegram alerting, and fork-bomb detection.

Changes

Cohort / File(s) Summary
CPU Load Guard Daemon
scripts/rw_cpu_guard.sh
New 657-line Bash daemon implementing multi-zone CPU load protection with progressive escalation (renice/ionice → STOP/CONT → SIGTERM/SIGKILL), cgroup v1/v2 quota management, process whitelist, fork-bomb detection, Telegram alerts, state persistence, and systemd integration with install/status/dry-run modes.

Sequence Diagram(s)

sequenceDiagram
    participant Daemon as CPU Guard Daemon
    participant Kernel as Linux Kernel
    participant ProcFS as /proc Filesystem
    participant CGroup as cgroup Subsystem
    participant Telegram as Telegram API
    participant StateFile as State File

    rect rgba(100, 150, 255, 0.5)
    Note over Daemon: Scan Cycle Start
    end

    Daemon->>Kernel: Read CPU load average
    Daemon->>ProcFS: Enumerate running processes
    Daemon->>StateFile: Load strike counters
    
    rect rgba(200, 150, 100, 0.5)
    Note over Daemon: Per-Process Evaluation
    end
    
    loop For each non-whitelisted process
        Daemon->>ProcFS: Get process CPU%, memory, cmdline
        Daemon->>Daemon: Check load zone & assign threshold
        alt Threshold exceeded (Strike 1)
            Daemon->>Kernel: renice/ionice process
            Daemon->>StateFile: Increment strike counter
        else Strike 2
            Daemon->>Kernel: Send SIGSTOP
            Daemon->>Telegram: Alert admin (optional)
            Daemon->>Kernel: Send SIGCONT (after delay)
            Daemon->>StateFile: Increment strike counter
        else Strike 3+
            Daemon->>Kernel: Send SIGTERM
            Daemon->>Telegram: Alert admin (optional)
            alt SIGTERM failed
                Daemon->>Kernel: Send SIGKILL
            end
            Daemon->>StateFile: Reset strike counter
        end
    end
    
    rect rgba(150, 200, 100, 0.5)
    Note over Daemon: Fork-Bomb Check
    end
    
    Daemon->>ProcFS: Count total processes
    alt Process count > threshold
        Daemon->>Kernel: Kill excess non-whitelisted processes
        Daemon->>Telegram: Alert fork-bomb event
    end
    
    rect rgba(100, 100, 200, 0.5)
    Note over Daemon: Cleanup & Persist
    end
    
    Daemon->>StateFile: Clean dead PIDs, atomic write
    Daemon->>Daemon: Sleep until next scan interval
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A guard hops through the CPU land,
With thresholds set and strikes so grand,
Renice, then freeze, then off they go—
The naughty procs don't steal the show!
Cgroups and alerts, both old and new,
One daemon script to see them through! 🛡️

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch claude/cpu-guard-script-DnD8u

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codeant-ai codeant-ai Bot added the size:XL This PR changes 500-999 lines, ignoring generated files label Mar 1, 2026
@codeant-ai

codeant-ai Bot commented Mar 1, 2026

Copy link
Copy Markdown

Sequence Diagram

The new script runs as a daemon, samples system load, scans processes, protects whitelisted processes, and applies a graduated "kill ladder" (throttle → pause → terminate) with optional cgroup throttling and Telegram alerts for severe actions.

sequenceDiagram
    participant rw_cpu_guard
    participant Host as System (/proc, ps)
    participant CGroup
    participant Process
    participant Telegram

    rw_cpu_guard->>Host: read /proc/loadavg and ps (process list)
    rw_cpu_guard->>rw_cpu_guard: determine load zone (none/soft/hard) and threshold
    rw_cpu_guard->>Process: inspect top CPU processes
    alt Process is whitelisted
        rw_cpu_guard-->>Process: skip protection
    else Non-whitelisted hog above threshold
        rw_cpu_guard->>CGroup: best-effort add PID to cgroup (throttle)
        rw_cpu_guard->>Process: renice + ionice (soft throttle)
        rw_cpu_guard->>Process: STOP (3s) then CONT (hard throttle)
        rw_cpu_guard->>Process: SIGTERM → if still alive → SIGKILL
        rw_cpu_guard->>Telegram: send alert for STOP/CONT or kills (cooldown)
    end
Loading

Generated by CodeAnt AI

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • The whitelist matching is very broad (simple substrings over the full cmdline, e.g. "bash", "node.*index", "/var/www") which can accidentally protect unexpected or malicious processes; consider tightening this by matching against the executable name (comm) or using anchored patterns and/or explicit path checks.
  • The Telegram alert messages interpolate unescaped short_cmd into Markdown text, which can break formatting or fail if the command contains Markdown metacharacters; consider escaping or switching to parse_mode=MarkdownV2 with proper escaping, or using parse_mode=HTML/no parse mode.
  • In _load_env, the simple KEY=VALUE parsing will misbehave on lines with export, spaces around =, or embedded = characters; if you expect such cases in .env, consider using a more robust parser (e.g. set -a; source .env; set +a in a subshell and then copying only the needed vars).
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The whitelist matching is very broad (simple substrings over the full cmdline, e.g. "bash", "node.*index", "/var/www") which can accidentally protect unexpected or malicious processes; consider tightening this by matching against the executable name (comm) or using anchored patterns and/or explicit path checks.
- The Telegram alert messages interpolate unescaped `short_cmd` into Markdown text, which can break formatting or fail if the command contains Markdown metacharacters; consider escaping or switching to `parse_mode=MarkdownV2` with proper escaping, or using `parse_mode=HTML`/no parse mode.
- In `_load_env`, the simple `KEY=VALUE` parsing will misbehave on lines with `export`, spaces around `=`, or embedded `=` characters; if you expect such cases in `.env`, consider using a more robust parser (e.g. `set -a; source .env; set +a` in a subshell and then copying only the needed vars).

## Individual Comments

### Comment 1
<location path="scripts/rw_cpu_guard.sh" line_range="143-146" />
<code_context>
+    "runc"
+
+    # Interactive shells and common admin tools
+    "bash"
+    "zsh"
+    "sh "
+    "sh\t"
+    "-bash"
+    "-zsh"
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Very broad shell whitelist entries risk protecting malicious or runaway shell workloads.

Patterns like `"bash"`, `"zsh"`, `"sh "`, `"sh\t"`, etc. effectively exempt *all* shell processes from throttling, including CPU-hungry scripts or fork bombs, which defeats this guard in realistic failure modes. Consider narrowing the logic by e.g. only whitelisting shells whose parent is `sshd`/`systemd` (interactive sessions) or by detecting the presence of a TTY (`ps`/`/proc`), so interactive admin use stays protected while non-interactive shell workloads remain subject to the guard.

Suggested implementation:

```
    # Interactive shells and common admin tools
    #
    # NOTE:
    # Avoid broadly whitelisting shells by name (e.g. "bash", "zsh", "sh") because
    # that would exempt non-interactive workloads and runaway scripts from throttling.
    # Interactive shells should be detected via parent/TTY inspection instead
    # (e.g. shell with parent "sshd"/"systemd" and an attached TTY).
    # Only keep explicitly interactive admin helpers here.
    "tmux"
    "screen"
    "vim"
    "nano"

```

To fully implement the behavior described in your review comment, you should also:
1. Introduce an `is_interactive_session` helper (or equivalent logic) that:
   - Checks whether the candidate process has an attached TTY (e.g. by inspecting `/proc/$pid/fd/0` or using `ps -o tty= -p "$pid"`).
   - Optionally verifies the parent process name is `sshd`, `systemd`, or another trusted session manager (via `/proc/$pid/stat` or `ps -o ppid= -p "$pid"` followed by a lookup of the parent’s command name).
2. Integrate this helper into the throttling decision path:
   - When deciding whether to throttle a process, first test the whitelist by `comm`.
   - If not whitelisted by `comm`, call `is_interactive_session "$pid"` and skip throttling when it returns true.
3. Ensure any existing callers that relied on `"bash"`/`"zsh"` name-based whitelisting are updated to use the new interactive-session logic so that legitimate interactive admin shells remain protected while non-interactive shell workloads are still subject to the guard.
</issue_to_address>

### Comment 2
<location path="scripts/rw_cpu_guard.sh" line_range="76" />
<code_context>
+# Files
+LOG_FILE="${LOG_FILE:-/var/log/${SCRIPT_NAME}.log}"
+STATE_FILE="${STATE_FILE:-/tmp/${SCRIPT_NAME}.state}"
+PID_FILE="${PID_FILE:-/tmp/${SCRIPT_NAME}.pid}"
+
+# cgroup CPU quota applied to throttled (non-whitelisted) processes
</code_context>
<issue_to_address>
**🚨 issue (security):** Storing the PID file in /tmp allows spoofing or DoS by unprivileged users.

Because any user can create or modify `/tmp/${SCRIPT_NAME}.pid`, a local attacker can pre-populate it with an arbitrary PID so the script believes an instance is already running and exits, or otherwise disrupts restarts. Prefer a root-only directory such as `/run/${SCRIPT_NAME}.pid` or `/var/run/${SCRIPT_NAME}.pid` when appropriate, and validate that the PID file owner and the process it references (e.g., via `/proc/$pid/cmdline`) match this script before trusting it.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread scripts/rw_cpu_guard.sh
Comment on lines +143 to +146
"bash"
"zsh"
"sh "
"sh\t"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Very broad shell whitelist entries risk protecting malicious or runaway shell workloads.

Patterns like "bash", "zsh", "sh ", "sh\t", etc. effectively exempt all shell processes from throttling, including CPU-hungry scripts or fork bombs, which defeats this guard in realistic failure modes. Consider narrowing the logic by e.g. only whitelisting shells whose parent is sshd/systemd (interactive sessions) or by detecting the presence of a TTY (ps//proc), so interactive admin use stays protected while non-interactive shell workloads remain subject to the guard.

Suggested implementation:

    # Interactive shells and common admin tools
    #
    # NOTE:
    # Avoid broadly whitelisting shells by name (e.g. "bash", "zsh", "sh") because
    # that would exempt non-interactive workloads and runaway scripts from throttling.
    # Interactive shells should be detected via parent/TTY inspection instead
    # (e.g. shell with parent "sshd"/"systemd" and an attached TTY).
    # Only keep explicitly interactive admin helpers here.
    "tmux"
    "screen"
    "vim"
    "nano"

To fully implement the behavior described in your review comment, you should also:

  1. Introduce an is_interactive_session helper (or equivalent logic) that:
    • Checks whether the candidate process has an attached TTY (e.g. by inspecting /proc/$pid/fd/0 or using ps -o tty= -p "$pid").
    • Optionally verifies the parent process name is sshd, systemd, or another trusted session manager (via /proc/$pid/stat or ps -o ppid= -p "$pid" followed by a lookup of the parent’s command name).
  2. Integrate this helper into the throttling decision path:
    • When deciding whether to throttle a process, first test the whitelist by comm.
    • If not whitelisted by comm, call is_interactive_session "$pid" and skip throttling when it returns true.
  3. Ensure any existing callers that relied on "bash"/"zsh" name-based whitelisting are updated to use the new interactive-session logic so that legitimate interactive admin shells remain protected while non-interactive shell workloads are still subject to the guard.

Comment thread scripts/rw_cpu_guard.sh
# Files
LOG_FILE="${LOG_FILE:-/var/log/${SCRIPT_NAME}.log}"
STATE_FILE="${STATE_FILE:-/tmp/${SCRIPT_NAME}.state}"
PID_FILE="${PID_FILE:-/tmp/${SCRIPT_NAME}.pid}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 issue (security): Storing the PID file in /tmp allows spoofing or DoS by unprivileged users.

Because any user can create or modify /tmp/${SCRIPT_NAME}.pid, a local attacker can pre-populate it with an arbitrary PID so the script believes an instance is already running and exits, or otherwise disrupts restarts. Prefer a root-only directory such as /run/${SCRIPT_NAME}.pid or /var/run/${SCRIPT_NAME}.pid when appropriate, and validate that the PID file owner and the process it references (e.g., via /proc/$pid/cmdline) match this script before trusting it.

@codeant-ai

codeant-ai Bot commented Mar 1, 2026

Copy link
Copy Markdown

Nitpicks 🔍

🔒 No security issues identified
⚡ Recommended areas for review

  • Cgroup handling
    cgroup setup and quota application silently ignore write failures (echo ... > cpu.max / cpu.cfs_*), which can leave the guard ineffective on systems without proper permissions. Consider explicit detection/logging when applying quotas fails and fallback behavior (or abort) so operators know quota enforcement didn't apply.

  • Whitelist breadth
    Many whitelist patterns are very generic (e.g. "bash", "sh ", "ps ", "node.*index"). These can unintentionally protect unrelated or malicious processes (and their children). The matching approach (substring + extended-regex) plus parent-propagation increases risk. Review and tighten these patterns or match against exact executable paths/comm to avoid over-whitelisting.

Comment thread scripts/rw_cpu_guard.sh
Comment on lines +389 to +415
if (( strikes == 1 )); then
# ── Strike 1: Graceful throttle ──────────────────────────────────────
_do renice +15 -p "$pid"
_do ionice -c 3 -p "$pid"
act "STRIKE1 | PID=${pid} CPU=${cpu_pct}% zone=${zone} cmd=${short_cmd}"

elif (( strikes == 2 )); then
# ── Strike 2: Hard pause — STOP 3s then CONT ─────────────────────────
_do kill -STOP "$pid"
sleep 3
_do kill -CONT "$pid"
act "STRIKE2 | PID=${pid} CPU=${cpu_pct}% zone=${zone} STOP/CONT cmd=${short_cmd}"
tg_alert "⚠️ STOP/CONT throttle — PID=${pid} CPU=${cpu_pct}% zone=${zone} — \`${short_cmd}\`"

else
# ── Strike 3+: Termination (SIGTERM then SIGKILL) ────────────────────
_do kill -TERM "$pid"
sleep 2
if _is_alive "$pid"; then
_do kill -KILL "$pid"
act "KILLED(SIGKILL) | PID=${pid} CPU=${cpu_pct}% zone=${zone} cmd=${short_cmd}"
tg_alert "💀 SIGKILL — PID=${pid} CPU=${cpu_pct}% zone=${zone} — \`${short_cmd}\`"
else
act "KILLED(SIGTERM) | PID=${pid} CPU=${cpu_pct}% zone=${zone} cmd=${short_cmd}"
tg_alert "💀 SIGTERM — PID=${pid} CPU=${cpu_pct}% zone=${zone} — \`${short_cmd}\`"
fi
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The configurable MAX_STRIKES value is never actually used in the kill ladder, so changing it in the environment has no effect and processes are always terminated from the third strike onward; this is a logic bug and can be fixed by incorporating MAX_STRIKES into the decision of when to move from STOP/CONT throttling to termination so the configured strike count is honored. [logic error]

Severity Level: Major ⚠️
- ⚠️ MAX_STRIKES configuration has no effect on kills.
- ⚠️ Admins cannot extend strike ladder before termination.
- ⚠️ Guard may kill heavy tasks earlier than intended.
Suggested change
if (( strikes == 1 )); then
# ── Strike 1: Graceful throttle ──────────────────────────────────────
_do renice +15 -p "$pid"
_do ionice -c 3 -p "$pid"
act "STRIKE1 | PID=${pid} CPU=${cpu_pct}% zone=${zone} cmd=${short_cmd}"
elif (( strikes == 2 )); then
# ── Strike 2: Hard pause — STOP 3s then CONT ─────────────────────────
_do kill -STOP "$pid"
sleep 3
_do kill -CONT "$pid"
act "STRIKE2 | PID=${pid} CPU=${cpu_pct}% zone=${zone} STOP/CONT cmd=${short_cmd}"
tg_alert "⚠️ STOP/CONT throttle — PID=${pid} CPU=${cpu_pct}% zone=${zone}\`${short_cmd}\`"
else
# ── Strike 3+: Termination (SIGTERM then SIGKILL) ────────────────────
_do kill -TERM "$pid"
sleep 2
if _is_alive "$pid"; then
_do kill -KILL "$pid"
act "KILLED(SIGKILL) | PID=${pid} CPU=${cpu_pct}% zone=${zone} cmd=${short_cmd}"
tg_alert "💀 SIGKILL — PID=${pid} CPU=${cpu_pct}% zone=${zone}\`${short_cmd}\`"
else
act "KILLED(SIGTERM) | PID=${pid} CPU=${cpu_pct}% zone=${zone} cmd=${short_cmd}"
tg_alert "💀 SIGTERM — PID=${pid} CPU=${cpu_pct}% zone=${zone}\`${short_cmd}\`"
fi
fi
if (( strikes == 1 )); then
# ── Strike 1: Graceful throttle ──────────────────────────────────────
_do renice +15 -p "$pid"
_do ionice -c 3 -p "$pid"
act "STRIKE1 | PID=${pid} CPU=${cpu_pct}% zone=${zone} cmd=${short_cmd}"
elif (( strikes < MAX_STRIKES )); then
# ── Intermediate strikes: Hard pause — STOP 3s then CONT ─────────────
_do kill -STOP "$pid"
sleep 3
_do kill -CONT "$pid"
act "STRIKE${strikes} | PID=${pid} CPU=${cpu_pct}% zone=${zone} STOP/CONT cmd=${short_cmd}"
tg_alert "⚠️ STOP/CONT throttle — PID=${pid} CPU=${cpu_pct}% zone=${zone}\`${short_cmd}\`"
else
# ── Final strike: Termination (SIGTERM then SIGKILL) ─────────────────
_do kill -TERM "$pid"
sleep 2
if _is_alive "$pid"; then
_do kill -KILL "$pid"
act "KILLED(SIGKILL) | PID=${pid} CPU=${cpu_pct}% zone=${zone} cmd=${short_cmd}"
tg_alert "💀 SIGKILL — PID=${pid} CPU=${cpu_pct}% zone=${zone}\`${short_cmd}\`"
else
act "KILLED(SIGTERM) | PID=${pid} CPU=${cpu_pct}% zone=${zone} cmd=${short_cmd}"
tg_alert "💀 SIGTERM — PID=${pid} CPU=${cpu_pct}% zone=${zone}\`${short_cmd}\`"
fi
fi
Steps of Reproduction ✅
1. Set `MAX_STRIKES=5` in the environment and start the guard with
`scripts/rw_cpu_guard.sh` so `MAX_STRIKES` is read into config at
`scripts/rw_cpu_guard.sh:67` and reported by `_show_status()` at lines 542-550.

2. Launch a CPU-intensive, non-whitelisted process (for example `yes > /dev/null &`) so
that `_scan_once()` at `scripts/rw_cpu_guard.sh:444-488` repeatedly identifies its PID as
exceeding the per-zone CPU threshold.

3. Each scan calls `_handle_hog()` at `scripts/rw_cpu_guard.sh:373-416`, which increments
the strike count for that PID via `_get_strikes` / `_set_strikes` (lines 332-347), then
dispatches behavior based only on `strikes == 1`, `strikes == 2`, or `else` at lines
389-403.

4. Despite `MAX_STRIKES=5`, observe from the logs (written by `act()` at lines 186-189)
and from the process list that on the third invocation of `_handle_hog()` (when `strikes
== 3`), the code enters the `else` branch at lines 403-415, sends SIGTERM/SIGKILL, and
kills the process, demonstrating that the configured `MAX_STRIKES` value is ignored and
termination always starts at strike 3.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** scripts/rw_cpu_guard.sh
**Line:** 389:415
**Comment:**
	*Logic Error: The configurable `MAX_STRIKES` value is never actually used in the kill ladder, so changing it in the environment has no effect and processes are always terminated from the third strike onward; this is a logic bug and can be fixed by incorporating `MAX_STRIKES` into the decision of when to move from STOP/CONT throttling to termination so the configured strike count is honored.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

@codeant-ai

codeant-ai Bot commented Mar 1, 2026

Copy link
Copy Markdown

CodeAnt AI finished reviewing your PR.

gamblecodezcom pushed a commit that referenced this pull request Mar 1, 2026
The ladder previously hardcoded 3 tiers (strike==1 / strike==2 / else),
so MAX_STRIKES had no effect — termination always fired on the 3rd hit
regardless of configuration.

Fix: replace `strikes == 2` with `strikes < MAX_STRIKES` so the STOP/CONT
pause repeats for strikes 2..(MAX_STRIKES-1) and termination only triggers
at strike >= MAX_STRIKES.  All log and Telegram messages now include the
current strike / total (e.g. "STRIKE3/5") for clarity.

Reported by CodeAnt AI review of PR #116.

https://claude.ai/code/session_01WpEqAiDbpqnBywMjYXJHf4
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL This PR changes 500-999 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants