diff --git a/docs/building.md b/docs/building.md index c6c0e9d8..1f138b3a 100644 --- a/docs/building.md +++ b/docs/building.md @@ -409,6 +409,13 @@ loss at the same moment end the process with exit code 72, one device report and device and opens no window; the device is not really lost, so what a driver answers after a loss is not covered. `device_lost_at = ` in `nx2navi.ini` ends a real run the same way at its n-th hand-over to the GPU queue. +A change to how a fault ends the process (`runtime/main.c` `crash_handler`, `mode_done`) passes +`python tests\exit_fault_check.py `, which runs two crash probes: a fault inside a library while a mode runs +is a crash (one FATAL line, exit code 71), and the same fault after the mode has returned, while the process is ending, +leaves the mode's own exit code and one `exit:` line. The second is what a library another program put into the +process does when it faults in its detach: an injector's Vulkan layer did so at the end of every shader preparation on +a player's PC, and the launcher took each finished preparation for a failed one. No game files, no GPU. + A change to the memory gate of the background pipeline builds (`runtime/vk_build_gate.h`, `runtime/vk_pipeline.c` `gate_enter`) passes `python tests\build_gate_check.py []`, which builds the header alone and checks what it admits and refuses and that builds under way count through their reservations. With the executable it also runs diff --git a/runtime/main.c b/runtime/main.c index a6e67e07..18901c62 100644 --- a/runtime/main.c +++ b/runtime/main.c @@ -264,20 +264,39 @@ static uint64_t lifted_function(uintptr_t rip) { static int readable(uint64_t a, uint64_t n) { return plat_mem_readable(a, n); } -// A module another program put into the game's process: the Vulkan layer or the hook of a recorder, an overlay or a -// capture tool, by the words such files carry in their names. A crash inside one is that program's: one report was an -// access violation in a recorder's layer the moment the present mode changed and the swapchain was made again. +// A module another program put into the game's process: the Vulkan layer or the hook of a recorder, an overlay, a +// capture tool or a post-processing injector, by the words such files carry in their names. A crash inside one is that +// program's: one report was an access violation in a recorder's layer the moment the present mode changed and the +// swapchain was made again, another an injector's layer reading a null pointer at the end of every shader +// preparation. static int foreign_hook(const char *module) { char low[160]; size_t n = 0; for (; module && module[n] && n < sizeof low - 1; n++) low[n] = (char)tolower((unsigned char)module[n]); low[n] = 0; - return strstr(low, "vk_layer") || strstr(low, "hook") || strstr(low, "overlay"); + return strstr(low, "vk_layer") || strstr(low, "hook") || strstr(low, "overlay") || strstr(low, "reshade"); } +// Set when main_run has returned: the mode's work is done and saved, and all that still runs is the C runtime's exit +// and, inside ExitProcess, the detach call of every library in the process, the graphics driver's and those other +// programs put there. A fault in one of them is not a failure of the run. On one PC an injector's Vulkan layer faulted +// in its detach at the end of every `--warm-shaders` and `--prepare-shaders` (four runs over three versions): each +// wrote a FATAL after "variants built into the driver cache" and ended with code 71, so the launcher took a finished +// preparation for one that failed. crash_handler ends such a process with the code its mode returned. A fault in this +// executable's own code on the way out (an atexit handler) is still reported. +static volatile int32_t mode_done; +static int mode_rc; + static void crash_handler(const PlatCrash *x) { static volatile int32_t busy; if (plat_xchg32(&busy, 1)) plat_exit(71); + if (__atomic_load_n(&mode_done, __ATOMIC_ACQUIRE) && x->module) { + rt_log("exit: %s (%#x) in %s while the process was ending, after its work was done and saved%s: the run ends " + "with its own code %d", x->what, x->code, *x->module ? x->module : "code outside every module", + foreign_hook(x->module) ? " (that library is not part of the game: another program put it there)" : "", + mode_rc); + plat_exit(mode_rc); + } rt_log_sync(); // the lines before the crash, then the report, each written as it is made char d[160]; uintptr_t rip = x->rip; @@ -536,11 +555,23 @@ static int probe_crash(int null) { return 1; } +// --probe-crash-exit: the fault of --probe-crash-call, made by an atexit handler after the mode has returned, the way +// a library another program put into the process faults in its detach. The process must end with the mode's own code +// (PROBE_EXIT_CODE) and an "exit:" line, with no FATAL (mode_done, crash_handler). tests/exit_fault_check.py runs it. +#define PROBE_EXIT_CODE 3 +static void probe_exit_fault(void) { printf("%zu\n", probe_strlen((const char *)(uintptr_t)8)); } + +static int probe_crash_exit(void) { + atexit(probe_exit_fault); + printf("probe-crash-exit: returning %d; a library then faults while the process ends\n", PROBE_EXIT_CODE); + return PROBE_EXIT_CODE; +} + static int usage(const char *argv0) { fprintf(stderr, "usage: %s [--exefs DIR] [--romfs DIR] [--profile DIR] [--audio-device VALUE] [--monitor VALUE] [--list-audio-devices | --list-monitors | --selftest | --probe-loader | " "--probe-cfg | --probe-dpi | --probe-mods | --probe-kernel | --probe-ipc | --probe-services | --probe-fs | --probe-gfx-abi | " "--probe-nvn | --probe-nvn-tex | --probe-window | --probe-shader | --probe-vkmem | " - "--probe-vkexec | --probe-meshfb | --probe-astc | --probe-zstd | --probe-input | --probe-movie | --probe-crash-call | --probe-crash-null | --probe-discord | --probe-coop | " + "--probe-vkexec | --probe-meshfb | --probe-astc | --probe-zstd | --probe-input | --probe-movie | --probe-crash-call | --probe-crash-null | --probe-crash-exit | --probe-discord | --probe-coop | " "--prepare-shaders | --warm-shaders | --prepare-pipelines | --probe-prewarm | --probe-buildgate | --probe-ffx-cache | --probe-memneed | --probe-devlost | --boot |--seconds S [--shot PNG]]\n", argv0); return 2; } @@ -577,7 +608,16 @@ static void portable_paths(RtConfig *c) { c->profile = portable_path(exe, "NX2NAVI_PROFILE_DIR", "profile", NULL, pp_profile, sizeof pp_profile, c->profile); } +static int main_run(int argc, char **argv); + +// A game run never comes back here: it ends in plat_exit (boot.c, gfx_present.c). Every other mode returns its code. int main(int argc, char **argv) { + mode_rc = main_run(argc, argv); + __atomic_store_n(&mode_done, 1, __ATOMIC_RELEASE); // from here on only the exit runs (mode_done, above) + return mode_rc; +} + +static int main_run(int argc, char **argv) { mem_init(); // first: claim the guest address space before anything else can land there; reads nx2navi.ini // The build that wrote the log, at its top: an attached log from another install or version shows at a glance. rt_log("version: NX2navi %s, game %d.%d.%d", rt_port_version, rt_game_version / 100, rt_game_version / 10 % 10, @@ -671,6 +711,7 @@ int main(int argc, char **argv) { if (!strcmp(mode, "--probe-loader")) return probe_loader(); if (!strcmp(mode, "--probe-crash-call")) return probe_crash(0); if (!strcmp(mode, "--probe-crash-null")) return probe_crash(1); + if (!strcmp(mode, "--probe-crash-exit")) return probe_crash_exit(); if (!strcmp(mode, "--probe-cfg")) return probe_cfg(); #ifdef _WIN32 if (!strcmp(mode, "--probe-dpi")) return probe_dpi(); // the Win32 DPI awareness of nx2navi.manifest diff --git a/tests/exit_fault_check.py b/tests/exit_fault_check.py new file mode 100644 index 00000000..2d0093a0 --- /dev/null +++ b/tests/exit_fault_check.py @@ -0,0 +1,63 @@ +"""Run the game executable's crash probes and check what a fault does to the way the process ends (runtime/main.c +crash_handler, mode_done). + + - --probe-crash-call: a fault inside a library while the mode still runs is a crash: a FATAL line that names the + library, exit code 71. + - --probe-crash-exit: the same fault after the mode has returned, while the process is ending (an atexit handler here; + on a player's PC the detach of a library another program put into the process). The work is done by then, so the + process ends with the mode's own code, 3, and one "exit:" line, with no FATAL. + +No game files, no GPU, no window. + +Usage: exit_fault_check.py PATH_TO_GAME_EXE +""" +import subprocess +import sys + +CRASH_EXIT = 71 +PROBE_EXIT = 3 + + +def run(exe, mode): + p = subprocess.run([exe, mode], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, errors='replace', + timeout=120) + return p.returncode, p.stdout.splitlines() + + +def main(): + if len(sys.argv) != 2: + print(__doc__) + return 2 + fails = [] + + def check(ok, what): + if not ok: + fails.append(what) + + code, lines = run(sys.argv[1], '--probe-crash-call') + fatal = [ln for ln in lines if ln.startswith('FATAL: ')] + check(code == CRASH_EXIT, '--probe-crash-call: exit code %d, expected %d' % (code, CRASH_EXIT)) + check(len(fatal) == 1 and 'access violation' in fatal[0], '--probe-crash-call: %d FATAL lines, expected one: %s' + % (len(fatal), fatal)) + check(not any('exit: ' in ln and 'while the process was ending' in ln for ln in lines), + '--probe-crash-call: a fault inside the mode was taken for one at the exit') + + code, lines = run(sys.argv[1], '--probe-crash-exit') + fatal = [ln for ln in lines if ln.startswith('FATAL: ')] + said = [ln for ln in lines if 'exit: ' in ln and 'while the process was ending' in ln] + check(any(ln.startswith('probe-crash-exit: returning') for ln in lines), '--probe-crash-exit: the mode did not run') + check(code == PROBE_EXIT, '--probe-crash-exit: exit code %d, expected the mode\'s own %d' % (code, PROBE_EXIT)) + check(not fatal, '--probe-crash-exit: a FATAL line for a fault after the work was done: %s' % fatal) + check(len(said) == 1 and 'with its own code %d' % PROBE_EXIT in said[0], + '--probe-crash-exit: %d "exit:" lines, expected one that names the code: %s' % (len(said), said)) + + for f in fails: + print('exit_fault_check: FAIL:', f) + if not fails: + print('exit_fault_check: passed (a fault in the mode: FATAL and %d; a fault at the exit: the mode\'s code %d)' + % (CRASH_EXIT, PROBE_EXIT)) + return 1 if fails else 0 + + +if __name__ == '__main__': + sys.exit(main())