mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
Let the first failure report end the run whole when another thread crashes or hits a fatal error beside it.
The platform layer calls the crash handler on every thread that faults, without waiting for the others (win32.c crash_veh, crash_filter), and main.c crash_handler ended any second entry with plat_exit(71) at once. So a second fault during the first report cut it after its first lines, together with its wait for a save Commit (fs_save_crash_wait) and RivaTuner's restore; the audit's probe showed it in 3 of 3 runs. fatal_end had no guard at all and none shared with the crash report: a crash during a FATAL's end wrote a whole crash report whose "FATAL: access violation" became the log's last FATAL line (the one the launcher shows) and replaced code 70 with 71, and a FATAL during a crash report cut it with code 70. dispatch.c rt_ending_enter is the one guard for both ends: the first caller reports and ends the process; the same thread again (a failure inside its own report) ends at once with its own code (crash_handler keeps plat_exit(71), fatal_end plat_terminate(code)); another thread waits in 10 ms sleeps, logging nothing since it may hold the log queue's lock, until the process ends, and after 30 s ends it with 71 should the first report hang. fatal_end calls it first; crash_handler calls it in place of its own busy flag. Reproduced before the change by tests/fatal_exit_check.py's new cases, which failed on the old guards: two faults at once (report lines 2 and 3 missing), a fault during rt_fatal (a crash report after the FATAL) and rt_fatal during a crash report (exit 70, the report cut). They pass now, with the earlier cases and the static check that crash_handler starts with the shared guard; the compile checks pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 <stdio.h>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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:']),
|
||||
]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user