Skip to content

fix(vk): report a driver fault in a pipeline compile on Linux - #975

Closed
tickernelz wants to merge 5 commits into
NotRexed:mainfrom
tickernelz:zha5347/linux-driver-fault-guard
Closed

tickernelz wants to merge 5 commits into
NotRexed:mainfrom
tickernelz:zha5347/linux-driver-fault-guard

Conversation

@tickernelz

@tickernelz tickernelz commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

What this fixes

On Linux a driver that faults while compiling a pipeline ends the game. Windows has had a guard for this since the amdvlk mesh-program crash (issue 26): __try around vkCreateComputePipelines and vkCreateGraphicsPipelines, one lost pipeline. The #else branch called the driver as it was.

That is the difference between a report and a dead process in issues 915 and 920 (RX 6700 XT and RX 7900 XTX, RADV, crash right after shader preparation), and in a report on an RX 6800 XT with Mesa 26.2.6:

CRASH: exception 0xc0000005 (write 0x40) at libvulkan_radeon.so+0x14a7d2
radv_shader_spirv_to_nir <- radv_compile_cs <- radv_CreateComputePipelines

The SPIR-V there is invalid, and RADV dereferences what spirv_to_nir returns for it. The invalid variants themselves are the subject of #894, #834 and #848; this PR is the container around the driver call, so the next invalid shader costs one pipeline instead of the start.

Change

  • gpu/vk/vk_draw.cpp: the Linux guarded_compute and guarded_graphics arm a thread-local window around the driver call and catch SIGSEGV, SIGBUS, SIGILL and SIGFPE inside it, returning the same values the Windows branch returns (-77 for compute, VK_ERROR_UNKNOWN for graphics). Those are the four the host's own handler translates (host/posix/win32_posix.c), so the guard sees the same set the crash report does.

  • A fault outside the window is handed to whoever held the signal before, per signal, so the game's existing crash report and its other signal handlers are unchanged.

  • A sigaction that fails leaves that one signal unguarded; the log now says which one, instead of the guard looking installed.

  • No behaviour change without a fault: the call is made where it was made.

  • gpu/vk/vk_draw.cpp (precompile_run): the compute worker called vkCreateComputePipelines itself, so the guard never covered the phase that builds the cached compute pipelines. It now goes through guarded_compute. This is the call Guard cached compute precompilation against driver faults #758 changes for Windows; on Linux it is what makes that change reach the driver at all.

Testing

tests/test_linux_driver_fault_guard.py (game-free, registered in tests/run_all.py, skipped on Windows) compiles the actual guarded calls from vk_draw.cpp against a fake Vulkan that faults on demand:

case expected
both calls compile VK_SUCCESS
compute call faults returns -77, no pipeline published, the next compile reaches the driver
graphics call faults returns VK_ERROR_UNKNOWN, no pipeline published
a fault raised as SIGILL or SIGFPE returns -77, no pipeline published
fault outside the window the process still dies
fault outside the window after a guarded call the handler that held the signal before the guard runs
the same test on unpatched vk_draw.cpp the fault cases die
  • python -I -B tests/run_all.py --jobs 1 linux_driver_fault_guard passes with clang++ and g++.
  • On an RX 6800 XT (RADV NAVI21, Mesa 26.2.6, Fedora 44) with the v2.0.5 game and this branch's hookshot_vk.so: warm-up completes, 10,083 compute pipelines built, then 59-60 fps steady for 15 minutes with no crash. Without the fix the same folder crashes in the first minute at libvulkan_radeon.so+0x14a7d2.

Not covered

  • A fault leaves the driver's own state as it was mid-call. The pipeline being created is abandoned and its memory is not reclaimed; a driver that faults while holding a lock would hang rather than fail. This is the same trade the Windows branch already makes.
  • Signals outside those four (SIGABRT, SIGTRAP and the like) are still handed to the game's own handler, so a driver that ends the process that way is not covered.

@tickernelz

