mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 01:41:02 +00:00
Leave a guest thread by our own register jump instead of the C runtime's longjmp
ExitThread went back to the thread start with longjmp on a buffer from a non-unwinding setjmp, whose frame field is 0. When Control Flow Guard is on for the process, ucrtbase's longjmp checks the buffer first and ends the process with fast fail 13 (0xc0000409) on a frame of 0: offset 0xb373c of ucrtbase 10.0.26100.9711, the offset in the event logs of #222, #176 and #75. Exploit protection can force CFG on any program, and the first guest thread to exit, right after the game's first six-axis read, killed the process with no line in the log; the abort and invalid-parameter handlers never run for a fast fail. plat_jmp_save and plat_jmp_go keep and restore the Win64 callee-saved registers, MXCSR and the x87 control word themselves and validate nothing; Linux keeps setjmp and longjmp. Started with CFG forced on, the build before this change ends with 0xc0000409 at the same log line and in --probe-kernel; this one reaches gameplay and passes the probe. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0164eca287
commit
cacbdf2549
4 files changed
+88
-12
No files matched your search
+1
-1
@@ -121,7 +121,7 @@ __attribute__((noinline, noreturn, cold)) static void bad_entry(Cpu *c) {
|
||||
}
|
||||
|
||||
// kern_host_track: the call counted in the thread's host depth. Not a tail call, so the host stack holds this frame
|
||||
// while fn runs; longjmp out of fn (ExitThread) leaves the depth raised on a thread that ends.
|
||||
// while fn runs; the jump out of fn (ExitThread) leaves the depth raised on a thread that ends.
|
||||
__attribute__((noinline)) static void hostfn_tracked(Cpu *c, GuestFn fn) {
|
||||
void *t = c->thread;
|
||||
int *d = kern_host_depth(t);
|
||||
|
||||
+7
-11
@@ -8,10 +8,6 @@
|
||||
// Cpu. The SDK's nn::os runs as lifted code; only the kernel under it is the host. Every wait hangs on one global lock
|
||||
// G with one condition variable per thread; wake order is the kernel's: lower priority value first, then first come.
|
||||
// Semantics follow the public Horizon kernel (KConditionVariable, KAddressArbiter, KSynchronization).
|
||||
#ifdef _WIN32
|
||||
#define __USE_MINGW_SETJMP_NON_SEH // setjmp(b) = _setjmp(b, NULL): longjmp restores registers, no SEH unwinding
|
||||
#endif
|
||||
#include <setjmp.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -77,7 +73,7 @@ struct RtThread {
|
||||
uint64_t cseq;
|
||||
RtThread *cnext;
|
||||
uint8_t *tlr, *tls;
|
||||
jmp_buf exit_jmp; // the thread start; ExitThread longjmps to it
|
||||
PlatJmp exit_jmp; // the thread start; ExitThread jumps back to it
|
||||
// fibers
|
||||
PlatFiber *base_fiber; // the host thread as a host fiber, after the first switch
|
||||
Fiber *fiber; // current guest fiber, NULL on the thread's own context
|
||||
@@ -1449,10 +1445,10 @@ static void thread_finish(RtThread *t) {
|
||||
unlock();
|
||||
}
|
||||
|
||||
// ExitThread: back to the thread start with a non-unwinding longjmp; no SEH unwinding through lifted frames.
|
||||
// (__builtin_setjmp is not usable: on Win64 LLVM saves the frame address, which is not rbp when the frame register
|
||||
// is offset, and the landing code then reads its locals through the wrong base.)
|
||||
static __attribute__((noreturn)) void exit_now(RtThread *t) { longjmp(t->exit_jmp, 1); }
|
||||
// ExitThread: back to the thread start with plat_jmp_go, which unwinds nothing: no SEH unwinding through lifted
|
||||
// frames. (__builtin_setjmp is not usable: on Win64 LLVM saves the frame address, which is not rbp when the frame
|
||||
// register is offset, and the landing code then reads its locals through the wrong base.)
|
||||
static __attribute__((noreturn)) void exit_now(RtThread *t) { plat_jmp_go(&t->exit_jmp); }
|
||||
|
||||
// The calling host thread, for ap_try (TOTK_ASYNC_PREEMPT) and dg_sample (TOTK_CORE_DIAG).
|
||||
static PlatIntr *ap_open_self(void) {
|
||||
@@ -1474,7 +1470,7 @@ static void thread_main(void *p) {
|
||||
unlock();
|
||||
Cpu *c = t->cpu;
|
||||
c->sp = t->stack_top & ~15ull;
|
||||
if (!setjmp(t->exit_jmp)) {
|
||||
if (!plat_jmp_save(&t->exit_jmp)) {
|
||||
rt_call(c, t->entry, 1, t->arg);
|
||||
char d[128];
|
||||
rt_fatal("guest thread %#x: entry %s returned (Horizon threads end with ExitThread)", t->handle,
|
||||
@@ -1534,7 +1530,7 @@ static void guest_main(void *p) {
|
||||
void (*fn)(Cpu *) = (void (*)(Cpu *))p;
|
||||
Cpu *c;
|
||||
kern_main_thread(&c);
|
||||
if (!setjmp(tcur->exit_jmp)) fn(c);
|
||||
if (!plat_jmp_save(&tcur->exit_jmp)) fn(c);
|
||||
thread_finish(tcur);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -61,6 +61,23 @@ void plat_fiber_unself(void); // the calling thread stops being a fi
|
||||
PlatFiber *plat_fiber_new(size_t stack, void (*fn)(void *arg), void *arg);
|
||||
void plat_fiber_switch(PlatFiber *to); // from the fiber the calling thread runs
|
||||
void plat_fiber_free(PlatFiber *f); // a fiber nobody runs
|
||||
|
||||
// A jump back to a point saved earlier on the same thread, without unwinding the frames in between (no SEH, no C++
|
||||
// cleanups): the kernel's ExitThread leaves the lifted frames this way. plat_jmp_save returns 0 when it saves and 1
|
||||
// when plat_jmp_go comes back to it; the function that called it must still be running. It keeps the callee-saved
|
||||
// registers and the floating-point control state. On Windows this is not the C runtime's setjmp: a non-unwinding
|
||||
// setjmp leaves the buffer's frame 0, and the C runtime's longjmp ends the process with 0xc0000409 (fast fail 13) on
|
||||
// such a buffer when Control Flow Guard is on for the process, which Exploit protection can force on any program.
|
||||
#ifdef _WIN32
|
||||
typedef struct { uint64_t gpr[10]; uint32_t mxcsr, fpcw; uint64_t pad; uint8_t xmm[160]; } PlatJmp;
|
||||
int plat_jmp_save(PlatJmp *j) __attribute__((returns_twice, nothrow));
|
||||
_Noreturn void plat_jmp_go(PlatJmp *j);
|
||||
#else
|
||||
#include <setjmp.h>
|
||||
typedef struct { jmp_buf b; } PlatJmp;
|
||||
#define plat_jmp_save(j) setjmp((j)->b)
|
||||
#define plat_jmp_go(j) longjmp((j)->b, 1)
|
||||
#endif
|
||||
// Scheduling priority, Windows' seven levels: of the calling thread, or of t. On Linux a thread lowered without the
|
||||
// right to raise its priority again (CAP_SYS_NICE, or an RLIMIT_NICE of 20) could never return to normal, and the
|
||||
// runtime lowers threads that later take shared locks at normal priority; without that right every change is skipped
|
||||
|
||||
@@ -199,6 +199,69 @@ void plat_fiber_free(PlatFiber *f) {
|
||||
free(f);
|
||||
}
|
||||
|
||||
// plat_jmp_save / plat_jmp_go: PlatJmp holds rbx, rbp, rdi, rsi, r12-r15, the caller's rsp and the return address at
|
||||
// 0..72, MXCSR at 80, the x87 control word at 84 and xmm6-xmm15 from 96, the Win64 callee-saved state. Both are leaf
|
||||
// code that does not move rsp before it leaves, so they need no unwind data.
|
||||
_Static_assert(sizeof(PlatJmp) == 256 && __builtin_offsetof(PlatJmp, xmm) == 96, "plat_jmp_save layout");
|
||||
__asm__(".text\n"
|
||||
".globl plat_jmp_save\n"
|
||||
".def plat_jmp_save; .scl 2; .type 32; .endef\n"
|
||||
".p2align 4\n"
|
||||
"plat_jmp_save:\n"
|
||||
" movq %rbx, 0(%rcx)\n"
|
||||
" movq %rbp, 8(%rcx)\n"
|
||||
" movq %rdi, 16(%rcx)\n"
|
||||
" movq %rsi, 24(%rcx)\n"
|
||||
" movq %r12, 32(%rcx)\n"
|
||||
" movq %r13, 40(%rcx)\n"
|
||||
" movq %r14, 48(%rcx)\n"
|
||||
" movq %r15, 56(%rcx)\n"
|
||||
" leaq 8(%rsp), %rdx\n" // the caller's rsp after this call returns
|
||||
" movq %rdx, 64(%rcx)\n"
|
||||
" movq (%rsp), %rdx\n"
|
||||
" movq %rdx, 72(%rcx)\n"
|
||||
" stmxcsr 80(%rcx)\n"
|
||||
" fnstcw 84(%rcx)\n"
|
||||
" movdqu %xmm6, 96(%rcx)\n"
|
||||
" movdqu %xmm7, 112(%rcx)\n"
|
||||
" movdqu %xmm8, 128(%rcx)\n"
|
||||
" movdqu %xmm9, 144(%rcx)\n"
|
||||
" movdqu %xmm10, 160(%rcx)\n"
|
||||
" movdqu %xmm11, 176(%rcx)\n"
|
||||
" movdqu %xmm12, 192(%rcx)\n"
|
||||
" movdqu %xmm13, 208(%rcx)\n"
|
||||
" movdqu %xmm14, 224(%rcx)\n"
|
||||
" movdqu %xmm15, 240(%rcx)\n"
|
||||
" xorl %eax, %eax\n"
|
||||
" ret\n"
|
||||
".globl plat_jmp_go\n"
|
||||
".def plat_jmp_go; .scl 2; .type 32; .endef\n"
|
||||
".p2align 4\n"
|
||||
"plat_jmp_go:\n"
|
||||
" movq 0(%rcx), %rbx\n"
|
||||
" movq 8(%rcx), %rbp\n"
|
||||
" movq 16(%rcx), %rdi\n"
|
||||
" movq 24(%rcx), %rsi\n"
|
||||
" movq 32(%rcx), %r12\n"
|
||||
" movq 40(%rcx), %r13\n"
|
||||
" movq 48(%rcx), %r14\n"
|
||||
" movq 56(%rcx), %r15\n"
|
||||
" ldmxcsr 80(%rcx)\n"
|
||||
" fldcw 84(%rcx)\n"
|
||||
" movdqu 96(%rcx), %xmm6\n"
|
||||
" movdqu 112(%rcx), %xmm7\n"
|
||||
" movdqu 128(%rcx), %xmm8\n"
|
||||
" movdqu 144(%rcx), %xmm9\n"
|
||||
" movdqu 160(%rcx), %xmm10\n"
|
||||
" movdqu 176(%rcx), %xmm11\n"
|
||||
" movdqu 192(%rcx), %xmm12\n"
|
||||
" movdqu 208(%rcx), %xmm13\n"
|
||||
" movdqu 224(%rcx), %xmm14\n"
|
||||
" movdqu 240(%rcx), %xmm15\n"
|
||||
" movq 64(%rcx), %rsp\n"
|
||||
" movl $1, %eax\n"
|
||||
" jmpq *72(%rcx)\n");
|
||||
|
||||
int plat_thread_spawn(PlatThreadFn fn, void *arg, const char *name, size_t stack) {
|
||||
PlatThread *t = plat_thread_new_ex(fn, arg, name, stack);
|
||||
if (t) plat_thread_detach(t);
|
||||
|
||||
Reference in new issue
Block a user