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

  1. Use one of the three accepted names without the SIG prefix: 'TERM' (graceful, default), 'KILL' (force), or 'INT' (Ctrl-C equivalent) — case-insensitive.
  2. Restrict the client dropdown to exactly TERM/KILL/INT so invalid values can never be sent.
  3. 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

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


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/7fd1772e11b310f8. Report an issue: GitHub.