Copy link
Copy Markdown
Contributor Author

Real-GPU evidence for the second and fourth commits, on an RX 6800 XT (RADV NAVI21, Mesa 26.2.6, Fedora 44, kernel 7.2.15) with the v2.0.5 game and this branch's hookshot_vk.so from the Linux AppImage build of ff95e92.

The hookshot_programs.bin in that folder was built by unpatched main, so it holds the 216 depth-compare compute variants that make RADV fault. That is the point: the cache that kills the old build is the workload.

vk: the driver faulted compiling a compute pipeline: that one is left out   (x216)
vk: precompile: 10107 programs, 0 graphics and 10107 compute pipelines in 9274 ms
vk: 24960 presents, 58.6 fps; ... GPU-bound wait 0.0 ms
  • 216 faults caught, 216 = exactly the number of invalid depth-compare modules spirv-val rejects in that cache.
  • Zero CRASH lines over seven minutes of frames at 59-60 fps.
  • The same folder, same procedure and the unpatched v2.0.5 hookshot_vk.so: dies in under a minute at libvulkan_radeon.so+0x14a7d2.

Two things this pinned down for me:

  1. With only the signal guard (first commit) the warm-up survives the 216 faults but the start still dies, because precompile_run's compute worker called vkCreateComputePipelines itself — the bypass Guard cached compute precompilation against driver faults #758 fixes for Windows. The fourth commit sends it through guarded_compute; the fault lines above sit immediately before the precompile: line, so that is the call being caught.
  2. Your __try used to __except (1) on Windows returns VK_ERROR_UNKNOWN, which the caller in precompile_run turns into a null pipeline; the same happens here through -77.

Caveat: the run renders offscreen (no display in the session I ran it from), so I can confirm warm-up, precompile and frame rate, not the window.

@tickernelz

tickernelz commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor Author

Re-verified at ca9dc50, which folds in the three notes from the internal review: a test of the previous-handler chain, SIGILL/SIGFPE, and a log line when sigaction itself fails.

Build: hookshot_vk.so from this fork's Linux AppImage (run 42569857433, sha256 b4e1f636d7e4…), run in a reflinked copy of a v2.0.5 folder whose hookshot_programs.bin was built by unpatched v2.0.5 (46,502 program keys, the 216 invalid compute variants among them). The original folder was read only.

same folder, same cache result
unpatched hookshot_vk.so (fd9b537e…) rc=139, 17 × CRASH: exception 0xc0000005 (write 0x40) at libvulkan_radeon.so+0x14a7d2
ca9dc50 prepared 38809 shader programs (46502 new, 165 failed, 9887 stage libraries; 10107 compute variants …) in 69.1 s; 432 × vk: the driver faulted compiling a compute pipeline: that one is left out across the warm-up and the precompile pass; precompile: 10107 programs, 0 graphics and 10107 compute pipelines in 1017 ms; then 15 minutes at 60.0 fps, 48,360 presents, GPU-bound wait 0.0 ms; 0 CRASH

RADV NAVI21, Mesa 26.2.6, Fedora 44, kernel 7.2.15. The run is offscreen 1920×1080 because this shell has no display: a virtual X server here has no DRI3, and Mesa refuses Vulkan presentation without it (No DRI3 support detected - required for presentation), so it says nothing about the window. The crash it covers is the one in issues 915 and 920: preparation and precompile.

Tests: python -I -B tests/run_all.py --jobs 1 linux_driver_fault_guard passes with clang++ and g++. The new cases fail on the older code: with the two-signal guard the SIGILL case ends in exit -4, and a handler that longjmps without checking the armed flag fails the "fault outside the window" case, while unpatched vk_draw.cpp still dies on the fault cases.

CI at ca9dc50 (fork, workflows dispatched): game-free source regressions 42569854735 — 109 passed, 0 failed, 4 skipped; Linux AppImage 42569857433; Windows builder 42569859980.

