fix(cli): kill orphaned scans when the parent process dies

PyInstaller onefile binaries run as an outer bootloader + inner Python
app. When a caller stops a scan by killing the direct child (e.g.
subprocess.run(timeout=...) sends SIGKILL, or a pipeline/CI sends
SIGKILL), only the bootloader dies: the inner app is reparented to
PID 1 and keeps scanning forever, leaking resources until OOM.

Strix already installs SIGINT/SIGTERM/SIGHUP handlers, but none of
those fire because SIGKILL cannot be caught or forwarded by the
bootloader. On Linux, PR_SET_PDEATHSIG asks the kernel to deliver
SIGTERM the moment the parent dies, so the app's existing SIGTERM
cleanup path runs instead of orphaning the scan.

No-op on non-Linux platforms.
This commit is contained in:
aunttwister 2026-09-08 09:40:47 +00:00
parent 52b1923347
commit 357f1baf7c

View file

@ -6,6 +6,7 @@ Strix Agent Interface
import argparse
import asyncio
import contextlib
import signal
import sys
from pathlib import Path
@ -418,7 +419,38 @@ def _bootstrap_scan(args: argparse.Namespace) -> None:
telemetry_start(args)
def _enable_parent_death_signal() -> None:
"""On Linux, ask the kernel to deliver SIGTERM when our parent dies.
Strix ships as a PyInstaller onefile binary: the process users see is an
outer bootloader that spawns the real Python app as a child. When a caller
stops a scan by killing the direct child -- e.g. ``subprocess.run(..., timeout=)``
sends SIGKILL, or a container/pipeline sends SIGKILL -- the bootloader dies
but the inner app cannot receive or forward that SIGKILL: it is reparented
to PID 1 and keeps scanning as an orphaned background process.
PR_SET_PDEATHSIG makes the kernel deliver SIGTERM to this process the moment
the bootloader dies, so the app's existing SIGTERM handler (cleanup + exit)
runs instead of leaking an orphaned scan. No-op outside Linux.
"""
if sys.platform != "linux":
return
try:
import ctypes # noqa: PLC0415 (stdlib; late import keeps startup lean)
libc = ctypes.CDLL(None, use_errno=True)
PR_SET_PDEATHSIG = 1
if libc.prctl(PR_SET_PDEATHSIG, signal.SIGTERM) != 0:
logger.warning(
"prctl(PR_SET_PDEATHSIG, SIGTERM) failed: %s",
ctypes.get_errno(),
)
except Exception:
logger.debug("PR_SET_PDEATHSIG unavailable", exc_info=True)
def main() -> None:
_enable_parent_death_signal()
configure_dependency_logging()
if sys.platform == "win32":