mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 01:41:02 +00:00
On 1.2.1 the game asks a compound physics shape for its first child's material at main+0xe056d0 and reads the result without a check. At the reported crash the shape had none of its 125 children left, although every child is filled when the shape is built. The crash handler now prints that shape's live bits and each record's shape pointer and name, which tells removed children from switched-off ones and names the object. It runs only after the crash. --probe-kernel builds a fake shape in a crashing child and checks the listing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
869 lines
39 KiB
C
869 lines
39 KiB
C
// --probe-kernel: the kernel's waits, wake orders, virtual cores, fibers and
|
|
// ExitThread on guest threads whose entries are host functions, then real lifted code on four guest threads: main's
|
|
// PLT stub for nn::os::GetSystemTick, through the lifted sdk function, to rt_ticks.
|
|
//
|
|
// Every probe thread is created with SVC 0x08 and drives the kernel through rt_svc, the entry the lifted code uses.
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "cfg.h"
|
|
#include "host.h"
|
|
#include "kernel.h"
|
|
#include "layout.h"
|
|
#include "platform/platform.h"
|
|
|
|
#define STACK 0x10000
|
|
#define JOIN_NS 10000000000ll
|
|
#define TICK_SYMBOL "_ZN2nn2os13GetSystemTickEv"
|
|
#define FIBER_SWITCHES 10000
|
|
#define CORE_WORKERS 8
|
|
#define CORE_RUNS 300
|
|
#define TICK_THREADS 4
|
|
#define TICK_CALLS 1000
|
|
|
|
static int failures;
|
|
#define CHECK(cond, ...) do { if (!(cond)) { failures++; printf(" FAIL: " __VA_ARGS__); putchar('\n'); } } while (0)
|
|
|
|
static uint64_t a_lock_waiter, a_cv_waiter, a_cv_order_waiter, a_arb_waiter, a_event_waiter, a_core_worker,
|
|
a_exit_entry, a_exit_nested, a_fiber_fn, a_fiber_exit_thread, a_fiber_exit_fn, a_tick_worker;
|
|
static uint64_t a_fib_init, a_fib_fin, a_fib_switch, a_fib_cur, tick_stub;
|
|
|
|
// ---- helpers: SVCs through rt_svc, thread start and join
|
|
static uint32_t sv(Cpu *c, uint32_t imm, uint64_t a0, uint64_t a1, uint64_t a2, uint64_t a3) {
|
|
c->x[0] = a0;
|
|
c->x[1] = a1;
|
|
c->x[2] = a2;
|
|
c->x[3] = a3;
|
|
rt_svc(c, imm, 0);
|
|
return (uint32_t)c->x[0];
|
|
}
|
|
|
|
static void sleep_ms(Cpu *c, int ms) { sv(c, 0x0B, (uint64_t)ms * 1000000, 0, 0, 0); }
|
|
static __attribute__((noreturn)) void exit_thread(Cpu *c) {
|
|
sv(c, 0x0A, 0, 0, 0, 0);
|
|
rt_fatal("probe-kernel: ExitThread returned");
|
|
}
|
|
static uint64_t ga(const volatile void *p) { return (uint64_t)(uintptr_t)p; }
|
|
|
|
static uint32_t create(Cpu *c, uint64_t entry, void *arg, int prio, int core) {
|
|
uint8_t *stack = mem_kernel_alloc(STACK, 16);
|
|
c->x[1] = entry;
|
|
c->x[2] = ga(arg);
|
|
c->x[3] = ga(stack + STACK);
|
|
c->x[4] = (uint64_t)prio;
|
|
c->x[5] = (uint64_t)(int64_t)core;
|
|
rt_svc(c, 0x08, 0);
|
|
if (c->x[0]) rt_fatal("probe-kernel: CreateThread failed %#llx", (unsigned long long)c->x[0]);
|
|
return (uint32_t)c->x[1];
|
|
}
|
|
|
|
static void start(Cpu *c, uint32_t h) { CHECK(sv(c, 0x09, h, 0, 0, 0) == 0, "StartThread %#x", h); }
|
|
|
|
static uint32_t wait1(Cpu *c, uint32_t h, int64_t ns) {
|
|
uint32_t hs[1] = {h};
|
|
return sv(c, 0x18, 0, ga(hs), 1, (uint64_t)ns);
|
|
}
|
|
|
|
static void join(Cpu *c, uint32_t h) {
|
|
uint32_t r = wait1(c, h, JOIN_NS);
|
|
if (r) rt_fatal("probe-kernel: thread %#x did not end (wait result %#x)", h, r);
|
|
CHECK(sv(c, 0x16, h, 0, 0, 0) == 0, "CloseHandle(thread %#x)", h);
|
|
}
|
|
|
|
// Poll (sleeping, so the virtual core is free) until the thread reaches a state; 5 s at most.
|
|
static int wait_state(Cpu *c, uint32_t h, int want) {
|
|
for (int i = 0; i < 5000; i++) {
|
|
if (kern_thread_state(h) == want) return 1;
|
|
sleep_ms(c, 1);
|
|
}
|
|
CHECK(0, "thread %#x never reached state %d (now %d)", h, want, kern_thread_state(h));
|
|
return 0;
|
|
}
|
|
|
|
static double ms_since(uint64_t t0) { return (double)(kern_now_ns() - t0) / 1e6; }
|
|
|
|
// ---- user-mode lock of the SDK: tag word = owner handle | waiters bit
|
|
static uint32_t ld(volatile uint32_t *a) { return __atomic_load_n(a, __ATOMIC_SEQ_CST); }
|
|
static int cas(volatile uint32_t *a, uint32_t *e, uint32_t v) {
|
|
return __atomic_compare_exchange_n(a, e, v, 0, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST);
|
|
}
|
|
|
|
static void user_lock(Cpu *c, volatile uint32_t *a, uint32_t tag) {
|
|
for (;;) {
|
|
uint32_t v = 0;
|
|
if (cas(a, &v, tag)) return;
|
|
if (!(v & KERN_WAITERS_BIT)) {
|
|
if (!cas(a, &v, v | KERN_WAITERS_BIT)) continue;
|
|
v |= KERN_WAITERS_BIT;
|
|
}
|
|
sv(c, 0x1A, v & ~KERN_WAITERS_BIT, ga(a), tag, 0);
|
|
if ((ld(a) & ~KERN_WAITERS_BIT) == tag) return;
|
|
}
|
|
}
|
|
|
|
static void user_unlock(Cpu *c, volatile uint32_t *a, uint32_t tag) {
|
|
uint32_t v = tag;
|
|
if (cas(a, &v, 0)) return;
|
|
sv(c, 0x1B, ga(a), 0, 0, 0);
|
|
}
|
|
|
|
// ---- lock handoff order
|
|
typedef struct { volatile uint32_t *lock, *key; uint32_t tag, r; int idx, held; } LockJob;
|
|
static volatile uint32_t g_lockw __attribute__((aligned(8))), g_keyw __attribute__((aligned(8)));
|
|
static volatile int32_t g_order_n;
|
|
static int g_order[8];
|
|
|
|
static void record(int idx) { g_order[plat_inc32(&g_order_n) - 1] = idx; }
|
|
|
|
static void lock_waiter(Cpu *c) {
|
|
LockJob *j = arg_p(c, 0);
|
|
user_lock(c, j->lock, j->tag);
|
|
record(j->idx);
|
|
user_unlock(c, j->lock, j->tag);
|
|
exit_thread(c);
|
|
}
|
|
|
|
static int order_is(const int *want, int n, char *buf, size_t size) {
|
|
int ok = g_order_n == n;
|
|
buf[0] = 0;
|
|
for (int i = 0; i < g_order_n && i < 8; i++) {
|
|
snprintf(buf + strlen(buf), size - strlen(buf), "%s%d", i ? " " : "", g_order[i]);
|
|
ok &= i < n && g_order[i] == want[i];
|
|
}
|
|
return ok;
|
|
}
|
|
|
|
static void test_lock(Cpu *c, uint32_t me) {
|
|
static const int prio[] = {40, 30, 40, 30, 35}, want[] = {1, 3, 4, 0, 2};
|
|
LockJob j[5];
|
|
uint32_t h[5];
|
|
g_lockw = me;
|
|
g_order_n = 0;
|
|
for (int i = 0; i < 5; i++) {
|
|
j[i] = (LockJob){.lock = &g_lockw, .idx = i};
|
|
h[i] = create(c, a_lock_waiter, &j[i], prio[i], -2);
|
|
j[i].tag = h[i];
|
|
start(c, h[i]);
|
|
wait_state(c, h[i], KERN_WAIT_LOCK);
|
|
}
|
|
CHECK(ld(&g_lockw) == (me | KERN_WAITERS_BIT), "lock word %#x with 5 waiters", ld(&g_lockw));
|
|
user_unlock(c, &g_lockw, me);
|
|
for (int i = 0; i < 5; i++) join(c, h[i]);
|
|
char got[64];
|
|
int ok = order_is(want, 5, got, sizeof got);
|
|
printf("lock handoff: priorities 40 30 40 30 35 registered in order, woken %s (expected 1 3 4 0 2): %s\n", got,
|
|
ok ? "ok" : "FAIL");
|
|
CHECK(ok, "lock handoff order");
|
|
CHECK(ld(&g_lockw) == 0, "lock word %#x after the last unlock", ld(&g_lockw));
|
|
}
|
|
|
|
// ---- condition variables
|
|
static void cv_waiter(Cpu *c) { // waits forever; woken holding the lock
|
|
LockJob *j = arg_p(c, 0);
|
|
user_lock(c, j->lock, j->tag);
|
|
j->r = sv(c, 0x1C, ga(j->lock), ga(j->key), j->tag, (uint64_t)-1);
|
|
j->held = (ld(j->lock) & ~KERN_WAITERS_BIT) == j->tag;
|
|
record(j->idx);
|
|
user_unlock(c, j->lock, j->tag);
|
|
exit_thread(c);
|
|
}
|
|
|
|
static void test_cv_wake_order(Cpu *c) {
|
|
static const int prio[] = {45, 35, 40}, want[] = {1, 2, 0};
|
|
LockJob j[3];
|
|
uint32_t h[3];
|
|
g_lockw = 0;
|
|
g_keyw = 0;
|
|
g_order_n = 0;
|
|
for (int i = 0; i < 3; i++) {
|
|
j[i] = (LockJob){.lock = &g_lockw, .key = &g_keyw, .idx = i};
|
|
h[i] = create(c, a_cv_waiter, &j[i], prio[i], -2);
|
|
j[i].tag = h[i];
|
|
start(c, h[i]);
|
|
wait_state(c, h[i], KERN_WAIT_CONDVAR);
|
|
}
|
|
CHECK(ld(&g_keyw) == 1, "condvar key %u with 3 waiters", ld(&g_keyw));
|
|
for (int i = 0; i < 3; i++) CHECK(sv(c, 0x1D, ga(&g_keyw), 1, 0, 0) == 0, "SignalProcessWideKey");
|
|
for (int i = 0; i < 3; i++) join(c, h[i]);
|
|
char got[64];
|
|
int ok = order_is(want, 3, got, sizeof got);
|
|
for (int i = 0; i < 3; i++) ok &= j[i].r == 0 && j[i].held;
|
|
printf("condvar wake order: priorities 45 35 40, signalled one at a time, woken %s holding the lock (expected "
|
|
"1 2 0): %s\n", got, ok ? "ok" : "FAIL");
|
|
CHECK(ok, "condvar wake order");
|
|
CHECK(ld(&g_keyw) == 0 && ld(&g_lockw) == 0, "key %u lock %#x after the last waiter", ld(&g_keyw), ld(&g_lockw));
|
|
}
|
|
|
|
// Another thread's user-mode critical section and Signal, run inside the waiter's svc 0x1C right after the lock
|
|
// release: take the lock, read the key (the SDK's Signal skips svc 0x1D on 0), release.
|
|
static volatile int g_seen_key;
|
|
static void cv_signaller(void) {
|
|
uint32_t e = 0;
|
|
if (cas(&g_lockw, &e, 0x7777)) {
|
|
g_seen_key = (int)ld(&g_keyw);
|
|
__atomic_store_n(&g_lockw, 0, __ATOMIC_SEQ_CST);
|
|
}
|
|
kern_test_cv_hook = NULL;
|
|
}
|
|
|
|
static void cv_order_waiter(Cpu *c) {
|
|
LockJob *j = arg_p(c, 0);
|
|
user_lock(c, j->lock, j->tag);
|
|
j->r = sv(c, 0x1C, ga(j->lock), ga(j->key), j->tag, 200000000);
|
|
if (!j->r) user_unlock(c, j->lock, j->tag);
|
|
exit_thread(c);
|
|
}
|
|
|
|
static uint32_t cv_order_case(Cpu *c, int old) {
|
|
LockJob j = {.lock = &g_lockw, .key = &g_keyw};
|
|
g_lockw = 0;
|
|
g_keyw = 0;
|
|
g_seen_key = -1;
|
|
kern_test_cv_old_order = old;
|
|
kern_test_cv_hook = cv_signaller;
|
|
uint32_t h = create(c, a_cv_order_waiter, &j, 44, -2);
|
|
j.tag = h;
|
|
start(c, h);
|
|
while (kern_thread_state(h) != KERN_WAIT_CONDVAR && kern_thread_state(h) != KERN_THREAD_EXITED) sleep_ms(c, 1);
|
|
if (g_seen_key != 0) sv(c, 0x1D, ga(&g_keyw), 1, 0, 0); // what the SDK's Signal does
|
|
join(c, h);
|
|
kern_test_cv_old_order = 0;
|
|
kern_test_cv_hook = NULL;
|
|
return j.r;
|
|
}
|
|
|
|
static void test_cv_key_order(Cpu *c) {
|
|
uint32_t r = cv_order_case(c, 0);
|
|
int seen = g_seen_key;
|
|
printf("condvar key order: key set before the lock release; a signaller between release and wait saw key %d, "
|
|
"waiter result %#x: %s\n", seen, r, seen == 1 && r == 0 ? "ok" : "FAIL");
|
|
CHECK(seen == 1 && r == 0, "condvar key order");
|
|
r = cv_order_case(c, 1);
|
|
seen = g_seen_key;
|
|
int lost = seen == 0 && r == KERN_R_TIMEOUT;
|
|
printf("condvar key order, negative control (MNG's old order, release then set the key): signaller saw key %d, "
|
|
"waiter result %#x: %s\n", seen, r, lost ? "lost wakeup, fails as expected" : "FAIL (the control did not "
|
|
"fail)");
|
|
CHECK(lost, "the old-order negative control did not lose the wakeup");
|
|
}
|
|
|
|
// ---- address arbiter
|
|
typedef struct { int type, value, idx; int64_t timeout; uint32_t r; } ArbJob;
|
|
static volatile uint32_t g_arb __attribute__((aligned(8)));
|
|
|
|
static void arb_waiter(Cpu *c) {
|
|
ArbJob *j = arg_p(c, 0);
|
|
j->r = sv(c, 0x34, ga(&g_arb), (uint64_t)j->type, (uint64_t)(int64_t)j->value, (uint64_t)j->timeout);
|
|
exit_thread(c);
|
|
}
|
|
|
|
static uint32_t arb_spawn(Cpu *c, ArbJob *j) {
|
|
uint32_t h = create(c, a_arb_waiter, j, 44, -2);
|
|
start(c, h);
|
|
wait_state(c, h, KERN_WAIT_ADDRESS);
|
|
return h;
|
|
}
|
|
|
|
static uint32_t sig(Cpu *c, int type, int value, int count) {
|
|
return sv(c, 0x35, ga(&g_arb), (uint64_t)type, (uint64_t)(int64_t)value, (uint64_t)(int64_t)count);
|
|
}
|
|
|
|
static void test_arbiter(Cpu *c) {
|
|
int before = failures;
|
|
uint64_t arb = ga(&g_arb);
|
|
// wait type 0, if less than
|
|
g_arb = 0;
|
|
CHECK(sv(c, 0x34, arb, 0, 0, (uint64_t)-1) == KERN_R_BAD_STATE, "wait type 0 with value not less");
|
|
CHECK(sv(c, 0x34, arb, 0, 1, 0) == KERN_R_TIMEOUT, "wait type 0 with timeout 0");
|
|
ArbJob j0 = {.type = 0, .value = 1, .timeout = -1};
|
|
uint32_t h = arb_spawn(c, &j0);
|
|
CHECK(sig(c, 0, 0, 1) == 0, "signal type 0");
|
|
join(c, h);
|
|
CHECK(j0.r == 0, "wait type 0 woken by signal type 0: %#x", j0.r);
|
|
// wait type 1, decrement if less than; signal type 1, increment if equal
|
|
g_arb = 0;
|
|
ArbJob j1 = {.type = 1, .value = 1, .timeout = -1};
|
|
h = arb_spawn(c, &j1);
|
|
CHECK((int32_t)g_arb == -1, "wait type 1 did not decrement (%d)", (int32_t)g_arb);
|
|
CHECK(sig(c, 1, 0, 1) == KERN_R_BAD_STATE && (int32_t)g_arb == -1 && kern_thread_state(h) == KERN_WAIT_ADDRESS,
|
|
"signal type 1 with a different value");
|
|
CHECK(sig(c, 1, -1, 1) == 0 && g_arb == 0, "signal type 1 did not increment (%d)", (int32_t)g_arb);
|
|
join(c, h);
|
|
CHECK(j1.r == 0, "wait type 1 woken by signal type 1: %#x", j1.r);
|
|
// wait type 2, if equal; signal type 2, modify by the number of waiters if equal
|
|
g_arb = 5;
|
|
CHECK(sv(c, 0x34, arb, 2, 6, (uint64_t)-1) == KERN_R_BAD_STATE, "wait type 2 with a different value");
|
|
ArbJob ja = {.type = 2, .value = 5, .timeout = -1}, jb = ja;
|
|
uint32_t ha = arb_spawn(c, &ja), hb = arb_spawn(c, &jb);
|
|
CHECK(sig(c, 2, 5, 1) == 0 && g_arb == 5, "signal type 2, 2 waiters, count 1: value %d (want 5)", (int32_t)g_arb);
|
|
wait_state(c, ha, KERN_THREAD_EXITED);
|
|
CHECK(kern_thread_state(hb) == KERN_WAIT_ADDRESS, "signal type 2 woke more than one, or not the first");
|
|
CHECK(sig(c, 2, 5, 1) == 0 && g_arb == 4, "signal type 2, 1 waiter, count 1: value %d (want 4)", (int32_t)g_arb);
|
|
join(c, ha);
|
|
join(c, hb);
|
|
CHECK(sig(c, 2, 4, 1) == 0 && g_arb == 5, "signal type 2, no waiter: value %d (want 5)", (int32_t)g_arb);
|
|
CHECK(sig(c, 2, 5, 0) == 0 && g_arb == 6, "signal type 2, no waiter, count 0: value %d (want 6)", (int32_t)g_arb);
|
|
ArbJob jc = {.type = 2, .value = 6, .timeout = -1};
|
|
h = arb_spawn(c, &jc);
|
|
// Waking all (count <= 0) with waiters subtracts 2 (mesosphere KAddressArbiter::SignalAndModifyByWaitingCountIfEqual).
|
|
CHECK(sig(c, 2, 6, -1) == 0 && g_arb == 4, "signal type 2, 1 waiter, count -1: value %d (want 4)", (int32_t)g_arb);
|
|
join(c, h);
|
|
g_arb = 6;
|
|
ArbJob jd = {.type = 2, .value = 6, .timeout = -1}, je = jd;
|
|
uint32_t hd = arb_spawn(c, &jd), he = arb_spawn(c, &je);
|
|
CHECK(sig(c, 2, 6, 0) == 0 && g_arb == 4, "signal type 2, 2 waiters, count 0: value %d (want 4)", (int32_t)g_arb);
|
|
join(c, hd);
|
|
join(c, he);
|
|
CHECK(ja.r == 0 && jb.r == 0 && jc.r == 0 && jd.r == 0 && je.r == 0, "wait type 2 results %#x %#x %#x %#x %#x", ja.r,
|
|
jb.r, jc.r, jd.r, je.r);
|
|
g_arb = 5;
|
|
CHECK(sig(c, 2, 7, 1) == KERN_R_BAD_STATE, "signal type 2 with a different value");
|
|
// timeout, invalid type
|
|
uint64_t t0 = kern_now_ns();
|
|
uint32_t r = sv(c, 0x34, arb, 2, 5, 5000000);
|
|
double ms = ms_since(t0);
|
|
CHECK(r == KERN_R_TIMEOUT && ms >= 5.0, "wait type 2 with a 5 ms timeout: %#x after %.2f ms", r, ms);
|
|
CHECK(sv(c, 0x34, arb, 3, 0, 0) == KERN_R_INVALID_ENUM && sig(c, 3, 0, 0) == KERN_R_INVALID_ENUM, "type 3");
|
|
printf("address arbiter: wait types 0-2 (less, decrement-less, equal), signal types 0-2 (wake, increment, modify by "
|
|
"waiters), 5 ms timeout after %.2f ms: %s\n", ms, failures == before ? "ok" : "FAIL");
|
|
}
|
|
|
|
// ---- events
|
|
// pre: a first wait on h0 with timeout t0 (result r0, index idx0) before the wait on h.
|
|
typedef struct { uint32_t h, r, idx, h0, r0, idx0; int64_t timeout, t0; volatile int delay, go; int pre; } EvJob;
|
|
|
|
static void event_waiter(Cpu *c) {
|
|
EvJob *j = arg_p(c, 0);
|
|
while (j->delay && !j->go) sleep_ms(c, 1);
|
|
if (j->pre) {
|
|
uint32_t h0[1] = {j->h0};
|
|
j->r0 = sv(c, 0x18, 0, ga(h0), 1, (uint64_t)j->t0);
|
|
j->idx0 = (uint32_t)c->x[1];
|
|
}
|
|
uint32_t hs[1] = {j->h};
|
|
j->r = sv(c, 0x18, 0, ga(hs), 1, (uint64_t)j->timeout);
|
|
j->idx = (uint32_t)c->x[1];
|
|
exit_thread(c);
|
|
}
|
|
|
|
static void test_events(Cpu *c) {
|
|
int before = failures;
|
|
CHECK(sv(c, 0x45, 0, 0, 0, 0) == 0, "CreateEvent");
|
|
uint32_t hw = (uint32_t)c->x[1], hr = (uint32_t)c->x[2];
|
|
uint64_t t0 = kern_now_ns();
|
|
uint32_t r = wait1(c, hr, 10000000);
|
|
double ms = ms_since(t0);
|
|
CHECK(r == KERN_R_TIMEOUT && ms >= 10.0, "wait with a 10 ms timeout: %#x after %.2f ms", r, ms);
|
|
CHECK(sv(c, 0x17, hr, 0, 0, 0) == KERN_R_BAD_STATE, "ResetSignal of an unsignalled event");
|
|
|
|
EvJob e = {.h = hr, .timeout = -1};
|
|
uint32_t h = create(c, a_event_waiter, &e, 44, -2);
|
|
start(c, h);
|
|
wait_state(c, h, KERN_WAIT_SYNC);
|
|
CHECK(sv(c, 0x11, hw, 0, 0, 0) == 0, "SignalEvent");
|
|
join(c, h);
|
|
CHECK(e.r == 0 && e.idx == 0, "waiter woken by SignalEvent: %#x index %u", e.r, e.idx);
|
|
|
|
CHECK(sv(c, 0x45, 0, 0, 0, 0) == 0, "CreateEvent");
|
|
uint32_t hw2 = (uint32_t)c->x[1], hr2 = (uint32_t)c->x[2];
|
|
uint32_t two[2] = {hr2, hr};
|
|
CHECK(sv(c, 0x18, 0, ga(two), 2, 0) == 0 && c->x[1] == 1, "wait on two handles, the second signalled");
|
|
CHECK(wait1(c, hw, 0) == KERN_R_BAD_HANDLE, "wait on a writable event handle");
|
|
CHECK(sv(c, 0x12, hr, 0, 0, 0) == 0 && wait1(c, hr, 0) == KERN_R_TIMEOUT, "ClearEvent");
|
|
CHECK(sv(c, 0x11, hr, 0, 0, 0) == KERN_R_BAD_HANDLE, "SignalEvent on the readable end");
|
|
CHECK(sv(c, 0x11, hw, 0, 0, 0) == 0 && sv(c, 0x17, hr, 0, 0, 0) == 0 && sv(c, 0x17, hr, 0, 0, 0) == KERN_R_BAD_STATE,
|
|
"ResetSignal");
|
|
|
|
EvJob f = {.h = hr, .timeout = -1};
|
|
h = create(c, a_event_waiter, &f, 44, -2);
|
|
start(c, h);
|
|
wait_state(c, h, KERN_WAIT_SYNC);
|
|
CHECK(sv(c, 0x19, h, 0, 0, 0) == 0, "CancelSynchronization");
|
|
join(c, h);
|
|
CHECK(f.r == KERN_R_CANCELLED, "cancelled wait: %#x", f.r);
|
|
|
|
EvJob g = {.h = hr, .timeout = -1, .delay = 1};
|
|
h = create(c, a_event_waiter, &g, 44, -2);
|
|
start(c, h);
|
|
CHECK(sv(c, 0x19, h, 0, 0, 0) == 0, "CancelSynchronization before the wait");
|
|
g.go = 1;
|
|
join(c, h);
|
|
CHECK(g.r == KERN_R_CANCELLED, "wait after a cancel: %#x", g.r);
|
|
|
|
// A pending cancel is for the next WaitSynchronization that would block (mesosphere KSynchronizationObject::Wait:
|
|
// signalled objects first, then timeout 0, then the cancel): a poll times out and a wait on a signalled object
|
|
// succeeds, both leaving it pending; the next blocking wait ends cancelled. The blocking waits have a 500 ms
|
|
// timeout, so a cancel used up too early shows as a timeout instead of a hang.
|
|
CHECK(sv(c, 0x11, hw2, 0, 0, 0) == 0, "SignalEvent of the second event");
|
|
EvJob p = {.h = hr, .timeout = 500000000, .delay = 1, .pre = 1, .h0 = hr, .t0 = 0};
|
|
EvJob q = {.h = hr, .timeout = 500000000, .delay = 1, .pre = 1, .h0 = hr2, .t0 = -1};
|
|
EvJob *pq[2] = {&p, &q};
|
|
for (int i = 0; i < 2; i++) {
|
|
h = create(c, a_event_waiter, pq[i], 44, -2);
|
|
start(c, h);
|
|
CHECK(sv(c, 0x19, h, 0, 0, 0) == 0, "CancelSynchronization before the %s", i ? "signalled wait" : "poll");
|
|
pq[i]->go = 1;
|
|
join(c, h);
|
|
}
|
|
CHECK(p.r0 == KERN_R_TIMEOUT, "poll (timeout 0) with a cancel pending: %#x (want timeout)", p.r0);
|
|
CHECK(p.r == KERN_R_CANCELLED, "blocking wait after the poll: %#x (want cancelled)", p.r);
|
|
CHECK(q.r0 == 0 && q.idx0 == 0, "wait on a signalled event with a cancel pending: %#x (want success)", q.r0);
|
|
CHECK(q.r == KERN_R_CANCELLED, "blocking wait after the signalled wait: %#x (want cancelled)", q.r);
|
|
|
|
uint32_t sw = kern_event_new(), sr = kern_event_readable(sw);
|
|
CHECK(wait1(c, sr, 0) == KERN_R_TIMEOUT, "service event before the signal");
|
|
kern_event_signal(sw);
|
|
CHECK(wait1(c, sr, 0) == 0, "service event after kern_event_signal");
|
|
uint32_t handles[] = {sr, sw, hr2, hw2, hr, hw};
|
|
for (unsigned i = 0; i < 6; i++) CHECK(sv(c, 0x16, handles[i], 0, 0, 0) == 0, "CloseHandle %#x", handles[i]);
|
|
CHECK(sv(c, 0x16, hr, 0, 0, 0) == KERN_R_BAD_HANDLE, "CloseHandle twice");
|
|
printf("events: signal, clear, reset, two handles, cancel while waiting and before, a pending cancel kept by a poll "
|
|
"and a signalled wait, service events, 10 ms "
|
|
"timeout after %.2f ms: %s\n", ms, failures == before ? "ok" : "FAIL");
|
|
}
|
|
|
|
// ---- virtual cores: count the threads running guest code at once, and that no two of them share a core number
|
|
static volatile int32_t g_running, g_run_max, g_core_use[3], g_core_clash;
|
|
|
|
static void core_worker(Cpu *c) {
|
|
for (int i = 0; i < CORE_RUNS; i++) {
|
|
uint32_t core = sv(c, 0x10, 0, 0, 0, 0);
|
|
if (core > 2 || plat_inc32(&g_core_use[core > 2 ? 0 : core]) != 1) plat_inc32(&g_core_clash);
|
|
long n = plat_inc32(&g_running), m;
|
|
while (n > (m = g_run_max) && plat_cas32(&g_run_max, n, m) != m) {}
|
|
uint64_t until = kern_now_ns() + 20000;
|
|
while (kern_now_ns() < until) {}
|
|
plat_dec32(&g_running);
|
|
plat_dec32(&g_core_use[core > 2 ? 0 : core]);
|
|
sv(c, 0x0B, i % 8 == 7 ? 50000 : 0, 0, 0, 0);
|
|
}
|
|
exit_thread(c);
|
|
}
|
|
|
|
static void test_cores(Cpu *c) {
|
|
uint32_t h[CORE_WORKERS];
|
|
g_running = g_run_max = g_core_clash = 0;
|
|
kern_core_stats_reset();
|
|
for (int i = 0; i < CORE_WORKERS; i++) {
|
|
h[i] = create(c, a_core_worker, NULL, 44, -2);
|
|
start(c, h[i]);
|
|
}
|
|
for (int i = 0; i < CORE_WORKERS; i++) join(c, h[i]);
|
|
int now, held_max;
|
|
kern_core_stats(&now, &held_max);
|
|
uint32_t cpus = plat_cpu_usable();
|
|
int gate = !cfg_bool("TOTK_FREE_CORES"); // the kernel's own switch
|
|
int ok = g_run_max <= 3 && held_max <= 3 && !g_core_clash && (cpus < 4 || g_run_max == 3);
|
|
printf("virtual cores: %d threads x %d runs on %u host CPUs: at most %d running guest code at once, at most %d "
|
|
"core holders (kernel count), %d core-number clashes: %s\n", CORE_WORKERS, CORE_RUNS, cpus, g_run_max,
|
|
held_max, g_core_clash, !gate ? "not checked (TOTK_FREE_CORES is set)" : ok ? "ok" : "FAIL");
|
|
CHECK(ok || !gate, "virtual cores");
|
|
}
|
|
|
|
// ---- fibers: two fibers switch to each other; each checks its callee-saved registers, sp and tpidr survive
|
|
typedef struct { uint64_t self, other; int id; } FiberJob;
|
|
static volatile int32_t g_switches, g_runs[3];
|
|
static volatile int g_last_fiber;
|
|
static long g_fiber_bad, g_fiber_turns;
|
|
|
|
static void set_regs(Cpu *c, uint64_t seed, uint64_t *save, __uint128_t *vsave) {
|
|
for (int r = 19; r <= 28; r++) save[r - 19] = c->x[r] = seed * 0x9E3779B97F4A7C15ull + (uint64_t)r;
|
|
for (int r = 8; r <= 15; r++)
|
|
vsave[r - 8] = c->v[r] = ((__uint128_t)(seed ^ 0xABCDull) << 64) | (seed * 31u + (uint64_t)r);
|
|
}
|
|
|
|
static int same_regs(const Cpu *c, const uint64_t *save, const __uint128_t *vsave) {
|
|
for (int r = 19; r <= 28; r++)
|
|
if (c->x[r] != save[r - 19]) return 0;
|
|
for (int r = 8; r <= 15; r++)
|
|
if (c->v[r] != vsave[r - 8]) return 0;
|
|
return 1;
|
|
}
|
|
|
|
static void fiber_fn(Cpu *c) {
|
|
FiberJob *j = arg_p(c, 0);
|
|
uint64_t save[10];
|
|
__uint128_t vsave[8];
|
|
for (;;) {
|
|
long n = g_switches;
|
|
if (n >= FIBER_SWITCHES) {
|
|
ret_x(c, 0); // back to the thread
|
|
return;
|
|
}
|
|
g_switches = n + 1;
|
|
g_runs[j->id]++;
|
|
set_regs(c, (uint64_t)n * 2 + (uint64_t)j->id, save, vsave);
|
|
uint64_t sp = c->sp, tp = c->tpidr, tlr = c->tpidrro;
|
|
g_last_fiber = j->id;
|
|
rt_call(c, a_fib_switch, 1, j->other);
|
|
if (g_last_fiber != 3 - j->id) g_fiber_turns++; // the other fiber did not run in between
|
|
if (!same_regs(c, save, vsave) || c->sp != sp || c->tpidr != tp || c->tpidrro != tlr) g_fiber_bad++;
|
|
if (rt_call(c, a_fib_cur, 0) != j->self) g_fiber_bad++;
|
|
}
|
|
}
|
|
|
|
static void test_fibers(Cpu *c) {
|
|
uint64_t fa = ga(mem_kernel_alloc(0x100, 16)), fb = ga(mem_kernel_alloc(0x100, 16));
|
|
uint64_t sa = ga(mem_kernel_alloc(STACK, 16)), sb = ga(mem_kernel_alloc(STACK, 16));
|
|
FiberJob ja = {fa, fb, 1}, jb = {fb, fa, 2};
|
|
rt_call(c, a_fib_init, 6, fa, a_fiber_fn, ga(&ja), sa, (uint64_t)STACK, (uint64_t)0);
|
|
rt_call(c, a_fib_init, 6, fb, a_fiber_fn, ga(&jb), sb, (uint64_t)STACK, (uint64_t)0);
|
|
g_switches = g_runs[1] = g_runs[2] = 0;
|
|
g_fiber_bad = g_fiber_turns = 0;
|
|
uint64_t save[10];
|
|
__uint128_t vsave[8];
|
|
set_regs(c, 0x5555, save, vsave);
|
|
uint64_t sp = c->sp, tp = c->tpidr;
|
|
uint64_t t0 = kern_now_ns();
|
|
rt_call(c, a_fib_switch, 1, fa);
|
|
double ms = ms_since(t0);
|
|
int base_ok = same_regs(c, save, vsave) && c->sp == sp && c->tpidr == tp && rt_call(c, a_fib_cur, 0) == 0;
|
|
rt_call(c, a_fib_fin, 1, fa);
|
|
rt_call(c, a_fib_fin, 1, fb);
|
|
int ok = g_switches == FIBER_SWITCHES && g_runs[1] == FIBER_SWITCHES / 2 && g_runs[2] == FIBER_SWITCHES / 2 &&
|
|
!g_fiber_turns && !g_fiber_bad && base_ok;
|
|
printf("fibers: %d switches (fiber 1 %d, fiber 2 %d, %ld out of turn) in %.1f ms (%.3f us each), %ld register "
|
|
"mismatches, thread context %s: %s\n", g_switches, g_runs[1], g_runs[2], g_fiber_turns, ms,
|
|
ms * 1000.0 / (double)(g_switches ? g_switches : 1), g_fiber_bad, base_ok ? "preserved" : "CHANGED",
|
|
ok ? "ok" : "FAIL");
|
|
CHECK(ok, "fiber ping-pong");
|
|
}
|
|
|
|
// ---- ExitThread unwinding: from three nested host calls deep, and from a fiber
|
|
typedef struct { int stage, depth, bad; uint64_t fiber, stack; } ExitJob;
|
|
|
|
static void exit_nested(Cpu *c) {
|
|
ExitJob *j = arg_p(c, 0);
|
|
if (++j->depth < 3) {
|
|
rt_call(c, a_exit_nested, 1, ga(j));
|
|
j->bad = 1;
|
|
return;
|
|
}
|
|
j->stage = 2;
|
|
sv(c, 0x0A, 0, 0, 0, 0);
|
|
j->bad = 2;
|
|
}
|
|
|
|
static void exit_entry(Cpu *c) {
|
|
ExitJob *j = arg_p(c, 0);
|
|
j->stage = 1;
|
|
rt_call(c, a_exit_nested, 1, ga(j));
|
|
j->bad = 3;
|
|
exit_thread(c);
|
|
}
|
|
|
|
static void fiber_exit_fn(Cpu *c) {
|
|
ExitJob *j = arg_p(c, 0);
|
|
j->stage = 3;
|
|
sv(c, 0x0A, 0, 0, 0, 0);
|
|
j->bad = 4;
|
|
}
|
|
|
|
static void fiber_exit_thread(Cpu *c) {
|
|
ExitJob *j = arg_p(c, 0);
|
|
rt_call(c, a_fib_init, 6, j->fiber, a_fiber_exit_fn, ga(j), j->stack, (uint64_t)STACK, (uint64_t)0);
|
|
rt_call(c, a_fib_switch, 1, j->fiber);
|
|
j->bad = 5;
|
|
exit_thread(c);
|
|
}
|
|
|
|
static void test_exit(Cpu *c) {
|
|
ExitJob a = {0};
|
|
uint32_t h = create(c, a_exit_entry, &a, 44, -2);
|
|
start(c, h);
|
|
join(c, h);
|
|
CHECK(wait1(c, h, 0) == KERN_R_BAD_HANDLE, "wait on a closed thread handle");
|
|
ExitJob b = {.fiber = ga(mem_kernel_alloc(0x100, 16)), .stack = ga(mem_kernel_alloc(STACK, 16))};
|
|
h = create(c, a_fiber_exit_thread, &b, 44, -2);
|
|
start(c, h);
|
|
join(c, h);
|
|
rt_call(c, a_fib_fin, 1, b.fiber);
|
|
int ok = a.stage == 2 && a.depth == 3 && !a.bad && b.stage == 3 && !b.bad;
|
|
printf("ExitThread: unwound from %d nested host calls (stage %d) and from a fiber (stage %d), nothing ran after "
|
|
"it: %s\n", a.depth, a.stage, b.stage, ok ? "ok" : "FAIL");
|
|
CHECK(ok, "ExitThread unwinding (bad %d %d)", a.bad, b.bad);
|
|
}
|
|
|
|
// ---- lifted code: main's PLT stub for nn::os::GetSystemTick -> lifted sdk -> rt_ticks
|
|
typedef struct { uint64_t first, last; long bad; } TickJob;
|
|
|
|
static void tick_worker(Cpu *c) {
|
|
TickJob *j = arg_p(c, 0);
|
|
uint64_t prev = 0;
|
|
for (int i = 0; i < TICK_CALLS; i++) {
|
|
uint64_t t = rt_call(c, tick_stub, 0);
|
|
if (!t || t < prev) j->bad++;
|
|
if (!i) j->first = t;
|
|
prev = t;
|
|
}
|
|
j->last = prev;
|
|
exit_thread(c);
|
|
}
|
|
|
|
static void test_lifted_ticks(Cpu *c) {
|
|
TickJob j[TICK_THREADS] = {0};
|
|
uint32_t h[TICK_THREADS];
|
|
uint64_t calls0 = hostfn_calls(), before = rt_ticks();
|
|
for (int i = 0; i < TICK_THREADS; i++) h[i] = create(c, a_tick_worker, &j[i], 44, -2);
|
|
for (int i = 0; i < TICK_THREADS; i++) start(c, h[i]);
|
|
for (int i = 0; i < TICK_THREADS; i++) join(c, h[i]);
|
|
uint64_t after = rt_ticks(), calls = hostfn_calls() - calls0;
|
|
char stub[128], def[128];
|
|
printf("lifted GetSystemTick: %d guest threads x %d calls of %s -> %s:\n", TICK_THREADS, TICK_CALLS,
|
|
ldr_describe(tick_stub, stub, sizeof stub), ldr_describe(ldr_symbol(TICK_SYMBOL), def, sizeof def));
|
|
int ok = 1;
|
|
for (int i = 0; i < TICK_THREADS; i++) {
|
|
int in = j[i].first >= before && j[i].last <= after && j[i].last > j[i].first;
|
|
printf(" thread %d: ticks %llu .. %llu, %ld out of order, inside the host's rt_ticks window: %s\n", i,
|
|
(unsigned long long)j[i].first, (unsigned long long)j[i].last, j[i].bad, in ? "yes" : "NO");
|
|
ok &= !j[i].bad && in;
|
|
}
|
|
// Host functions entered by dispatch meanwhile: only the four thread entries, so no call reached a host override.
|
|
int lifted = calls == TICK_THREADS;
|
|
printf(" monotonic per thread: %s; host-function calls meanwhile %llu (the %d thread entries): %s\n",
|
|
ok ? "ok" : "FAIL", (unsigned long long)calls, TICK_THREADS, lifted ? "all ticks came from lifted code"
|
|
: "FAIL");
|
|
CHECK(ok && lifted, "lifted GetSystemTick");
|
|
}
|
|
|
|
// svc 0x07 ExitProcess: the path up to the final ExitProcess (logged, pipeline cache saved, logs flushed) runs with the
|
|
// test hook in its place, which must see exit code 0; the SVC then returns to the probe.
|
|
static volatile int32_t exit_calls;
|
|
static volatile unsigned exit_code = 99;
|
|
static void exit_hook(unsigned code) { exit_code = code, exit_calls++; }
|
|
static void test_exit_process(Cpu *c) {
|
|
svc_test_exit_hook = exit_hook;
|
|
sv(c, 0x07, 0, 0, 0, 0);
|
|
svc_test_exit_hook = NULL;
|
|
CHECK(exit_calls == 1 && exit_code == 0, "ExitProcess reached the exit %d times with code %u", exit_calls, exit_code);
|
|
printf("ExitProcess: exit path reached once with code %u\n", exit_code);
|
|
}
|
|
|
|
static void probe_main(Cpu *c) {
|
|
Cpu *again;
|
|
uint32_t me = kern_main_thread(&again);
|
|
uint64_t mb, ms;
|
|
kern_main_stack(&mb, &ms);
|
|
printf("main thread %#x: prio 44 core 0, guest stack %#llx+%#llx, sp %#llx, TLR %#llx (+0x1F8 -> %#llx)\n", me,
|
|
(unsigned long long)mb, (unsigned long long)ms, (unsigned long long)c->sp, (unsigned long long)c->tpidrro,
|
|
(unsigned long long)LD64(c->tpidrro + 0x1F8));
|
|
CHECK(again == c && c->sp == mb + ms && LD64(c->tpidrro + 0x1F8) == c->tpidrro + 0x200, "main thread set-up");
|
|
CHECK(sv(c, 0x25, 0, KERN_H_CURRENT_THREAD, 0, 0) == 0 && c->x[1] == 0x51, "GetThreadId(current) %#llx",
|
|
(unsigned long long)c->x[1]);
|
|
test_lock(c, me);
|
|
test_cv_wake_order(c);
|
|
test_cv_key_order(c);
|
|
test_arbiter(c);
|
|
test_events(c);
|
|
test_cores(c);
|
|
test_fibers(c);
|
|
test_exit(c);
|
|
test_lifted_ticks(c);
|
|
test_exit_process(c);
|
|
}
|
|
|
|
// ---- platform layer (platform/win32.c): a thread handing values over a condition variable, and plat_sleep_ns(1 ms)
|
|
// overshoot (median of 21 under 0.5 ms; frame pacing depends on it).
|
|
static PlatMutex g_pm = PLAT_MUTEX_INIT;
|
|
static PlatCond g_pc = PLAT_COND_INIT;
|
|
static int g_pv;
|
|
|
|
static void plat_worker(void *arg) {
|
|
plat_mutex_lock(&g_pm);
|
|
g_pv = (int)(intptr_t)arg;
|
|
plat_cond_broadcast(&g_pc);
|
|
while (g_pv != 2) plat_cond_wait(&g_pc, &g_pm);
|
|
g_pv = 3;
|
|
plat_mutex_unlock(&g_pm);
|
|
}
|
|
|
|
static int cmp_u64(const void *a, const void *b) {
|
|
uint64_t x = *(const uint64_t *)a, y = *(const uint64_t *)b;
|
|
return x < y ? -1 : x > y;
|
|
}
|
|
|
|
// A refused commit waits and retries (memory.c commit, oom.c rt_oom_wait): three forced refusals, then the page maps.
|
|
static void test_oom_commit(void) {
|
|
const RtGuestLayout *g = mem_layout();
|
|
uint64_t a = g->alias.base + g->alias.size - 0x10000;
|
|
rt_oom_force(RT_OOM_COMMIT, 3);
|
|
uint64_t t0 = plat_now_ms();
|
|
uint32_t r = mem_map_physical(a, 0x1000);
|
|
uint64_t ms = plat_now_ms() - t0;
|
|
CHECK(r == 0 && rt_oom_forced_left(RT_OOM_COMMIT) == 0 && ms >= 20,
|
|
"MapPhysicalMemory after 3 refused commits: result %#x, %d refusals left, %llu ms", r,
|
|
rt_oom_forced_left(RT_OOM_COMMIT), (unsigned long long)ms);
|
|
rt_oom_force(RT_OOM_COMMIT, 0);
|
|
if (r == 0) {
|
|
*(volatile uint64_t *)(uintptr_t)a = 1;
|
|
CHECK(mem_unmap_physical(a, 0x1000) == 0, "UnmapPhysicalMemory after the refused commits");
|
|
}
|
|
printf("oom: commit waited %llu ms through 3 refusals\n", (unsigned long long)ms);
|
|
}
|
|
|
|
static void test_platform(void) {
|
|
PlatThread *t = plat_thread_new(plat_worker, (void *)(intptr_t)1, "probe-kernel plat");
|
|
CHECK(t, "plat_thread_new");
|
|
if (t) {
|
|
plat_mutex_lock(&g_pm);
|
|
int timed_out = 0;
|
|
while (g_pv != 1 && !timed_out) timed_out = !plat_cond_wait_ns(&g_pc, &g_pm, 5000000000ull);
|
|
CHECK(g_pv == 1, "plat thread did not start (%d)", g_pv);
|
|
g_pv = 2;
|
|
plat_cond_broadcast(&g_pc);
|
|
plat_mutex_unlock(&g_pm);
|
|
plat_thread_join(t);
|
|
CHECK(g_pv == 3, "plat thread did not finish (%d)", g_pv);
|
|
}
|
|
PlatMutex m = PLAT_MUTEX_INIT;
|
|
PlatCond c = PLAT_COND_INIT;
|
|
plat_mutex_lock(&m);
|
|
uint64_t t0 = plat_now_ns();
|
|
CHECK(plat_cond_wait_ns(&c, &m, 2000000) == 0 && plat_now_ns() - t0 >= 1000000, "plat_cond_wait_ns timeout");
|
|
plat_mutex_unlock(&m);
|
|
CHECK(plat_mutex_trylock(&m), "plat_mutex_trylock");
|
|
plat_mutex_unlock(&m);
|
|
|
|
enum { N = 21 };
|
|
uint64_t over[N];
|
|
for (int i = 0; i < N; i++) {
|
|
uint64_t a = plat_now_ns();
|
|
plat_sleep_ns(1000000);
|
|
uint64_t d = plat_now_ns() - a;
|
|
CHECK(d >= 1000000, "plat_sleep_ns(1 ms) returned after %llu ns", (unsigned long long)d);
|
|
over[i] = d > 1000000 ? d - 1000000 : 0;
|
|
}
|
|
qsort(over, N, sizeof *over, cmp_u64);
|
|
printf(" plat_sleep_ns(1 ms) overshoot: median %.3f ms, max %.3f ms\n", over[N / 2] / 1e6, over[N - 1] / 1e6);
|
|
CHECK(over[N / 2] < 500000, "plat_sleep_ns(1 ms) median overshoot %.3f ms (want < 0.5)", over[N / 2] / 1e6);
|
|
}
|
|
|
|
// ---- crash report (main.c crash_handler): a child faults on a guest thread with known values in rbx and x19; the
|
|
// report must hold the host registers, the faulting instruction's bytes and the memory both values point at.
|
|
#define CRASH_CHILD_ENV "TOTK_PROBE_KERNEL_CRASH_CHILD"
|
|
static uint64_t crash_host_words[6] = {0, 0, 0x5ca1ab1e0c0de001ull, 0, 0, 0};
|
|
static uint64_t crash_guest_words[6] = {0, 0, 0x5ca1ab1e0c0de002ull, 0, 0, 0};
|
|
|
|
// The compound shape note (main.c crash_compound_shape, 1.2.1): a shape of 3 records whose child 0 has no shape, child
|
|
// 1 has one but its live bit is clear, child 2 is live and filled.
|
|
static uint64_t crash_shape[0x1a0 / 8], crash_records[3 * 0x78 / 8], crash_child_shape;
|
|
|
|
static void crash_shape_fill(Cpu *c) {
|
|
uint8_t *s = (uint8_t *)crash_shape, *r = (uint8_t *)crash_records;
|
|
static const char *const names[3] = {"PartA", "PartB", "PartC"};
|
|
for (int i = 0; i < 3; i++) {
|
|
uint64_t name = (uint64_t)(uintptr_t)(r + i * 0x78 + 0x14);
|
|
memcpy(r + i * 0x78 + 8, &name, 8);
|
|
memcpy(r + i * 0x78 + 0x14, names[i], strlen(names[i]) + 1);
|
|
}
|
|
uint64_t child = (uint64_t)(uintptr_t)&crash_child_shape, vt = MOD_MAIN_BASE + 0x45e1218;
|
|
uint64_t rec = (uint64_t)(uintptr_t)crash_records;
|
|
uint32_t n = 3, bits = 0x5;
|
|
memcpy(r + 0x78, &child, 8);
|
|
memcpy(r + 2 * 0x78, &child, 8);
|
|
memcpy(s, &vt, 8);
|
|
memcpy(s + 0x78, &n, 4);
|
|
memcpy(s + 0x80, &rec, 8);
|
|
memcpy(s + 0x88, &bits, 4);
|
|
memcpy(s + 0x190, &n, 4);
|
|
c->x[20] = (uint64_t)(uintptr_t)crash_shape;
|
|
c->x[21] = 0x1000038;
|
|
c->x[30] = MOD_MAIN_BASE + 0xe056d4;
|
|
}
|
|
|
|
static void crash_child_main(Cpu *c) {
|
|
const char *mode = getenv(CRASH_CHILD_ENV);
|
|
if (mode && !strcmp(mode, "shape")) crash_shape_fill(c);
|
|
c->x[19] = (uint64_t)(uintptr_t)&crash_guest_words[2];
|
|
c->sp -= 0x100; // a frame on the guest stack, as a crashing function has
|
|
__asm__ volatile("movq %0, %%rbx\n\tmovq 0x10, %%rax" : : "r"(&crash_host_words[2]) : "rax", "rbx", "memory");
|
|
}
|
|
|
|
// The report line that holds needle, NULL when none does
|
|
static const char *report_line(const char *out, const char *needle, char *line, size_t cap) {
|
|
const char *p = strstr(out, needle);
|
|
if (!p) return NULL;
|
|
while (p > out && p[-1] != '\n') p--;
|
|
size_t n = strcspn(p, "\r\n");
|
|
snprintf(line, cap, "%.*s", (int)(n < cap ? n : cap - 1), p);
|
|
return line;
|
|
}
|
|
|
|
static void test_crash_report(void) {
|
|
static char out[65536];
|
|
char line[512], want[64];
|
|
int code = plat_run_self(CRASH_CHILD_ENV, "1", out, sizeof out, 30000);
|
|
CHECK(code == 71 && strstr(out, "FATAL: access violation") && strstr(out, "reading address 0x10"),
|
|
"crash child: exit code %d, no access violation at 0x10 reported", code);
|
|
snprintf(want, sizeof want, "rbx %016llx", (unsigned long long)(uintptr_t)&crash_host_words[2]);
|
|
CHECK(strstr(out, want), "crash report: no host register line with %s", want);
|
|
CHECK(report_line(out, "host code at rip-", line, sizeof line) && strstr(line, "| 48 8b 04 25 10 00 00 00"),
|
|
"crash report: the faulting instruction's bytes are missing");
|
|
const char *h = report_line(out, "*5ca1ab1e0c0de001", line, sizeof line);
|
|
CHECK(h && strstr(h, "rbx"), "crash report: no memory line for rbx (%s)", h ? h : "none");
|
|
const char *g = report_line(out, "*5ca1ab1e0c0de002", line, sizeof line);
|
|
CHECK(g && strstr(g, "x19"), "crash report: no memory line for x19 (%s)", g ? g : "none");
|
|
CHECK(strstr(out, "guest stack sp+0x00:"), "crash report: no guest stack words");
|
|
CHECK(report_line(out, " memory commit: this process ", line, sizeof line) && !strstr(line, "process -1 MB"),
|
|
"crash report: no memory commit line");
|
|
printf("crash report: exit code %d, host registers, code bytes, memory at rbx and x19, guest stack, commit %s\n", code,
|
|
failures ? "checked with failures" : "present");
|
|
if (rt_game_version != 121) return;
|
|
code = plat_run_self(CRASH_CHILD_ENV, "shape", out, sizeof out, 30000);
|
|
CHECK(report_line(out, " compound shape ", line, sizeof line) && strstr(line, "3 children, 3 records") &&
|
|
strstr(line, "live bits: 00000005"),
|
|
"crash report: no compound shape line (%s)", strstr(out, "compound shape") ? line : "none");
|
|
CHECK(report_line(out, "child 0:", line, sizeof line) && strstr(line, "shape 0 \"PartA\""),
|
|
"crash report: child 0 should have no shape (%s)", line);
|
|
CHECK(report_line(out, "child 1:", line, sizeof line) && strstr(line, "(off) \"PartB\""),
|
|
"crash report: child 1 should be off (%s)", line);
|
|
CHECK(report_line(out, "child 2:", line, sizeof line) && !strstr(line, "(off)") && strstr(line, "\"PartC\""),
|
|
"crash report: child 2 should be live (%s)", line);
|
|
printf("crash report, compound shape: exit code %d, %s\n", code, failures ? "checked with failures" : "children listed");
|
|
}
|
|
|
|
int probe_kernel(void) {
|
|
a_lock_waiter = hostfn_register("probe-kernel lock waiter", lock_waiter);
|
|
a_cv_waiter = hostfn_register("probe-kernel condvar waiter", cv_waiter);
|
|
a_cv_order_waiter = hostfn_register("probe-kernel condvar order waiter", cv_order_waiter);
|
|
a_arb_waiter = hostfn_register("probe-kernel arbiter waiter", arb_waiter);
|
|
a_event_waiter = hostfn_register("probe-kernel event waiter", event_waiter);
|
|
a_core_worker = hostfn_register("probe-kernel core worker", core_worker);
|
|
a_exit_entry = hostfn_register("probe-kernel exit entry", exit_entry);
|
|
a_exit_nested = hostfn_register("probe-kernel exit nested", exit_nested);
|
|
a_fiber_fn = hostfn_register("probe-kernel fiber", fiber_fn);
|
|
a_fiber_exit_thread = hostfn_register("probe-kernel fiber exit thread", fiber_exit_thread);
|
|
a_fiber_exit_fn = hostfn_register("probe-kernel fiber exit", fiber_exit_fn);
|
|
a_tick_worker = hostfn_register("probe-kernel tick worker", tick_worker);
|
|
a_fib_init = hostfn_register(KERN_SYM_INITIALIZE_FIBER, kern_fiber_initialize); // the loader's addresses
|
|
a_fib_fin = hostfn_register(KERN_SYM_FINALIZE_FIBER, kern_fiber_finalize);
|
|
a_fib_switch = hostfn_register(KERN_SYM_SWITCH_TO_FIBER, kern_fiber_switch);
|
|
a_fib_cur = hostfn_register(KERN_SYM_GET_CURRENT_FIBER, kern_fiber_current);
|
|
ldr_load(rt_config()->exefs);
|
|
tick_stub = ldr_plt_stub("main", TICK_SYMBOL);
|
|
if (!tick_stub || !ldr_symbol(TICK_SYMBOL)) {
|
|
printf("probe-kernel: FAILED (no PLT stub for %s in main)\n", TICK_SYMBOL);
|
|
return 1;
|
|
}
|
|
const char *child = getenv(CRASH_CHILD_ENV);
|
|
if (child && *child) {
|
|
kern_run_on_guest_main(crash_child_main);
|
|
printf("crash child: returned without a fault\n");
|
|
return 1;
|
|
}
|
|
test_platform();
|
|
test_oom_commit();
|
|
test_crash_report();
|
|
kern_run_on_guest_main(probe_main);
|
|
RtStats s = rt_stats();
|
|
printf("stats: %llu SVCs, %llu threads, %llu host calls\n", (unsigned long long)s.svcs,
|
|
(unsigned long long)s.threads, (unsigned long long)s.host_calls);
|
|
if (failures) {
|
|
printf("probe-kernel: FAILED (%d failures)\n", failures);
|
|
return 1;
|
|
}
|
|
printf("probe-kernel: passed\n");
|
|
return 0;
|
|
}
|