From 357f1baf7cdb3833d1f50b34f0ed02cc6b36d42e Mon Sep 17 00:00:00 2001 From: aunttwister Date: Tue, 8 Sep 2026 09:40:47 +0000 Subject: [PATCH] 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. --- strix/interface/main.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/strix/interface/main.py b/strix/interface/main.py index c9bd55961..7c1a3c783 100644 --- a/strix/interface/main.py +++ b/strix/interface/main.py @@ -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":