From 6eaf2baa57ddde213322a4c16f3a6f8c7726bc61 Mon Sep 17 00:00:00 2001 From: jeffr Date: Sat, 21 Feb 2026 23:10:06 -0800 Subject: [PATCH] fix: detect zombie processes in isPidAlive on Linux kill(pid, 0) succeeds for zombie processes, causing the gateway lock to treat a zombie lock owner as alive. Read /proc//status on Linux to check for 'Z' (zombie) state before reporting the process as alive. This prevents the lock from being held indefinitely by a zombie process during gateway restart. Co-Authored-By: Claude Opus 4.6 --- src/shared/pid-alive.test.ts | 47 ++++++++++++++++++++++++++++++++++++ src/shared/pid-alive.ts | 24 +++++++++++++++++- 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 src/shared/pid-alive.test.ts diff --git a/src/shared/pid-alive.test.ts b/src/shared/pid-alive.test.ts new file mode 100644 index 00000000000..70249a961ff --- /dev/null +++ b/src/shared/pid-alive.test.ts @@ -0,0 +1,47 @@ +import fsSync from "node:fs"; +import { describe, expect, it, vi } from "vitest"; +import { isPidAlive } from "./pid-alive.js"; + +describe("isPidAlive", () => { + it("returns true for the current running process", () => { + expect(isPidAlive(process.pid)).toBe(true); + }); + + it("returns false for a non-existent PID", () => { + expect(isPidAlive(2 ** 30)).toBe(false); + }); + + it("returns false for invalid PIDs", () => { + expect(isPidAlive(0)).toBe(false); + expect(isPidAlive(-1)).toBe(false); + expect(isPidAlive(Number.NaN)).toBe(false); + expect(isPidAlive(Number.POSITIVE_INFINITY)).toBe(false); + }); + + it("returns false for zombie processes on Linux", async () => { + const zombiePid = process.pid; + + // Mock readFileSync to return zombie state for /proc//status + const originalReadFileSync = fsSync.readFileSync; + vi.spyOn(fsSync, "readFileSync").mockImplementation((filePath, encoding) => { + if (filePath === `/proc/${zombiePid}/status`) { + return `Name:\tnode\nUmask:\t0022\nState:\tZ (zombie)\nTgid:\t${zombiePid}\nPid:\t${zombiePid}\n`; + } + return originalReadFileSync(filePath as never, encoding as never) as never; + }); + + // Override platform to linux so the zombie check runs + const originalPlatform = process.platform; + Object.defineProperty(process, "platform", { value: "linux", writable: true }); + + try { + // Re-import the module so it picks up the mocked platform and fs + vi.resetModules(); + const { isPidAlive: freshIsPidAlive } = await import("./pid-alive.js"); + expect(freshIsPidAlive(zombiePid)).toBe(false); + } finally { + Object.defineProperty(process, "platform", { value: originalPlatform, writable: true }); + vi.restoreAllMocks(); + } + }); +}); diff --git a/src/shared/pid-alive.ts b/src/shared/pid-alive.ts index a1e9c84eac7..d3aeaaf6f43 100644 --- a/src/shared/pid-alive.ts +++ b/src/shared/pid-alive.ts @@ -1,11 +1,33 @@ +import fsSync from "node:fs"; + +/** + * Check if a process is a zombie on Linux by reading /proc//status. + * Returns false on non-Linux platforms or if the proc file can't be read. + */ +function isZombieProcess(pid: number): boolean { + if (process.platform !== "linux") { + return false; + } + try { + const status = fsSync.readFileSync(`/proc/${pid}/status`, "utf8"); + const stateMatch = status.match(/^State:\s+(\S)/m); + return stateMatch?.[1] === "Z"; + } catch { + return false; + } +} + export function isPidAlive(pid: number): boolean { if (!Number.isFinite(pid) || pid <= 0) { return false; } try { process.kill(pid, 0); - return true; } catch { return false; } + if (isZombieProcess(pid)) { + return false; + } + return true; }