cli & replayer: Prevent Linux worker process orphaning with race-free PR_SET_PDEATHSIG - #305
Open
louzt wants to merge 7 commits into
Open
cli & replayer: Prevent Linux worker process orphaning with race-free PR_SET_PDEATHSIG#305louzt wants to merge 7 commits into
louzt wants to merge 7 commits into
Conversation
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.
…PDEATHSIG and CTest coverage
…tion-test for CI robustness
This was referenced Jul 24, 2026
…ns for multi-GPU filtering
This was referenced Jul 26, 2026
…rctl return checks The predicate that backs --device-pci-vendor / --device-pci-device selection moves from an inline expression in VulkanDevice::init_device into a single static helper on VulkanDevice::pci_filter_matches. Both the CLI selection loop and the gpu_selection_test now exercise the same logic, eliminating the risk of the test drifting from the production path. Also defensively check the return value of prctl(PR_SET_PDEATHSIG, SIGKILL) in both Linux fork sites (cli/fossilize_replay_linux.hpp and fossilize_external_replayer_linux.hpp), aborting the child with _exit(EXIT_FAILURE) on syscall failure instead of continuing as if the binding succeeded. The gpu_selection_test gains a synthetic NVIDIA / AMD / Intel catalogue and seven cases covering vendor-only, vendor+device, mismatched-device, unset-filter, middle-slot match, last-entry match, and mixed-vendor rejection. Replaces the prior 21-line smoke test. All 8 ctest pass on this host (1.94 s).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Background
When a Steam/Proton parent exits abruptly (SIGKILL, crash, game exit), the
forked
fossilize_replay/ExternalReplayerworker can end up reparentedto a subreaper (PID 1 in plain hosts,
pv-adverbinsidepressure-vessel/srt-bwrap) and keep compiling Vulkan SPIR-V in the background.Two upstream fixes already exist on this path:
close_fdsentinel +setpgidkillpg(SIGKILL)in the replayer master. Handles the case where thespawned worker shares the parent's process group.
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE.This PR complements #96 by binding the worker to a kernel-level death
signal (
prctl(PR_SET_PDEATHSIG, SIGKILL)) so the cleanup also fires whenthe worker ends up in a different process group (the CLI tool path used
when
inherit_process_group=true, seecli/fossilize_replay.cpp). It alsoadds explicit
--device-pci-vendor/--device-pci-deviceselection sohybrid laptops can pin shader compilation to the target dGPU instead of
falling through to the iGPU.
Closes: #247, #264, #279, #302.
#288 (104 GB RAM growth) and #303 (status flag) are tracked separately
and are not in scope of this PR — see "Out of scope" below.
Relationship with PR #96
flowchart TD subgraph PR96["PR #96 (HansKristian-Work, 2020) - User-Space Group Cleanup"] P1["Worker spawned"] --> P2{"inherit_process_group?"} P2 -- "false (default)" --> P3["close_fd pipe + setpgid()"] P3 --> P4["epoll sentinel in master"] P4 --> P5{"Parent dies?"} P5 -- "Yes, gracefully" --> P6["killpg(SIGKILL) in user-space"] P5 -- "SIGKILL / OOM / crash" --> P7["Master code never runs, no killpg, worker orphaned to PID 1"] P2 -- "true (CLI path)" --> P8["Pipe and setpgid bypassed, no user-space cleanup at all"] end subgraph PR305["PR #305 (Louzt, 2026) - Kernel-Level Invariant"] Q1["Worker spawned"] --> Q2["prctl(PR_SET_PDEATHSIG, SIGKILL)"] Q2 --> Q3["getppid() race-free gate"] Q3 --> Q4["Kernel delivers SIGKILL on parent death, any pgroup or namespace"] end P7 -. "Orphaned to PID 1 or pv-adverb" .-> Q1 P8 -. "Same orphan path" .-> Q1PR #96 works while control returns to the replayer epoll loop. It cannot
cover three cases that PR #305 now does:
SIGKILL, Proton panic, OOM killer):the master code path is interrupted before
killpgis reached, so theworker outlives the parent.
inherit_process_group=true(CLI tool path, seecli/fossilize_replay.cpp): theclose_fdpipe is only created when!options.inherit_process_group. When the CLI inherits the processgroup, no user-space cleanup runs at all.
pressure-vessel/srt-bwrap): workersreparented to
pv-adverb(the container subreaper) escape the parent'sprocess group, so neither
killpgnorsetpgidreaches them.PR #305 binds each worker to a kernel death signal after
fork()insteadof relying on user-space event loops, which closes all three gaps.
Changes
Linux worker cleanup (
fossilize_replay_linux.hpp,fossilize_external_replayer_linux.hpp)After
fork()in both the CLI replayer master and theExternalReplayerworker path:
The
getppid()re-check is the race-free gate: betweenfork()returningand
prctltaking effect, the parent could already have died and beenreaped. If
getppid() != parent_pid, the parent is gone and we abortcleanly instead of running unparented.
Multi-GPU selection (
cli/device.cpp,cli/fossilize_replay.cpp,fossilize_external_replayer.cpp/hpp)New flags
--device-pci-vendorand--device-pci-device(hex, e.g.0x10defor NVIDIA). Selection is gated onvendorID; the optionaldeviceIDnarrows it to a specific GPU. If the filter is set but no GPUmatches,
LOGWwarns and falls back to default selection instead ofsilently picking the wrong device.
The predicate lives in
cli/device.hpp::VulkanDevice::pci_filter_matchesso both the CLI selection loop and the unit test exercise the same logic.
Verification
All 8 ctest pass (1.5 s on this host):
orphan-prevention-testforks an intermediate parent, kills it, andasserts the grandchild is reaped within 2 s (bounded poll, 200 × 10 ms).
gpu-selection-testrunspci_filter_matchesagainst a syntheticcatalogue of NVIDIA / AMD / Intel IDs and checks vendor-only,
vendor+device, mismatched device, and unset-filter cases.
The change is Linux-only; Windows behavior is unchanged and still
governed by Job Objects (#233).
Out of scope
kernel's
drm_release()path the same way every other Vulkan processdoes. Not benchmarked here.
prctlis Linux-only. macOS still uses the PR Terminate replayer processes when parent process (unexpectedly) dies. #96 process-groupcleanup.
#288(104 GB RAM growth): separate pathological pattern — looks likecompilation state / VM growth rather than orphaning. This PR does not
address it; a separate report with
ps/nvidia-smioutput would helptriage.
#303(progress flag): enhancement request, not a bug fix. Not in scopeof this PR.
measured in this PR and have been removed.