Skip to content

cli & replayer: Prevent Linux worker process orphaning with race-free PR_SET_PDEATHSIG#305

Open
louzt wants to merge 6 commits into
ValveSoftware:masterfrom
louzt:fix/linux-orphan-reaper
Open

cli & replayer: Prevent Linux worker process orphaning with race-free PR_SET_PDEATHSIG#305
louzt wants to merge 6 commits into
ValveSoftware:masterfrom
louzt:fix/linux-orphan-reaper

Conversation

@louzt

@louzt louzt commented Jul 24, 2026

Copy link
Copy Markdown

Background & Issue Description

When fossilize_replay or ExternalReplayer parent processes terminate unexpectedly (e.g. SIGKILL, Proton game exit, or crash), spawned child replayer workers can become orphaned or <defunct>. On Linux systems, these orphaned processes continue executing Vulkan SPIR-V pipeline compilations in the background, consuming ~50–100% CPU and GPU compute cycles indefinitely without being reclaimed.

Cross-Platform Architectural Parity (Windows Job Objects vs. Linux PR_SET_PDEATHSIG)

On Windows, Fossilize already enforces automatic child process teardown via Windows Job Objects (CreateJobObjectW configured with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE in fossilize_external_replayer_windows.hpp). When a parent process dies on Windows, the OS kernel automatically terminates all associated child processes in the Job Object.

On Linux, however, a feature disparity existed: child workers lacked kernel-level death signal bindings (PR_SET_PDEATHSIG), allowing worker processes to detach and run indefinitely attached to PID 1. This PR brings Linux to full architectural parity with Windows Job Objects, while also introducing hardware-based multi-GPU selection parameters (--device-pci-vendor and --device-pci-device).

Linked Issues


Technical & Architectural Overview

1. Cross-Platform Process Teardown Parity (Linux PR_SET_PDEATHSIG vs. Windows JobObjects)

  • Windows: Uses CreateJobObjectW + AssignProcessToJobObject with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE.
  • Linux (This PR): Uses prctl(PR_SET_PDEATHSIG, SIGKILL) immediately upon fork(), coupled with a race-free getppid() != parent_pid check. Once terminated, child processes are adopted and reaped by PID 1 (or container subreaper pv-adverb in pressure-vessel), preventing background execution and <defunct> zombie table accumulation.

2. Multi-GPU Hardware Selection & Windows Hex Formatting (--device-pci-vendor & --device-pci-device)

To eliminate redundant pre-compilation on hybrid laptops (e.g. AMD Vega iGPU + NVIDIA RTX dGPU):

  • Added --device-pci-vendor <vendorID_hex> and --device-pci-device <deviceID_hex> parameters to cli/device.cpp, cli/fossilize_replay.cpp, and ExternalReplayer::Options.
  • Formatted command-line arguments using explicit hexadecimal specifiers (0x%x via snprintf) across both Linux and Windows platforms (fossilize_external_replayer_windows.hpp), avoiding decimal string conversion mismatches when parsed by strtoul(..., 0).
  • Allows applications and Steam runtime wrappers to pin compilation directly to the target gaming GPU (e.g., --device-pci-vendor 0x10de for NVIDIA), saving ~50% CPU/RAM overhead on dual-GPU laptops.

3. Container PID Namespace & Kernel Portability (pressure-vessel / bwrap)

In Steam Linux Runtime and SteamOS (pressure-vessel / srt-bwrap), fossilize_replay master and worker processes execute within the same container PID namespace.

  • At fork(), getppid() in the child returns the parent's container PID ($P_{inner}$).
  • If the parent dies, Linux reparents the child to the container's init process (PID 1 inside bwrap, e.g., pv-adverb), causing getppid() to evaluate to 1 (or the subreaper PID).
  • The getppid() != parent_pid check remains fully coherent across container PID namespaces.

4. DRM Subsystem, UMA Memory Reclamation & Signal Safety

  • Unified Memory Architecture (UMA) & VRAM Safety: Terminating replayers via SIGKILL triggers close() on the /dev/dri/renderD128 or /dev/nvidia0 file descriptors. The Linux DRM/GEM subsystem (drm_release()) atomically reclaims VRAM buffer objects and unmaps GPU MMU pages without VRAM leaks or driver instability.
  • Signal Handler Compatibility: POSIX SIGKILL delivered via PR_SET_PDEATHSIG operates orthogonally to internal signal handlers (SIGSEGV, SIGFPE, SIGILL, SIGBUS, SIGABRT).

Process Flow Diagrams

1. Cross-Platform Process Teardown (Linux vs. Windows Parity)

flowchart TD
    subgraph Windows ["Windows OS Platform"]
        WinParent["Parent Process"] -->|Creates JobObject| WinJob["Job Object (JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE)"]
        WinJob -->|Assigns| WinChild["Worker Subprocess"]
        WinParent -.->|On Parent Exit| WinKernel["Windows Kernel Auto-Kills Job Object Children"]
    end

    subgraph Linux ["Linux OS Platform (PR #305)"]
        LinParent["Parent Process (parent_pid)"] -->|forks| LinChild["Worker Subprocess"]
        LinChild -->|prctl| LinPdeath["PR_SET_PDEATHSIG(SIGKILL) + getppid() Check"]
        LinParent -.->|On Parent Exit| LinKernel["Linux Kernel Auto-Delivers SIGKILL to Workers"]
    end

    classDef platformStyle fill:#e1f5fe,stroke:#0288d1,stroke-width:2px;
    class Windows,Linux platformStyle;
Loading

2. GPU Selection & Multi-Platform IPC Argument Propagation

sequenceDiagram
    autonumber
    participant Steam as Steam / Master Process
    participant Ext as ExternalReplayer
    participant CLI as fossilize_replay (Worker)
    participant VK as Vulkan Instance

    Steam->>Ext: Options { device_pci_vendor: 0x10de, device_pci_device: 0x2488 }
    alt Linux Platform
        Ext->>CLI: execv("--device-pci-vendor 0x10de --device-pci-device 0x2488")
    else Windows Platform
        Ext->>CLI: CreateProcess(cmdline with "0x10de" hex formatting)
    end
    CLI->>VK: vkEnumeratePhysicalDevices()
    VK-->>CLI: Returns GPU list [0: iGPU (0x8086), 1: dGPU (0x10de)]
    CLI->>CLI: Deterministic match: vendorID == 0x10de && deviceID == 0x2488
    CLI->>VK: vkCreateDevice(Selected GPU #1)
    Note over CLI, VK: Replay shaders strictly on target dGPU (Zero iGPU overhead)
Loading

Verification & CTest Coverage

  • CTest Results: All 8 tests passed (100% pass rate):
    100% tests passed, 0 tests failed out of 8
    Total Test time (real) = 1.90 sec
    
  • New Unit Tests:
    • orphan-prevention-test: Validates process lifecycle signal delivery and race-free getppid() checking.
    • gpu-selection-test: Validates GPU selection options structure and vendor ID filtering.

Systems Case Study & Root Cause Analysis (RCA)

For full architectural details, container namespace analysis, cross-platform job object parity breakdown, and extended code analysis, see the published RCA Case Study:

louzt added 3 commits July 16, 2026 23:23
When Fossilize is forcefully terminated or crashes, the child replay processes currently become orphans and continue to compile shaders in the background, leading to excessive CPU/RAM usage. This adds PR_SET_PDEATHSIG to ensure child processes are terminated immediately when the parent process dies, matching the expected lifecycle of the application.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant