Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 67 additions & 3 deletions gpu/vk/vk_draw.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
#include <map>
#include <vector>
#include <thread>
#include <setjmp.h>
#include <signal.h>

#include "shader_recompiler/backend/spirv/emit_spirv.h"

Expand Down Expand Up @@ -737,12 +739,74 @@ __declspec(noinline) VkResult guarded_compute(VkPipelineCache cache, const VkCom
}
}
#else
// (linux: no structured exceptions; the driver is called as it is)
// (linux: no structured exceptions, so the fault arrives as a signal on the compiling thread: an armed window covers the driver
// call alone, and a fault in it is reported as the call's failure, as the Windows guard above reports an access violation. A fault
// outside it goes to whoever had the signal before, so the game's own crash report is unchanged)
static thread_local sigjmp_buf fault_jmp;
static thread_local volatile sig_atomic_t fault_armed;
static const int fault_signals[] = {SIGSEGV, SIGBUS, SIGILL, SIGFPE};
static constexpr int fault_signal_count = (int)(sizeof fault_signals / sizeof fault_signals[0]);
static struct sigaction fault_previous[fault_signal_count];

static int fault_slot(int sig) {
for (int i = 0; i < fault_signal_count; i++) {
if (fault_signals[i] == sig) return i;
}
return 0;
}

static void on_driver_fault(int sig, siginfo_t* si, void* ctx) {
if (fault_armed) {
fault_armed = 0;
siglongjmp(fault_jmp, 1);
}
const struct sigaction* p = &fault_previous[fault_slot(sig)];
if ((p->sa_flags & SA_SIGINFO) && p->sa_sigaction) {
p->sa_sigaction(sig, si, ctx);
} else if (p->sa_handler && p->sa_handler != SIG_IGN && p->sa_handler != SIG_DFL) {
p->sa_handler(sig);
} else {
signal(sig, SIG_DFL);
}
}

static void install_driver_fault_guard() {
static std::once_flag once;
std::call_once(once, [] {
for (int i = 0; i < fault_signal_count; i++) {
struct sigaction sa;
memset(&sa, 0, sizeof sa);
sa.sa_sigaction = on_driver_fault;
sa.sa_flags = SA_SIGINFO;
sigemptyset(&sa.sa_mask);
if (sigaction(fault_signals[i], &sa, &fault_previous[i]) != 0) {
logf("vk: the driver fault guard could not take signal %d: a fault in it ends the game", fault_signals[i]);
}
}
});
}

VkResult guarded_graphics(VkPipelineCache cache, const VkGraphicsPipelineCreateInfo* ci, VkPipeline* pl) {
return vkCreateGraphicsPipelines(dev.device, cache, 1, ci, nullptr, pl);
install_driver_fault_guard();
if (sigsetjmp(fault_jmp, 1) != 0) {
logf("vk: the driver faulted compiling a graphics pipeline: that one is left out");
return VK_ERROR_UNKNOWN;
}
fault_armed = 1;
const VkResult r = vkCreateGraphicsPipelines(dev.device, cache, 1, ci, nullptr, pl);
fault_armed = 0;
return r;
}
VkResult guarded_compute(VkPipelineCache cache, const VkComputePipelineCreateInfo* ci, VkPipeline* pl) {
return vkCreateComputePipelines(dev.device, cache, 1, ci, nullptr, pl);
install_driver_fault_guard();
if (sigsetjmp(fault_jmp, 1) != 0) {
logf("vk: the driver faulted compiling a compute pipeline: that one is left out");
return (VkResult)-77;
}
fault_armed = 1;
const VkResult r = vkCreateComputePipelines(dev.device, cache, 1, ci, nullptr, pl);
fault_armed = 0;
return r;
}
#endif
VkPipeline build_graphics(const Program& p, const NvnState& s, const Targets& at, VkPrimitiveTopology topo, int part = -1);
Expand Down
2 changes: 2 additions & 0 deletions tests/run_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"tests/test_cpp_preflight.py": ["--cxx", "g++"],
"tests/test_invalidate_vcore.py": ["--cxx", "g++"],
"tests/test_seed_cs_pointers.py": ["--cxx", "g++"],
"tests/test_linux_driver_fault_guard.py": ["--cxx", "{cxx}"],
"tests/test_lower_atomg_cas.py": ["--cxx", "g++"],
"nx2/test_cbuf_table_extent.py": ["--cxx", "g++"],
"nx2/test_dump_ssbo_read.py": ["--cxx", "g++"],
Expand Down Expand Up @@ -71,6 +72,7 @@
# (Linux only)
"tests/test_hookshot_launch.py": ({"windows"}, "the Linux launcher, a POSIX shell script"),
"tests/test_linux_update.py": ({"windows"}, "the Linux builder's update and folder launcher, POSIX files and shell scripts"),
"tests/test_linux_driver_fault_guard.py": ({"windows"}, "POSIX signals: the Linux guard"),
}
# (spirv-val: SPIRV-Tools comes with the Linux runner's packages, not on Windows)
for _name in ("nx2/test_depth_sample_types.py", "nx2/test_default_point_size.py", "nx2/test_draw_parameters.py", "nx2/test_coop_spirv.py",
Expand Down
177 changes: 177 additions & 0 deletions tests/test_linux_driver_fault_guard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# SPDX-License-Identifier: GPL-3.0-or-later
"""Game-free check of the Linux driver-fault guard: the actual guarded pipeline calls from vk_draw.cpp, compiled with a fake
Vulkan whose compute and graphics creation fault on demand. A fault inside the guarded call must come back as the call's
failure and leave the process able to compile again; a fault outside it must still kill the process.

python -I -B tests/test_linux_driver_fault_guard.py [--cxx clang++] [--expect-fault]
"""
import argparse
import os
from pathlib import Path
import shutil
import signal
import subprocess
import sys
import tempfile

ROOT = Path(__file__).resolve().parents[1]
START = "return (VkResult)-77; // (a fault in the driver, told apart from what it returns)\n }\n}\n#else\n"
END = "\n#endif\nVkPipeline build_graphics"

HEAD = r"""
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <mutex>
#include <setjmp.h>
#include <signal.h>
#include <unistd.h>
using VkResult = int;
using VkPipeline = uintptr_t;
using VkPipelineCache = uintptr_t;
constexpr VkResult VK_SUCCESS = 0;
constexpr VkResult VK_ERROR_UNKNOWN = -13;
constexpr uintptr_t VK_NULL_HANDLE = 0;
struct VkComputePipelineCreateInfo { int sType = 0; };
struct VkGraphicsPipelineCreateInfo { int sType = 0; };
struct { uintptr_t device = 0x1234; } dev;
static int mode; /* (0: compile, 1 to 4: the driver faults) */
static int calls;
static constexpr VkResult DRIVER_FAULT = -77;
static int logged;
void logf(const char *, ...) { ++logged; }
static void fault_now() { *(volatile int *)0x40 = 1; }
static void previous_handler(int, siginfo_t *, void *) {
static const char line[] = "previous handler ran\n";
(void)!::write(1, line, sizeof line - 1);
_exit(42);
}
static void install_previous() {
struct sigaction sa;
memset(&sa, 0, sizeof sa);
sa.sa_sigaction = previous_handler;
sa.sa_flags = SA_SIGINFO;
sigemptyset(&sa.sa_mask);
sigaction(SIGSEGV, &sa, nullptr);
}
VkResult vkCreateComputePipelines(uintptr_t device, VkPipelineCache cache, unsigned count,
const VkComputePipelineCreateInfo *ci, const void *allocator, VkPipeline *out) {
if (device != dev.device || cache != 0 || count != 1 || !ci || allocator || !out) return -1;
++calls;
if (mode == 1) fault_now();
if (mode == 3) raise(SIGILL);
if (mode == 4) raise(SIGFPE);
*out = 0xcafe;
return VK_SUCCESS;
}
VkResult vkCreateGraphicsPipelines(uintptr_t device, VkPipelineCache cache, unsigned count,
const VkGraphicsPipelineCreateInfo *ci, const void *allocator, VkPipeline *out) {
if (device != dev.device || cache != 0 || count != 1 || !ci || allocator || !out) return -1;
++calls;
if (mode == 2) fault_now();
*out = 0xbeef;
return VK_SUCCESS;
}
"""

MAIN = r"""
int main(int argc, char **argv) {
const char *what = argc > 1 ? argv[1] : "ok";
VkComputePipelineCreateInfo ci;
VkGraphicsPipelineCreateInfo gci;
VkPipeline pl = 0, gpl = 0;
if (!std::strcmp(what, "fault-outside")) { fault_now(); std::printf("fail: the fault returned\n"); return 8; }
if (!std::strcmp(what, "fault-outside-after-guard")) {
install_previous();
if (guarded_compute(0, &ci, &pl) != VK_SUCCESS || pl != 0xcafe) { std::printf("fail: the compile before the fault\n"); return 10; }
fault_now();
std::printf("fail: the fault returned\n");
return 8;
}
if (!std::strcmp(what, "ok")) {
if (guarded_compute(0, &ci, &pl) != VK_SUCCESS || pl != 0xcafe) { std::printf("fail: compute\n"); return 3; }
if (guarded_graphics(0, &gci, &gpl) != VK_SUCCESS || gpl != 0xbeef) { std::printf("fail: graphics\n"); return 4; }
std::printf("ok: both compiled\n");
return 0;
}
if (!std::strcmp(what, "fault")) {
mode = 1;
if (guarded_compute(0, &ci, &pl) != DRIVER_FAULT) { std::printf("fail: the fault was not reported\n"); return 5; }
if (pl != VK_NULL_HANDLE) { std::printf("fail: a pipeline was published\n"); return 6; }
} else if (!std::strcmp(what, "fault-graphics")) {
mode = 2;
if (guarded_graphics(0, &gci, &gpl) != VK_ERROR_UNKNOWN) { std::printf("fail: the fault was not reported\n"); return 5; }
if (gpl != VK_NULL_HANDLE) { std::printf("fail: a pipeline was published\n"); return 6; }
} else if (!std::strcmp(what, "fault-signals")) {
for (mode = 3; mode <= 4; mode++) {
pl = 0;
if (guarded_compute(0, &ci, &pl) != DRIVER_FAULT || pl != VK_NULL_HANDLE) {
std::printf("fail: the signal that mode %d raised was not reported\n", mode);
return 5;
}
}
} else {
std::printf("fail: unknown case %s\n", what);
return 9;
}
mode = 0;
calls = 0;
pl = gpl = 0;
if (guarded_compute(0, &ci, &pl) != VK_SUCCESS || pl != 0xcafe || calls != 1) {
std::printf("fail: the next compile after the fault did not reach the driver\n");
return 7;
}
std::printf("ok: the fault was reported and the next compile made\n");
return 0;
}
"""


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--cxx", default="clang++")
parser.add_argument("--source", type=Path, default=ROOT / "gpu/vk/vk_draw.cpp")
parser.add_argument("--expect-fault", action="store_true",
help="unfixed source: a fault in a guarded call must end the process")
args = parser.parse_args()
if os.name == "nt":
raise SystemExit("POSIX signals required")
compiler = shutil.which(args.cxx)
if not compiler:
raise SystemExit("C++ compiler not found: " + args.cxx)
source = args.source.read_text(encoding="utf-8")
start, end = source.index(START) + len(START), source.index(END)
guard = source[start:end]
failures = []
with tempfile.TemporaryDirectory(prefix="hookshot-fault-guard-") as tmp:
cpp = Path(tmp) / "guard.cpp"
cpp.write_text(HEAD + guard + MAIN, encoding="utf-8")
binary = Path(tmp) / "guard"
build = subprocess.run([compiler, "-std=c++20", "-O1", "-Wall", "-Wextra", "-pthread", "-o", str(binary), str(cpp)],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
if build.returncode:
raise SystemExit("the guard did not compile:\n" + build.stdout)
for case in ("ok", "fault", "fault-graphics", "fault-signals", "fault-outside", "fault-outside-after-guard"):
run = subprocess.run([str(binary), case], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
killed = run.returncode in (-signal.SIGSEGV, -signal.SIGBUS, -signal.SIGILL, -signal.SIGFPE)
guarded_case = case in ("fault", "fault-graphics", "fault-signals")
if case == "fault-outside-after-guard":
if run.returncode != 42 or "previous handler ran" not in run.stdout:
failures.append(case + ": the fault outside the window did not reach the handler it found: "
+ run.stdout.strip() + " (exit " + str(run.returncode) + ")")
elif case == "fault-outside" or (args.expect_fault and guarded_case):
if not killed:
failures.append(case + ": the fault did not end the process: " + run.stdout.strip())
elif run.returncode != 0:
failures.append(case + ": " + (run.stdout.strip() or "exit " + str(run.returncode)))
else:
print(case + ": " + run.stdout.strip())
if failures:
print("\n".join(failures), file=sys.stderr)
raise SystemExit(1)
print("the guard reports a driver fault as the call's failure" if not args.expect_fault
else "the unfixed source dies on a driver fault")


if __name__ == "__main__":
main()