fix(status): cross-platform start-time fingerprint via psutil fallback

The PID-reuse guard (#43846) reads /proc/<pid>/stat field 22, which only
exists on Linux — on macOS/Windows it returned None and the guard silently
degraded to a bare liveness check (a no-op, safety-wise). Add a
psutil.create_time() fallback (psutil is a hard dep, cross-platform),
quantized to centiseconds for stable equality, so the recycled-PID guard
actually protects macOS/Windows too. /proc always wins first on Linux and
always misses on macOS/Windows, so the two sources never mix on one host and
same-source equality is all the guard needs.
This commit is contained in:
teknium1 2026-06-21 17:11:28 -07:00 committed by Teknium
parent 1cefc2a24e
commit 012f40c98c
2 changed files with 73 additions and 1 deletions

View file

@ -110,12 +110,37 @@ def _get_scope_lock_path(scope: str, identity: str) -> Path:
def _get_process_start_time(pid: int) -> Optional[int]:
"""Return the kernel start time for a process when available."""
"""Return a stable per-process start-time fingerprint, or None.
Used as a PID-reuse guard: a ``(pid, start_time)`` pair uniquely identifies
a process, so a recycled PID (same number, different process) yields a
different value and is never mistaken for the original.
On Linux this is field 22 of ``/proc/<pid>/stat`` (start time in clock
ticks since boot, an int). On platforms without ``/proc`` (macOS, Windows)
we fall back to ``psutil.Process(pid).create_time()`` a float epoch
timestamp quantized to an int (centiseconds) for stable equality.
The two sources are never mixed on a single platform: ``/proc`` always
succeeds first on Linux, and always fails on macOS/Windows so psutil is
always used there. Because the guard only compares the value recorded at
spawn against the live value *on the same host*, the differing units across
platforms are irrelevant only same-source equality matters.
"""
stat_path = Path(f"/proc/{pid}/stat")
try:
# Field 22 in /proc/<pid>/stat is process start time (clock ticks).
return int(stat_path.read_text(encoding="utf-8").split()[21])
except (FileNotFoundError, IndexError, PermissionError, ValueError, OSError):
pass
# No /proc (macOS / Windows): psutil is a hard dependency and exposes a
# cross-platform creation time. Quantize to centiseconds so repeated reads
# of the same process compare equal without float-precision fragility.
try:
import psutil # type: ignore
return int(round(psutil.Process(pid).create_time() * 100))
except Exception:
return None

View file

@ -359,6 +359,53 @@ class TestGatewayRuntimeStatus:
assert payload["platforms"]["discord"]["error_message"] is None
class TestGetProcessStartTime:
"""Start-time fingerprint backing the PID-reuse guard (#43846 / #50468).
Must be stable across repeated reads of the same live process and degrade to
a cross-platform psutil fallback when /proc is unavailable (macOS/Windows),
so the guard isn't a Linux-only no-op.
"""
def test_live_process_is_stable_int(self):
import subprocess
import time
p = subprocess.Popen(["sleep", "20"])
try:
a = status._get_process_start_time(p.pid)
time.sleep(0.2)
b = status._get_process_start_time(p.pid)
assert a is not None and isinstance(a, int)
assert a == b # same process → identical fingerprint
finally:
p.kill()
p.wait()
def test_dead_pid_returns_none(self):
assert status._get_process_start_time(999999999) is None
def test_psutil_fallback_when_no_proc(self, monkeypatch):
"""When /proc is missing (macOS/Windows), psutil supplies a stable int."""
import subprocess
orig_read_text = Path.read_text
def no_proc(self, *args, **kwargs):
if str(self).startswith("/proc/"):
raise FileNotFoundError
return orig_read_text(self, *args, **kwargs)
monkeypatch.setattr(Path, "read_text", no_proc)
p = subprocess.Popen(["sleep", "20"])
try:
a = status._get_process_start_time(p.pid)
b = status._get_process_start_time(p.pid)
assert a is not None and isinstance(a, int)
assert a == b # fallback is stable across reads
finally:
p.kill()
p.wait()
class TestTerminatePid:
def test_force_uses_taskkill_on_windows(self, monkeypatch):
calls = []