From 310bbe9fce31473c8b5b1b3f82a4654463d97b19 Mon Sep 17 00:00:00 2001 From: ProofOfConcept Date: Fri, 3 Apr 2026 23:47:36 -0400 Subject: [PATCH] KillOnDrop: SIGTERM process group when tool task is aborted tokio::spawn abort drops the future but leaves child processes running as orphans. KillOnDrop sends SIGTERM to the process group on drop, ensuring cleanup. Defused via mem::forget on normal completion. Co-Developed-By: Kent Overstreet --- src/agent/tools/bash.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/agent/tools/bash.rs b/src/agent/tools/bash.rs index 8482335..e525851 100644 --- a/src/agent/tools/bash.rs +++ b/src/agent/tools/bash.rs @@ -14,6 +14,18 @@ use tokio::io::AsyncReadExt; use super::{ToolDef, ProcessTracker, default_timeout}; +/// RAII guard that SIGTERMs the process group on drop. +/// Ensures child processes are cleaned up when a task is aborted. +struct KillOnDrop(u32); // pid + +impl Drop for KillOnDrop { + fn drop(&mut self) { + if self.0 != 0 { + unsafe { libc::kill(-(self.0 as i32), libc::SIGTERM); } + } + } +} + #[derive(Deserialize)] struct Args { command: String, @@ -60,6 +72,7 @@ pub async fn run_bash(args: &serde_json::Value, tracker: &ProcessTracker) -> Res .with_context(|| format!("Failed to spawn: {}", command))?; let pid = child.id().unwrap_or(0); + let kill_guard = KillOnDrop(pid); tracker.register(pid, command).await; // Take ownership of stdout/stderr handles before waiting, @@ -132,6 +145,8 @@ pub async fn run_bash(args: &serde_json::Value, tracker: &ProcessTracker) -> Res } }; + // Process completed normally — defuse the kill guard + std::mem::forget(kill_guard); tracker.unregister(pid).await; result }