From 80ffcbf82c77478e8a6de149827f26bc183aa1aa Mon Sep 17 00:00:00 2001 From: aunttwister Date: Tue, 8 Sep 2026 10:27:44 +0000 Subject: [PATCH] fix(cli): close parent-death signaling gaps found in review - Gate PR_SET_PDEATHSIG on frozen PyInstaller binaries (is_binary_install) so pip/source installs are not killed when their launching shell exits. - Capture the parent PID before prctl and verify afterward: if the bootloader died before registration the process is reparented and PDEATHSIG would watch the wrong parent, so deliver SIGTERM ourselves. - Install a cleanup-aware SIGTERM fallback before enabling the parent-death signal; run_cli replaces it later, and interactive/TUI scans now enter report-state cleanup instead of dying by the default action. --- strix/interface/main.py | 50 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/strix/interface/main.py b/strix/interface/main.py index 7c1a3c783..8ac1c81aa 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 os import signal import sys from pathlib import Path @@ -419,8 +420,31 @@ def _bootstrap_scan(args: argparse.Namespace) -> None: telemetry_start(args) +def _install_cleanup_sigterm_handler() -> None: + """Install a cleanup-aware SIGTERM fallback before the scan engine starts. + + ``run_cli`` installs its own SIGINT/SIGTERM/SIGHUP handler once a report + state exists, but the interactive Go TUI installs none Python-side. Without + an early handler, a SIGTERM that arrives mid-setup or during an interactive + scan terminates the process by the default action, skipping report-state + cleanup. This fallback runs ``cleanup(status="interrupted")`` when a report + state exists, then exits; scan paths replace it with their own handler as + soon as it is safe to do so. + """ + + def _on_sigterm(_signum: int, _frame: object) -> None: + from strix.report.state import get_global_report_state + + state = get_global_report_state() + if state is not None: + state.cleanup(status="interrupted") + sys.exit(1) + + signal.signal(signal.SIGTERM, _on_sigterm) + + def _enable_parent_death_signal() -> None: - """On Linux, ask the kernel to deliver SIGTERM when our parent dies. + """On Linux frozen binaries, 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 @@ -430,26 +454,40 @@ def _enable_parent_death_signal() -> None: 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. + the bootloader dies, so the app's SIGTERM handler (cleanup + exit) runs + instead of leaking an orphaned scan. Only meaningful when running as the + inner process of a PyInstaller onefile binary; pip/source installs keep the + user's shell as their parent and must not be killed when that shell exits. + No-op outside Linux or when not frozen. """ if sys.platform != "linux": return + if not is_binary_install(): + return try: - import ctypes # noqa: PLC0415 (stdlib; late import keeps startup lean) + import ctypes # 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: + pr_set_pdeathsig = 1 + parent_pid = os.getppid() + if libc.prctl(pr_set_pdeathsig, signal.SIGTERM) != 0: logger.warning( "prctl(PR_SET_PDEATHSIG, SIGTERM) failed: %s", ctypes.get_errno(), ) + return + # The bootloader can die between spawning this process and the prctl + # above; the process is then reparented before the kernel snapshots the + # parent, and PDEATHSIG would watch the wrong parent. Deliver SIGTERM + # ourselves when that happened so the scan still cleans up and exits. + if os.getppid() != parent_pid: + os.kill(os.getpid(), signal.SIGTERM) except Exception: logger.debug("PR_SET_PDEATHSIG unavailable", exc_info=True) def main() -> None: + _install_cleanup_sigterm_handler() _enable_parent_death_signal() configure_dependency_logging()