End a hung fatal error's report with its own code when a thread waiting beside it gives up, not with the crash code.

The review of the shared failure guard found that a thread parked in dispatch.c rt_ending_enter ended the process
with 71 after 30 s even when the first report was a FATAL (70, or 72 for a lost device), so a fatal error whose report
hung read as a crash to the launcher.

rt_ending_enter now takes the code its caller ends with: fatal_end passes its own, main.c crash_handler 71. The first
caller stores it next to ending_tid, and a waiting thread that gives up after 30 s ends the process with it. Before
the store the value is 71, as before.

Reproduced before the change with a one-off build of tests/fatal_exit_check.c given a fatal note that never returns
and a fault on another thread: the process ended with 71 after 30.5 s; with this change it ends with 70 after 30.5 s.
That case takes 30 s, so the matrix gets a static check instead: tests/fatal_exit_check.py holds rt_ending_enter to
storing the first caller's code and ending with it, fatal_end to passing its code, and crash_handler to its first
line `if (!rt_ending_enter(71)) plat_exit(71);`. fatal_exit_check and the compile check of every runtime file (a
header changed) pass, with no new -Wall warnings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-13 18:30:49 -05:00
parent e6a6d6a809
commit 66c607414b
5 changed files with 27 additions and 12 deletions

View File

@@ -88,21 +88,26 @@ void rt_rumble_stop_bounded(void) {
// 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.
// hangs still ends: after 30 s the waiting thread ends the process with the first report's own code (a fatal error's
// 70 or 72, the crash report's 71), so the launcher reads the end as the report says it.
static volatile uint32_t ending_tid; // the thread that reports, 0 before one does
static volatile int32_t ending_code = 71; // the code that report ends with, stored right after it took ending_tid
int rt_ending_enter(void) {
int rt_ending_enter(int code) {
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 (__atomic_compare_exchange_n(&ending_tid, &was, me, 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) {
__atomic_store_n(&ending_code, code, __ATOMIC_RELEASE);
return 1;
}
if (was == me) return 0;
for (int i = 0; i < 3000; i++) plat_sleep_ms(10);
plat_terminate(71);
plat_terminate(__atomic_load_n(&ending_code, __ATOMIC_ACQUIRE));
}
// 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
if (!rt_ending_enter(code)) plat_terminate(code); // a fatal error inside this thread's own report: no second one
rt_log_ending(); // the report reaches the log file past its cap (main.c log_sink)
rt_log_sync(); // main.c: the lines queued for the log thread, then these, written here
rt_log_raw("FATAL: ", 7);

View File

@@ -62,9 +62,10 @@ 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);
// The one end of a failed run: the fatal error (rt_fatal) and the crash report (main.c crash_handler) call it first,
// with the code they end the process with. 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, and after 30 s ends it with the first caller's code.
int rt_ending_enter(int code);
// HD rumble stopped before a failed run ends, on a thread of its own that the caller waits for 300 ms at most.
void rt_rumble_stop_bounded(void);

View File

@@ -373,7 +373,7 @@ static int mode_rc;
static void crash_handler(const PlatCrash *x) {
// 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 (!rt_ending_enter(71)) plat_exit(71);
rt_log_ending(); // the report reaches the log file past its cap (log_sink)
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 "

View File

@@ -74,7 +74,7 @@ void input_rumble_hd_stop(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
if (!rt_ending_enter(71)) 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());

View File

@@ -75,7 +75,7 @@ def static_checks():
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);'
ok = first == 'if (!rt_ending_enter(71)) plat_exit(71);'
print(f'static: main.c crash_handler starts with the shared guard: {"ok" if ok else "FAIL (" + first + ")"}',
flush=True)
bad += not ok
@@ -95,6 +95,15 @@ def static_checks():
print(f'static: {name} stops HD rumble through rt_rumble_stop_bounded after the RTSS restore: '
f'{"ok" if ok else "FAIL"}', flush=True)
bad += not ok
# a first report that hangs: the thread that waits for it ends the process after 30 s with that report's code, and
# fatal_end hands its own code to the guard (a hung FATAL must not read as a crash to the launcher)
dispatch = re.sub(r'//[^\n]*', '', (RT / 'dispatch.c').read_text(encoding='utf-8'))
enter = function(dispatch, 'int rt_ending_enter(int code)')
ok = ('__atomic_store_n(&ending_code, code' in enter
and 'plat_terminate(__atomic_load_n(&ending_code' in enter and 'rt_ending_enter(code)' in fatal)
print(f'static: dispatch.c rt_ending_enter ends a hung first report with that report\'s code: '
f'{"ok" if ok else "FAIL"}', flush=True)
bad += not ok
# the fatal error's frame-time summary comes after the RTSS restore and the rumble stop, as in the orderly end
s, p = fatal.find('rt_rumble_stop_bounded('), fatal.find('perf_summary_flush_try(')
ok = 0 <= s < p