odysseus-dev/odysseus · error · HTTPException

Refusing to signal PID {req.pid} (<100, likely system proces

Error message

Refusing to signal PID {req.pid} (<100, likely system process)

What it means

Raised (HTTP 400) by POST /api/cookbook/kill-pid (admin-gated) when req.pid < 100. Low PIDs on Unix-like systems are kernel threads and core system daemons (init is 1, kworkers and early daemons occupy the low range), so signalling them risks destabilizing or rebooting the host. The guard fires before any local kill or SSH kill command is constructed.

Source

Thrown at routes/cookbook_routes.py:3326

        return {"ok": False, "error": nvidia_error or "No GPU memory probe available", "gpus": []}

    class KillPidRequest(BaseModel):
        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

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Send the real PID of the GPU-memory-holding process (nvidia-smi / fuser output), which is virtually always >= 100.
  2. Verify the PID before submitting: check it maps to a python/ollama/docker process you own, e.g. `ps -p <pid> -o pid,comm,user`.
  3. If a client UI auto-fills the PID field, remove the default so the user must explicitly pick a row.

Example fix

// before
await post('/api/cookbook/kill-pid', { pid: 0, signal: 'KILL' }); // 400

// after
await post('/api/cookbook/kill-pid', { pid: 23841, signal: 'TERM' });
Defensive patterns

Strategy: validation

Validate before calling

function assertKillablePid(pid: number) {
  if (!Number.isInteger(pid) || pid < 100) {
    throw new Error(`PID ${pid} is invalid or below the safe threshold (>=100 required)`);
  }
}

Type guard

const isSafePid = (pid: unknown): pid is number => typeof pid === 'number' && Number.isInteger(pid) && pid >= 100;

Try / catch

try { await post('/api/cookbook/kill-pid', { pid, signal: 'TERM' }); } catch (e) { if (e.status === 400 && /<100/.test(e.message)) { toast('Pick the real GPU process PID from nvidia-smi'); return; } throw e; }

Prevention

When it happens

Trigger: POST /api/cookbook/kill-pid with body {"pid": 1}, {"pid": 42}, or any pid in 0–99. Also triggered by a UI bug that defaults the PID field to 0 or 1 when no GPU-memory-holding process was actually selected.

Common situations: User copies a PID column misaligned with the process list (header row or footer summary); GPU diagnostics output is parsed incorrectly and a line number is sent instead of a PID; automated cleanup script passes an unset variable that defaults to 0.

Related errors


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