Windows wraps vkCreateComputePipelines and vkCreateGraphicsPipelines in
__try so a driver that faults on a shader costs one pipeline, not the
game. Linux had no equivalent: the call went to the driver as it was, and
an access violation inside RADV's compiler ended the process during the
first start's warm-up (issues 915 and 920, and the RX 6800 XT report).

Arm a thread-local window around the driver call and catch SIGSEGV and
SIGBUS inside it, reporting the call's failure with the same values the
Windows branch returns. A fault outside the window is passed to whoever
held the signal, so the game's own crash report and its slow-path
handlers are unchanged.

tests/test_linux_driver_fault_guard.py compiles the actual guarded calls
from vk_draw.cpp against a fake Vulkan that faults on demand: a fault in
the compute or graphics call returns -77 or VK_ERROR_UNKNOWN and the next
compile still reaches the driver, a fault outside the window still ends
the process, and the unfixed source dies on the same fault.
A silent missing pipeline is harder to place in a report than one line naming which kind went.
The guarded calls now log the pipeline they drop, so the compiled harness needs that symbol.
The Windows branch wraps the pipeline calls in __try and catches every
violation. The signal guard took only SIGSEGV and SIGBUS, so a driver
that faults with an illegal instruction or an arithmetic trap still ended
the game. The host's own handler (host/posix/win32_posix.c) already
translates all four, so guard the same four and keep the previous handler
per signal.

A sigaction that fails used to leave the process unguarded for that
signal with nothing in the log.
A fault outside the window has to reach whatever handler held the signal
before the guard. A guarded compile now runs first, so that handler is
the guard's predecessor rather than the default, and the case fails
unless it runs. The fake driver can also raise SIGILL and SIGFPE, so the
wider signal set is covered instead of assumed.
@tickernelz
tickernelz force-pushed the zha5347/linux-driver-fault-guard branch from ca9dc50 to bad04d0 Compare November 9, 2026 07:01
@tickernelz

tickernelz commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor Author

Rebased onto upstream main (91873ba, PR #983) because the branch had gone CONFLICTING; the pull request is MERGEABLE again.

  • Only conflict was tests/run_all.py: two entries added to the same --cxx table, so both are kept (test_seed_cs_pointers from main, test_linux_driver_fault_guard from this branch).
  • ff95e92 (precompile compute worker through the guard) was dropped by the rebase: that change is already on main via Guard cached compute precompilation against driver faults #758, and precompile_run still reaches the driver through guarded_compute.
  • Old-to-new SHAs, same content, same messages: 2079b41→f9a2fcf, 0560ecb→e36670c, 221683b→a2f19b6, 5a1cb80→4f98383, ca9dc50→bad04d0. The evidence in the two comments above was gathered on the old SHAs.
  • python -I -B tests/run_all.py --jobs 1 linux_driver_fault_guard: 1 passed.
  • All three workflows at bad04d0 are green on the fork: Linux AppImage, Windows builder, Game-free source regressions.

The three runs on this pull request itself are still action_required — GitHub is waiting for a maintainer to approve workflows from this fork. Clicking Approve and run workflows here will run the same three jobs in this repository.

@tickernelz

tickernelz commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor Author

Superseded by #834, which is in v2.0.6 (fa43939): !flags.is_depth && is_texture_pixel_format_integer(cbuf) in texture_pass.cpp fixes the invalid depth-compare SPIR-V at the root, so there is no driver fault left to catch for this crash. Verified in the released tag, not only on main.

Closing to keep the queue clear rather than carrying a band-aid on top of a fixed root. The guard still has a residual use (any RADV/AMDVLK fault inside a pipeline compile, e.g. issue 26 on Windows), so say the word and I will rebase and reopen it as a standalone hardening PR instead of a fix for 915/920.

@tickernelz tickernelz closed this Nov 9, 2026
NotRexed pushed a commit that referenced this pull request Nov 9, 2026
@tickernelz
tickernelz deleted the zha5347/linux-driver-fault-guard branch November 9, 2026 09:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant