diff --git a/runtime/dispatch.c b/runtime/dispatch.c index 7491b98c..dd925278 100644 --- a/runtime/dispatch.c +++ b/runtime/dispatch.c @@ -59,9 +59,27 @@ void rt_fatal_notes(void) { __attribute__((weak)) void fs_save_crash_wait(void) {} // fs_host.c; builds without it (the unit checks) have no saves +// One end for a failed run. The platform layer calls the crash handler on every thread that faults, without +// waiting for the others (platform/win32.c crash_veh, crash_filter), and a fatal error can come beside a crash from +// the same bad state (an allocation that failed here, a null pointer read there). The later end cut the first report +// where it was, together with its wait for a save Commit and RivaTuner's restore, and its own FATAL line and exit +// code replaced the first (the launcher shows the last FATAL). Another thread now waits until the first end is done; +// it logs nothing, since it may hold the log queue's lock, which the first report only tries. A first report that +// hangs still ends: after 30 s the waiting thread ends the process with the crash report's code. +static volatile uint32_t ending_tid; // the thread that reports, 0 before one does + +int rt_ending_enter(void) { + uint32_t me = plat_thread_id(), was = 0; + if (__atomic_compare_exchange_n(&ending_tid, &was, me, 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) return 1; + if (was == me) return 0; + for (int i = 0; i < 3000; i++) plat_sleep_ms(10); + plat_terminate(71); +} + // The fatal line, the report's notes and what has to be flushed, then the exit with `code`. static void fatal_end(int code, const char *fmt, va_list ap) __attribute__((noreturn)); static void fatal_end(int code, const char *fmt, va_list ap) { + if (!rt_ending_enter()) plat_terminate(code); // a fatal error inside this thread's own report: no second one rt_log_sync(); // main.c: the lines queued for the log thread, then these, written here rt_log_raw("FATAL: ", 7); rt_vlogf(fmt, ap); diff --git a/runtime/dispatch.h b/runtime/dispatch.h index 7950e618..7a707505 100644 --- a/runtime/dispatch.h +++ b/runtime/dispatch.h @@ -62,3 +62,7 @@ void rt_fatal_code(int code, const char *fmt, ...) __attribute__((noreturn, form // needs none. At most four; adding one twice keeps one. rt_fatal_notes runs them, once per process. void rt_fatal_note_add(void (*fn)(void)); void rt_fatal_notes(void); +// The one end of a failed run: the fatal error (rt_fatal) and the crash report (main.c crash_handler) call it first. 1 +// for the first caller, which reports and ends the process; 0 for that thread again (a failure inside its own report), +// which ends at once with its code; any other thread waits, logging nothing, until the process ends (30 s at most). +int rt_ending_enter(void); diff --git a/runtime/main.c b/runtime/main.c index 6db3fc0d..6b953d89 100644 --- a/runtime/main.c +++ b/runtime/main.c @@ -315,8 +315,9 @@ 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); + // shared with the fatal error's end (dispatch.c rt_ending_enter): another thread's fault or fatal error waits there + // while this report is written; a fault inside this thread's own report ends the process at once + if (!rt_ending_enter()) 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", diff --git a/tests/fatal_exit_check.c b/tests/fatal_exit_check.c index a6180aae..ea9c0718 100644 --- a/tests/fatal_exit_check.c +++ b/tests/fatal_exit_check.c @@ -3,7 +3,13 @@ // printed and its exit code. The exe has a TLS callback that prints a line when the process's libraries get their // detach call, which ExitProcess (and _Exit, which is ExitProcess in the C runtime) makes and TerminateProcess does not. // -// fatal_exit_check fatal rt_fatal: exit code 70, the FATAL line, no detach line +// fatal_exit_check fatal rt_fatal: exit code 70, the FATAL line, no detach line +// fatal_exit_check crash_crash two threads fault at once: one whole crash report, exit code 71 +// fatal_exit_check fatal_crash a fault on another thread while rt_fatal reports: its FATAL line alone, exit code 70 +// fatal_exit_check crash_fatal rt_fatal on another thread while a crash is reported: the whole report, exit code 71 +// +// The crash handler here is a stand-in for main.c crash_handler with its first line (the static check in +// tests/fatal_exit_check.py holds main.c to it): it writes three report lines 100 ms apart and ends with plat_exit(71). #include "dispatch.c" #include @@ -52,11 +58,61 @@ void prof_sampler_write(void) {} void rtss_limit_restore(void) {} void kern_checkpoint(void) {} +// ---- the crash report and the threads that fail beside it +static void crash(const PlatCrash *x) { + if (!rt_ending_enter()) plat_exit(71); // main.c crash_handler's first line + char b[160]; + for (int i = 1; i <= 3; i++) { + snprintf(b, sizeof b, "report line %d: %s on thread %lu\n", i, x->what, (unsigned long)GetCurrentThreadId()); + say(b); + Sleep(100); + } + plat_exit(71); +} + +static volatile LONG go; +static DWORD WINAPI fault(void *arg) { + Sleep((DWORD)(uintptr_t)arg); + while (!go) Sleep(0); + *(volatile int *)(uintptr_t)16 = 1; + return 0; +} +static DWORD WINAPI fatal_thread(void *arg) { + Sleep((DWORD)(uintptr_t)arg); + rt_fatal("the test's fatal error %d", 2); +} +static void slow_note(void) { // a fatal note that takes a while, as the card's and the memory settings' may + say("fatal note: start\n"); + Sleep(300); + say("fatal note: end\n"); +} + int main(int argc, char **argv) { setvbuf(stdout, NULL, _IONBF, 0); const char *mode = argc > 1 ? argv[1] : ""; tls_var = 1; + plat_crash_hook(crash); if (!strcmp(mode, "fatal")) rt_fatal("the test's fatal error %d", 1); - printf("usage: fatal_exit_check fatal\n"); + if (!strcmp(mode, "crash_crash")) { + CloseHandle(CreateThread(NULL, 0, fault, (void *)0, 0, NULL)); + CloseHandle(CreateThread(NULL, 0, fault, (void *)0, 0, NULL)); + Sleep(50); + go = 1; + Sleep(10000); + return 3; + } + if (!strcmp(mode, "fatal_crash")) { + rt_fatal_note_add(slow_note); + go = 1; + CloseHandle(CreateThread(NULL, 0, fault, (void *)100, 0, NULL)); + rt_fatal("the test's fatal error %d", 1); + } + if (!strcmp(mode, "crash_fatal")) { + CloseHandle(CreateThread(NULL, 0, fatal_thread, (void *)100, 0, NULL)); + go = 1; + fault(0); + return 3; + } + printf("usage: fatal_exit_check fatal|crash_crash|fatal_crash|crash_fatal\n"); return 2; } diff --git a/tests/fatal_exit_check.py b/tests/fatal_exit_check.py index f7eb9caf..97eee1d0 100644 --- a/tests/fatal_exit_check.py +++ b/tests/fatal_exit_check.py @@ -4,6 +4,10 @@ ExitProcess makes (a driver's detach that faulted there turned the end into a cr checks statically that no runtime file outside the platform layer ends the process with _Exit, and that the fatal error's end and the lost device's (runtime/vk_device.c lost_wait_exit) end with plat_terminate. No game files, no GPU. +Two failures at once end once: two threads that fault, a fault while a fatal error is reported and a fatal error +while a crash is: the first report is written whole, the process ends with its code, and the second adds no line +(runtime/dispatch.c rt_ending_enter, which runtime/main.c crash_handler calls first). + Usage: fatal_exit_check.py """ import os @@ -62,12 +66,25 @@ def static_checks(): bad += 1 print(f'static: no _Exit outside the platform layer, fatal_end and lost_wait_exit end with plat_terminate: ' f'{"FAILED" if bad else "ok"}', flush=True) - return bad + # the crash handler's guard is the one the stand-in in tests/fatal_exit_check.c runs + main_c = (RT / 'main.c').read_text(encoding='utf-8') + body = re.sub(r'//[^\n]*', '', function(main_c, 'static void crash_handler(const PlatCrash *x)')) + first = body[body.index('{') + 1:].strip().split('\n')[0].strip() + ok = first == 'if (!rt_ending_enter()) plat_exit(71);' + print(f'static: main.c crash_handler starts with the shared guard: {"ok" if ok else "FAIL (" + first + ")"}', + flush=True) + return bad + (not ok) CASES = [ # mode, exit code, lines that must be there, lines that must not ('fatal', 70, ['FATAL: the test\'s fatal error 1'], ['detach:']), + # two faults at once: the first report whole, the second thread waits (dispatch.c rt_ending_enter) + ('crash_crash', 71, ['report line 1', 'report line 2', 'report line 3'], ['detach:', 'FATAL:']), + # a fault while rt_fatal reports: its FATAL line and notes whole, no crash report, rt_fatal's code + ('fatal_crash', 70, ['FATAL: the test\'s fatal error 1', 'fatal note: end'], ['report line', 'detach:']), + # rt_fatal on another thread while a crash is reported: the report whole, the crash report's code + ('crash_fatal', 71, ['report line 1', 'report line 2', 'report line 3'], ['FATAL:', 'detach:']), ]