mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
Keep 32 KiB of every thread's and fiber's stack back for a stack overflow's crash report.
A stack overflow leaves the faulting thread only a guard page's worth of stack for the handler, and the crash report needs more: main.c crash_handler's rt_log buffers are 2 KiB each, it walks the frames, and while the boot builds pipelines its first call writes a file (plat_file_write's frame is about 4 KiB). Nothing in the runtime set a stack guarantee, so the report faulted again; the verifier's probe ended with 0xC0000005 and no line once the report's first call was the file write. The launcher starts the game with stderr on DEVNULL, so raw_note's stderr line was all there was, and nobody saw it. The new platform plat_stack_guarantee calls SetThreadStackGuarantee with PLAT_STACK_GUARANTEE (32 KiB) and runs at the start of every thread win32.c thread_main starts, in fiber_start (a guest fiber's stack is its own and the guarantee belongs to the running fiber), and on the main thread in main. Each thread and fiber commits that much more of its stack. Linux's crash handling already runs on its signal stack (linux.c alt_stack), so it is empty there. Reproduced before the change by the new tests/crash_stack_check.py: a plat_thread_spawn thread and a plat_fiber_new fiber recurse until their stacks run out, and the handler writes a file and three 2 KiB log lines. Without the guarantee both ended with 0xC0000005 and no line; with it both write every line and end with 71. thread_end_check, fatal_exit_check and the compile checks pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -641,6 +641,7 @@ 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) {
|
||||
plat_stack_guarantee(); // a stack overflow on the main thread still gets its crash report (platform.h)
|
||||
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;
|
||||
|
||||
@@ -1929,6 +1929,8 @@ void plat_exit(int code) {
|
||||
|
||||
void plat_terminate(int code) { _exit(code); }
|
||||
|
||||
void plat_stack_guarantee(void) {} // the crash report runs on the signal stack crash handling sets up
|
||||
|
||||
int plat_setenv(const char *name, const char *value) {
|
||||
return (value ? setenv(name, value, 1) : unsetenv(name)) == 0;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,10 @@ typedef struct PlatThread PlatThread;
|
||||
typedef void (*PlatThreadFn)(void *arg);
|
||||
|
||||
// Starts fn(arg) on a new thread named name (shown in debuggers and profilers; may be NULL). NULL on failure.
|
||||
// Keeps PLAT_STACK_GUARANTEE bytes of the calling thread's (or fiber's) stack for its own stack overflow: the crash
|
||||
// report then runs on them. The threads and fibers made here get it at their start; main calls it for its thread.
|
||||
#define PLAT_STACK_GUARANTEE 32768
|
||||
void plat_stack_guarantee(void);
|
||||
PlatThread *plat_thread_new(PlatThreadFn fn, void *arg, const char *name);
|
||||
// The same with the stack size given: stack bytes reserved for the thread (0: PLAT_STACK_DEFAULT). The default is the
|
||||
// same on every OS, so a thread that fits on Windows fits elsewhere.
|
||||
|
||||
@@ -44,8 +44,20 @@ struct PlatThread {
|
||||
// ExitThread jumps back to the thread's start).
|
||||
static void thread_handles_close(void);
|
||||
|
||||
// A stack overflow leaves the faulting thread a guard page's worth of stack, about 12 KiB of which a crash handler
|
||||
// gets, and the report (main.c crash_handler) needs more: rt_log's 2 KiB buffers, the frame walk, and during the
|
||||
// boot's builds a file write first (plat_file_write's frame is about 4 KiB). It faulted again inside the report, and
|
||||
// the process ended with no FATAL line (the audit's probe: a 16 MiB-reserve thread recursing in 1 KiB frames). With
|
||||
// 32 KiB kept back, the probe's report ran whole with room to spare. The guarantee belongs to the running fiber, so
|
||||
// fiber_start asks for it too. Each thread and fiber commits that much more of its stack.
|
||||
void plat_stack_guarantee(void) {
|
||||
ULONG g = PLAT_STACK_GUARANTEE;
|
||||
SetThreadStackGuarantee(&g);
|
||||
}
|
||||
|
||||
static DWORD WINAPI thread_main(LPVOID p) {
|
||||
PlatThread *t = p;
|
||||
plat_stack_guarantee();
|
||||
t->fn(t->arg);
|
||||
plat_thread_end_run();
|
||||
thread_handles_close();
|
||||
@@ -336,6 +348,7 @@ void plat_fiber_unself(void) {
|
||||
|
||||
static void WINAPI fiber_start(void *p) {
|
||||
PlatFiber *f = p;
|
||||
plat_stack_guarantee(); // a guest fiber's stack is its own (kernel.c kern_fiber_init)
|
||||
f->fn(f->arg);
|
||||
}
|
||||
|
||||
|
||||
80
tests/crash_stack_check.c
Normal file
80
tests/crash_stack_check.c
Normal file
@@ -0,0 +1,80 @@
|
||||
// A stack overflow's crash report (runtime/platform/win32.c plat_stack_guarantee) with the platform layer linked: a
|
||||
// thread made by plat_thread_spawn, or a fiber made by plat_fiber_new, recurses until its stack runs out, and the crash
|
||||
// handler then writes its lines as main.c crash_handler does (2 KiB log buffers, a file write first, as during the
|
||||
// boot's builds, and a frame walk). Built and run by tests/crash_stack_check.py, which wants every line and code 71.
|
||||
// crash_stack_check thread <file> the overflow on a thread
|
||||
// crash_stack_check fiber <file> the overflow on a fiber
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include "platform/platform.h"
|
||||
|
||||
void rt_log(const char *fmt, ...) { (void)fmt; }
|
||||
|
||||
static void out(const char *s) {
|
||||
DWORD w;
|
||||
WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), s, (DWORD)strlen(s), &w, NULL);
|
||||
}
|
||||
|
||||
// main.c rt_log: a 2 KiB buffer on the stack and vsnprintf
|
||||
__attribute__((noinline)) static void log2k(const char *fmt, ...) {
|
||||
char buf[2048];
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(buf, sizeof buf, fmt, ap);
|
||||
va_end(ap);
|
||||
out(buf);
|
||||
}
|
||||
|
||||
static const char *file;
|
||||
|
||||
static void crash(const PlatCrash *x) {
|
||||
// main.c crash_handler's first call while the boot builds pipelines: vk_pipeline_crash_note writes a file
|
||||
static const char text[] = "the process faulted while the boot built pipelines\n";
|
||||
int wrote = plat_file_write(file, text, sizeof text - 1);
|
||||
log2k("report line 1: %s (%#x), the file written: %d\n", x->what, x->code, wrote);
|
||||
log2k("report line 2: %d host frames\n", x->nframes);
|
||||
void *f[62];
|
||||
USHORT n = RtlCaptureStackBackTrace(0, 62, f, NULL);
|
||||
log2k("report line 3: %u frames captured; the end\n", (unsigned)n);
|
||||
TerminateProcess(GetCurrentProcess(), 71);
|
||||
}
|
||||
|
||||
static volatile int never; // keeps the recursion from looking endless to the compiler
|
||||
__attribute__((noinline)) static int recurse(int d) {
|
||||
if (never) return 0;
|
||||
volatile char pad[1024];
|
||||
pad[0] = (char)d;
|
||||
return recurse(d + 1) + pad[0];
|
||||
}
|
||||
|
||||
static void overflower(void *arg) {
|
||||
(void)arg;
|
||||
recurse(0);
|
||||
}
|
||||
|
||||
static PlatFiber *main_fiber;
|
||||
static void fiber_body(void *arg) {
|
||||
(void)arg;
|
||||
recurse(0);
|
||||
plat_fiber_switch(main_fiber);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc < 3) return 2;
|
||||
file = argv[2];
|
||||
plat_crash_hook(crash);
|
||||
if (!strcmp(argv[1], "thread")) {
|
||||
plat_thread_spawn(overflower, NULL, "deep", 0);
|
||||
} else {
|
||||
main_fiber = plat_fiber_self();
|
||||
PlatFiber *f = plat_fiber_new(1 << 20, fiber_body, NULL);
|
||||
if (!main_fiber || !f) return 3;
|
||||
plat_fiber_switch(f);
|
||||
}
|
||||
Sleep(10000);
|
||||
out("main: no crash ended the process in 10 s\n");
|
||||
return 0;
|
||||
}
|
||||
68
tests/crash_stack_check.py
Normal file
68
tests/crash_stack_check.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""Build and run tests/crash_stack_check.c: a stack overflow on a thread and on a fiber made by the platform layer
|
||||
(runtime/platform/win32.c thread_main, fiber_start, plat_stack_guarantee) still gets its whole crash report, a file
|
||||
write and three 2 KiB log lines, and ends with the report's code 71. Without the stack kept back the report faulted
|
||||
again and the process ended with an access violation and no line. No game files.
|
||||
|
||||
Usage: crash_stack_check.py
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
MSYS_BIN = Path(r'C:\msys64\ucrt64\bin')
|
||||
|
||||
LAYOUT = '''// A stand-in for the generated layout.h (tests/crash_stack_check.py).
|
||||
#pragma once
|
||||
#define MOD_MAIN_BASE 0x7000000000ull
|
||||
#define MOD_MAIN_SIZE 0x4ea0000ull
|
||||
'''
|
||||
|
||||
|
||||
def compiler():
|
||||
if os.environ.get('CC'):
|
||||
return os.environ['CC']
|
||||
if os.name == 'nt' and (MSYS_BIN / 'clang.exe').exists():
|
||||
return str(MSYS_BIN / 'clang.exe')
|
||||
return shutil.which('clang') or 'clang'
|
||||
|
||||
|
||||
def main():
|
||||
if os.name != 'nt':
|
||||
print('crash_stack_check: Windows only')
|
||||
return 0
|
||||
work = ROOT / 'build' / 'test' / 'crash_stack'
|
||||
work.mkdir(parents=True, exist_ok=True)
|
||||
(work / 'layout.h').write_text(LAYOUT)
|
||||
exe = work / 'crash_stack_check.exe'
|
||||
env = dict(os.environ)
|
||||
if MSYS_BIN.exists():
|
||||
env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '')
|
||||
rt = ROOT / 'runtime'
|
||||
plat = [rt / 'platform' / 'win32.c', rt / 'platform' / 'common.c']
|
||||
libs = ['-lbcrypt', '-lws2_32', '-lpsapi', '-lwinmm', '-ldbghelp', '-lshell32', '-lole32', '-luser32', '-ladvapi32']
|
||||
subprocess.check_call([compiler(), '-O1', '-g', '-Wall', '-Wno-unused-function', '-fno-strict-aliasing',
|
||||
'-march=x86-64-v3', '-I', str(rt), '-I', str(work), str(ROOT / 'tests' / 'crash_stack_check.c'),
|
||||
*map(str, plat), '-o', str(exe), '-lm', *libs], env=env)
|
||||
bad = 0
|
||||
for mode in ('thread', 'fiber'):
|
||||
note = work / f'note_{mode}.txt'
|
||||
note.unlink(missing_ok=True)
|
||||
p = subprocess.run([str(exe), mode, str(note)], env=env, capture_output=True, text=True, errors='replace',
|
||||
timeout=60)
|
||||
lines = [f'report line {i}' for i in (1, 2, 3)]
|
||||
missing = [x for x in lines if x not in p.stdout]
|
||||
ok = p.returncode == 71 and not missing and 'stack overflow' in p.stdout and 'the file written: 1' in p.stdout
|
||||
print(f'a stack overflow on a {mode}: exit code {p.returncode & 0xffffffff:#x} (want 0x47), '
|
||||
f'{3 - len(missing)} of 3 report lines: {"ok" if ok else "FAIL"}', flush=True)
|
||||
if not ok:
|
||||
print(p.stdout[-2000:], p.stderr[-2000:])
|
||||
bad += not ok
|
||||
print(f'crash_stack_check: {"FAILED" if bad else "ok"}')
|
||||
return 1 if bad else 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user