fix(core): kill sudo/root subprocess tree on task termination (RC#7) - #1298
fix(core): kill sudo/root subprocess tree on task termination (RC#7)#1298ocervell wants to merge 1 commit into
Conversation
Celery `revoke(terminate=True)` and secator's own timeout monitor reported success but tools kept running: tools launched as `sudo <tool>` (root) run in a root-owned process, which a non-root worker cannot signal. The SIGKILL hit the worker child, orphaning the root tool (reparented to PID 1) where it also lost its timeout monitor and ran to natural completion. - Always spawn the tool in its own session (start_new_session) so stop_process can kill the whole process group via killpg and reach the sudo child. Keep setsid off only for a genuine interactive sudo prompt (preserves #722). - stop_process now escalates to `sudo -n kill -<sig> -<pgid>` when killpg/kill raises EPERM (root-owned group), so the sudo tool actually dies. Best-effort: requires a passwordless `sudo kill` sudoers entry in the worker (deploy note). - Install a worker-only SIGTERM handler so `revoke(terminate=True)` (default SIGTERM) runs this cleanup instead of dying and orphaning the child. SIGKILL is uncatchable — that path needs a deploy backstop (activeDeadlineSeconds or dropping sudo for CAP_NET_RAW/CAP_NET_ADMIN capabilities). Test spawns a real long-running child, stops it, asserts the whole process group is gone; a sudo variant skips gracefully without passwordless sudo. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
Walkthrough
ChangesCommand process lifecycle
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Worker
participant Command
participant ProcessGroup
Worker->>Command: terminate task
Command->>Command: _on_worker_term
Command->>ProcessGroup: stop_process(SIGKILL)
ProcessGroup-->>Command: process tree terminated
Command->>Worker: restore previous SIGTERM handler
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Problem (RC#7, prod 2026-07-10)
Celery
app.control.revoke(ids, terminate=True, signal="SIGKILL")reportedsuccess and
inspect().active()found all tasks, but the tasks keptrunning. Tools that run as
sudo <tool>(e.g.sudo nmap -sS) execute asroot. The terminate SIGKILL hits the worker's non-root Python child, which
orphans the root tool (reparented to PID 1) instead of killing it — and the
orphan then loses secator's timeout-monitor thread, so it runs to natural
completion (up to ~91h for a paranoid all-ports nmap, ignoring even the 10h
cap).
sudo pkillfrom inside the container also fails: the container's sudoersis passwordless for the tool only, not
pkill. Only tearing down the podkilled them.
Root cause: a non-root worker cannot signal a root process
(
os.kill/os.killpg→ EPERM), and the tool wasn't in its own process groupso the signal never reached the whole
sudo <tool>tree.Fix (secator-core)
All termination paths route through
Command.stop_process, so the fix livesthere plus the spawn:
start_new_session=True(setsid) in the worker/passwordless case, so
stop_processcankillpgthewhole group — sudo parent and tool child — in one shot. setsid is kept
off only for a genuine interactive sudo password prompt (detaching the
tty breaks it, the regression fix: broken sudo prompt because of os.setsid #722 fixed); the new condition is more precise
than the old blanket "no setsid for any sudo".
killpg/killraisesPermissionError(root-ownedgroup),
stop_processescalates tosudo -n kill -<sig> -<pgid>so the roottool actually dies. Best-effort and idempotent — logs and moves on if not
permitted.
revoke(terminate=True)sends SIGTERM to thepool child; without a handler it just dies and orphans the tool. A worker-only,
main-thread-guarded SIGTERM handler now invokes the cleanup, then restores the
previous (billiard) handler. This also fixes the timeout-monitor path that let
the orphaned nmap run unbounded.
Required deploy follow-ups (NOT changed here — code side only)
Pick one; the code supports all:
sudo, use Linux capabilities. Give the nmap binarycap_net_raw,cap_net_admin+eip(setcapat image build) and run it withoutsudo. The worker then owns the process and kills it directly — no orphan, no
sudo-kill. Cleanest.
sudo kill. Add a sudoers entry allowing the worker user torun
killpasswordless, to enable the escalation in (2).real Kubernetes Job
activeDeadlineSeconds(and prefer terminating withSIGTERM, not SIGKILL) so any orphan is bounded.
Tests
tests/unit/test_command.py::TestStopProcessKillsTreespawns a reallong-running child, calls
stop_process, and asserts the whole process group isgone (and that the tool is its own group leader). A
sudo-wrapped variantasserts the escalation path and skips gracefully where passwordless sudo is
unavailable (e.g. CI).
Open decisions
backstop required (documented above). The in-process fix targets the catchable
paths (SIGTERM terminate, timeout/memory monitor, user stop, exceptions).
sudo -n killescalation is a no-op without the sudoers entry. If you gothe capabilities route instead, it's never exercised.
🤖 Generated with Claude Code
https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
Summary by CodeRabbit
Bug Fixes
Tests