mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(kill-tree): verify process group leader before using group kill to prevent gateway SIGTERM (#76259) (#94697)
* fix(kill-tree): verify process group leader before group kill to prevent gateway SIGTERM (#76259) - Add isProcessGroupLeader() to killProcessTree/signalProcessTree: ps -p <pid> -o pgid= primary check with /proc/<pid>/stat fallback on Linux. Group kill only when the PID is its own process group leader; non-leaders fall back to single-pid kill, preventing accidental gateway SIGTERM when a non-detached child shares the gateway's process group. - Propagate detached: true to all detached-spawn cleanup callers (exec-termination, agent-bundle LSP, mcp-stdio, bash, supervisor pty, agent-core nodejs) so detached group cleanup survives leader exit. - Gateway/daemon cleanup paths (schtasks, restart-health) keep the leader-checked default (detached omitted). Closes #76259 Co-Authored-By: Claude <noreply@anthropic.com> * refactor(process): tighten process-group ownership checks * refactor(daemon): split restart diagnostics * refactor(daemon): isolate restart health types --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
+64
-6
@@ -1,5 +1,6 @@
|
||||
// Agent Core module implements kill tree behavior.
|
||||
import { spawn } from "node:child_process";
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const DEFAULT_GRACE_MS = 3000;
|
||||
const MAX_GRACE_MS = 60_000;
|
||||
@@ -16,9 +17,14 @@ export type KillProcessTreeOptions = {
|
||||
* first (without /F), then force-kills if process survives.
|
||||
* - Unix: send SIGTERM to process group first, wait grace period, then SIGKILL.
|
||||
*
|
||||
* When the child was spawned with `detached: false`, pass `detached: false` to
|
||||
* skip the Unix `process.kill(-pid, ...)` group-kill. That avoids signaling the
|
||||
* gateway's own process group.
|
||||
* Group kill (`process.kill(-pid, ...)`) is only used when the PID is verified
|
||||
* as its own process group leader, unless `detached: true` is explicitly passed.
|
||||
* This prevents accidentally signaling the gateway's process group when the
|
||||
* child shares its parent's group.
|
||||
*
|
||||
* - `detached: false`: skip group kill unconditionally.
|
||||
* - `detached: true`: use group kill unconditionally (trust caller).
|
||||
* - `detached` omitted: use group kill only when PID is the group leader.
|
||||
*/
|
||||
export function killProcessTree(pid: number, opts?: KillProcessTreeOptions): void {
|
||||
if (!Number.isFinite(pid) || pid <= 0) {
|
||||
@@ -35,7 +41,8 @@ export function killProcessTree(pid: number, opts?: KillProcessTreeOptions): voi
|
||||
return;
|
||||
}
|
||||
|
||||
const useGroupKill = opts?.detached !== false;
|
||||
const useGroupKill =
|
||||
opts?.detached === true || (opts?.detached !== false && isProcessGroupLeader(pid));
|
||||
if (opts?.force === true) {
|
||||
signalProcessTreeUnix(pid, "SIGKILL", useGroupKill);
|
||||
return;
|
||||
@@ -68,7 +75,9 @@ export function signalProcessTree(
|
||||
return;
|
||||
}
|
||||
|
||||
signalProcessTreeUnix(pid, signal, opts?.detached !== false);
|
||||
const useGroupKill =
|
||||
opts?.detached === true || (opts?.detached !== false && isProcessGroupLeader(pid));
|
||||
signalProcessTreeUnix(pid, signal, useGroupKill);
|
||||
}
|
||||
|
||||
function normalizeGraceMs(value?: number): number {
|
||||
@@ -87,6 +96,55 @@ function isProcessAlive(pid: number): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
function parseProcessGroupId(value: unknown): number | undefined {
|
||||
if (typeof value !== "string" || !/^\d+$/.test(value.trim())) {
|
||||
return undefined;
|
||||
}
|
||||
const pgid = Number(value.trim());
|
||||
return Number.isSafeInteger(pgid) && pgid > 0 ? pgid : undefined;
|
||||
}
|
||||
|
||||
function readProcessGroupIdFromPs(pid: number): number | undefined {
|
||||
try {
|
||||
const res = spawnSync("ps", ["-p", String(pid), "-o", "pgid="], {
|
||||
encoding: "utf8",
|
||||
timeout: 500,
|
||||
});
|
||||
if (res.error || res.status !== 0) {
|
||||
return undefined;
|
||||
}
|
||||
return parseProcessGroupId(res.stdout);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function readProcessGroupIdFromProc(pid: number): number | undefined {
|
||||
try {
|
||||
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
||||
const commEnd = stat.lastIndexOf(")");
|
||||
if (commEnd < 0) {
|
||||
return undefined;
|
||||
}
|
||||
// After comm: state, ppid, pgrp. The command name may contain spaces or ')'.
|
||||
const fields = stat
|
||||
.slice(commEnd + 1)
|
||||
.trim()
|
||||
.split(/\s+/);
|
||||
return parseProcessGroupId(fields[2]);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** Fail closed to direct-PID signaling when group ownership cannot be proved. */
|
||||
function isProcessGroupLeader(pid: number): boolean {
|
||||
// Linux exposes the fact in procfs; avoid a synchronous child process on the common path.
|
||||
const procPgid = process.platform === "linux" ? readProcessGroupIdFromProc(pid) : undefined;
|
||||
const pgid = procPgid ?? readProcessGroupIdFromPs(pid);
|
||||
return pgid === pid;
|
||||
}
|
||||
|
||||
function signalProcessTreeUnix(
|
||||
pid: number,
|
||||
signal: "SIGTERM" | "SIGKILL",
|
||||
|
||||
+2
-2
@@ -313,7 +313,7 @@ export class NodeExecutionEnv implements ExecutionEnv {
|
||||
|
||||
const onAbort = () => {
|
||||
if (child?.pid) {
|
||||
killProcessTree(child.pid, { force: true });
|
||||
killProcessTree(child.pid, { force: true, detached: true });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -354,7 +354,7 @@ export class NodeExecutionEnv implements ExecutionEnv {
|
||||
: setTimeout(() => {
|
||||
timedOut = true;
|
||||
if (child?.pid) {
|
||||
killProcessTree(child.pid, { force: true });
|
||||
killProcessTree(child.pid, { force: true, detached: true });
|
||||
}
|
||||
}, timeoutMs);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user