odysseus-dev/odysseus · error · HTTPException
signal must be TERM, KILL, or INT
Error message
signal must be TERM, KILL, or INT
What it means
Raised (HTTP 400) by POST /api/cookbook/kill-pid when req.signal, uppercased, is not one of TERM, KILL, or INT. The endpoint only forwards whitelisted POSIX signal names into `kill -<sig> <pid>` (locally or over SSH), rejecting anything else to prevent shell injection through the signal field. The default is 'TERM' when signal is omitted/null.
Source
Thrown at routes/cookbook_routes.py:3329
pid: int
host: str | None = None
ssh_port: str | None = None
signal: str = "TERM" # TERM (graceful) or KILL (force)
@router.post("/api/cookbook/kill-pid")
async def kill_pid(request: Request, req: KillPidRequest):
"""Kill a PID that's holding GPU memory.
Admin-gated. Validates PID is positive int, signal is TERM/KILL, and
forbids low PIDs (<100) to avoid accidentally signalling init/system
daemons. Uses `kill -<sig> <pid>` locally or over SSH.
"""
require_admin(request)
if req.pid < 100:
raise HTTPException(400, f"Refusing to signal PID {req.pid} (<100, likely system process)")
sig = (req.signal or "TERM").upper()
if sig not in ("TERM", "KILL", "INT"):
raise HTTPException(400, "signal must be TERM, KILL, or INT")
host = validate_remote_host(req.host)
req.ssh_port = validate_ssh_port(req.ssh_port)
kill_cmd = f"kill -{sig} {req.pid}"
try:
if host:
pf = f"-p {req.ssh_port} " if req.ssh_port and req.ssh_port != "22" else ""
cmd = f"ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no {pf}{host} '{kill_cmd}'"
proc = await asyncio.create_subprocess_shell(
cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
)
elif IS_WINDOWS:
# No `kill` binary / POSIX signals on Windows. taskkill /F /T tears
# down the PID and its children. There's no graceful-vs-force
# distinction, so TERM/KILL/INT all map to the same forced kill.
# NB: never use os.kill(pid, 0) to probe here — on Windows that
# routes to TerminateProcess and would kill the process.
if not pid_alive(req.pid):
return {"ok": False, "error": f"PID {req.pid} is not running"}View on GitHub (pinned to f9235ebbf1)
Solutions
- Use one of the three accepted names without the SIG prefix: 'TERM' (graceful, default), 'KILL' (force), or 'INT' (Ctrl-C equivalent) — case-insensitive.
- Restrict the client dropdown to exactly TERM/KILL/INT so invalid values can never be sent.
- If you need other signals, patch the whitelist in the route and keep values uppercase bare names.
Example fix
// before
await post('/api/cookbook/kill-pid', { pid: 23841, signal: 'SIGKILL' }); // 400
// after
await post('/api/cookbook/kill-pid', { pid: 23841, signal: 'KILL' }); Defensive patterns
Strategy: validation
Validate before calling
const ALLOWED_SIGNALS = new Set(['TERM', 'KILL', 'INT']);
function normalizeSignal(sig?: string): string {
const s = (sig ?? 'TERM').toUpperCase().replace(/^SIG/, '');
if (!ALLOWED_SIGNALS.has(s)) throw new Error(`signal must be one of ${[...ALLOWED_SIGNALS].join('/')}`);
return s;
} Type guard
const isAllowedSignal = (s: unknown): s is 'TERM' | 'KILL' | 'INT' => typeof s === 'string' && ['TERM', 'KILL', 'INT'].includes(s.toUpperCase());
Prevention
- Restrict the signal dropdown to TERM/KILL/INT only.
- Strip any 'SIG' prefix before sending (SIGKILL → KILL).
When it happens
Trigger: POST /api/cookbook/kill-pid with signal values like 'HUP', 'SIGKILL' (the SIG- prefix fails the check), 'STOP', 'quit', or any arbitrary string. Empty/None signal does NOT trigger it — the code defaults to TERM.
Common situations: Client sends 'SIGKILL' instead of 'KILL' (the most common mismatch, since the kill CLI accepts both); client offers a full signal dropdown and the user picks an unsupported one; a script passes kill -9 style numeric signals as '9'.
Related errors
- Refusing to signal PID {req.pid} (<100, likely system proces
- Remote Windows Diffusers serving is not supported yet; use l
- host is required
- This task has no clearable cache
- d && d.detail ? d.detail : ('HTTP ' + res.status)
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/7fd1772e11b310f8.
Report an issue: GitHub.