diff --git a/dm/host/hle_svc.c b/dm/host/hle_svc.c index 4e1e061..f934829 100644 --- a/dm/host/hle_svc.c +++ b/dm/host/hle_svc.c @@ -147,6 +147,7 @@ uint64_t svc_dispatch(int num, uint64_t *regs, uint64_t orig_addr) { int rc = 0; if (size > g_heap_cur) rc = mprotect(g_heap + g_heap_cur, size - g_heap_cur, PROT_READ | PROT_WRITE); else if (size < g_heap_cur) rc = mprotect(g_heap + size, g_heap_cur - size, PROT_NONE); + if (rc) die("SetHeapSize mprotect failed: req=%#zx errno=%d", size, errno); if (g_flag.svc) LOG("SetHeapSize %#zx -> base %p mprotect=%d errno=%d", size, g_heap, rc, errno); else if (size > g_heap_cur) LOG("[mem] heap %.2f GB (heap region %.2f GB)", size / 1073741824.0, g_heap_cap / 1073741824.0); g_heap_cur = size; diff --git a/dm/host/tests/Makefile b/dm/host/tests/Makefile index 4b46e2d..0ad6110 100644 --- a/dm/host/tests/Makefile +++ b/dm/host/tests/Makefile @@ -4,7 +4,8 @@ B = build # tests that use the game profile need GAME= (there is no default game); GINC only expands when used, so tests without the profile build as before GINC = $(if $(strip $(GAME)),-I../gen/$(GAME) -I../../games/$(GAME),$(error GAME is not set: make GAME=: $(shell python3 ../../games/game.py list | awk -F'\t' '{printf "%s (%s) ", $$1, $$3}'))) GAME_H = ../gen/$(GAME)/game_profile.h -run: $(B)/t_touch $(B)/t_gesture $(B)/t_oe $(B)/t_fs $(B)/t_fs_commit_list $(B)/t_thread_leak $(B)/t_fs_case $(B)/t_audio $(B)/t_audio_spsc $(B)/t_swkbd $(B)/t_altstack $(B)/t_condvar $(B)/t_condvar_old $(B)/t_pctl $(B)/t_time $(B)/t_shared_font $(B)/t_vorbis $(B)/t_vorbis_divides $(B)/t_hid $(B)/t_haptics $(B)/t_haptics_apple +run: $(B)/t_heap_size $(B)/t_touch $(B)/t_gesture $(B)/t_oe $(B)/t_fs $(B)/t_fs_commit_list $(B)/t_thread_leak $(B)/t_fs_case $(B)/t_audio $(B)/t_audio_spsc $(B)/t_swkbd $(B)/t_altstack $(B)/t_condvar $(B)/t_condvar_old $(B)/t_pctl $(B)/t_time $(B)/t_shared_font $(B)/t_vorbis $(B)/t_vorbis_divides $(B)/t_hid $(B)/t_haptics $(B)/t_haptics_apple + $(B)/t_heap_size $(B)/t_touch $(B)/t_gesture $(B)/t_oe 2>/dev/null @@ -35,6 +36,11 @@ nro: $(B)/t_nro $(B)/t_nro $(GAME_H): $(MAKE) -C .. game_profile GAME=$(GAME) +$(B)/t_heap_size: t_heap_size.c ../hle_svc.c ../dm.h $(GAME_H) | $(B) + clang -O1 -Wall -Werror -Wno-unused-function -fsanitize=undefined -I.. $(GINC) -o $@ t_heap_size.c +.PHONY: heap_size +heap_size: $(B)/t_heap_size + $(B)/t_heap_size $(B)/t_touch: t_touch.c ../hle_touch.c ../front.h ../dm.h $(GAME_H) | $(B) clang -O1 -Wno-unused-function -I.. $(GINC) -o $@ t_touch.c ../hle_touch.c $(B)/t_gesture: t_gesture.c ../hle_touch.c ../front.h ../dm.h $(GAME_H) | $(B) diff --git a/dm/host/tests/t_heap_size.c b/dm/host/tests/t_heap_size.c new file mode 100644 index 0000000..47722c9 --- /dev/null +++ b/dm/host/tests/t_heap_size.c @@ -0,0 +1,120 @@ +// Exercise the production SVC dispatcher, including its fatal path, without game files. +#include "../dm.h" +#include +#include +#include +#include +#include +#include + +static int heap_mprotect(void *addr, size_t size, int prot); +static _Noreturn void heap_abort(void); +// Substitution is confined to this translation unit; production needs no test API. +#define mprotect heap_mprotect +#define abort heap_abort +#ifndef HLE_SVC_SOURCE +#define HLE_SVC_SOURCE "../hle_svc.c" +#endif +#include HLE_SVC_SOURCE +#undef abort +#undef mprotect + +module_t g_mod[MAX_MOD]; +int g_nmod; +struct dm_flags g_flag; +void hlecount_svc(int num) { (void)num; } +int svc_thread(int num, uint64_t *regs) { (void)num; (void)regs; return 0; } +module_t *module_of(const void *p) { (void)p; return NULL; } +void dm_backtrace(uint64_t fp) { (void)fp; } +void guest_clock_read_wait(void) {} +uint64_t guest_ns(void) { return 0; } +uint64_t dm_entropy64(void) { return 0; } +int thr_limit_used(void) { return 0; } + +static size_t page, old_size, requested; +static int calls, injected_error, last_prot; +static volatile sig_atomic_t abort_validated; +static void *last_addr; +static size_t last_size; +static uint64_t regs[31]; +static const uint64_t sentinel = 0xabcdef1234567890ull; + +static int heap_mprotect(void *addr, size_t size, int prot) { + calls++; last_addr = addr; last_size = size; last_prot = prot; + // The production state must not be published before the host operation. + assert(g_heap_cur == old_size && regs[0] == sentinel && regs[1] == requested); + if (injected_error) { errno = injected_error; return -1; } + return mprotect(addr, size, prot); +} +static int correct_call(void) { + int grow = requested > old_size; + return calls == 1 && last_addr == g_heap + (grow ? old_size : requested) && + last_size == (grow ? requested - old_size : old_size - requested) && + last_prot == (grow ? PROT_READ | PROT_WRITE : PROT_NONE); +} +static void aborted(int sig) { _exit(sig == SIGABRT && abort_validated ? 77 : 78); } +static _Noreturn void heap_abort(void) { + // Observe the exact state at die(), before letting the real abort run. + if (!injected_error || errno != injected_error || !correct_call() || + g_heap_cur != old_size || regs[0] != sentinel || regs[1] != requested || + !dm_guest_heap_readable((uint64_t)g_heap, old_size) || + dm_guest_heap_readable((uint64_t)g_heap, old_size + 1)) _exit(78); + abort_validated = 1; // An unrelated assertion abort must not pass the failure test. + abort(); +} +static void request(size_t size) { + old_size = g_heap_cur; requested = size; calls = 0; + memset(regs, 0, sizeof regs); regs[0] = sentinel; regs[1] = size; + assert(svc_dispatch(0x01, regs, 0) == 0); +} +static void success(size_t size) { + injected_error = 0; + request(size); + assert(g_heap_cur == size && regs[0] == 0 && regs[1] == (uint64_t)g_heap); + assert(size == old_size ? calls == 0 : correct_call()); + assert(dm_guest_heap_readable((uint64_t)g_heap, size)); + assert(!dm_guest_heap_readable((uint64_t)g_heap, size + 1)); + if (size) { g_heap[0] = 0x5a; g_heap[size - 1] = 0xa5; } +} +static void failure(size_t size, int error) { + fflush(NULL); + pid_t p = fork(); assert(p >= 0); + if (!p) { + signal(SIGABRT, aborted); alarm(5); + injected_error = error; + request(size); + _exit(79); // Any return to the guest, even with untouched registers, is wrong. + } + int status; assert(waitpid(p, &status, 0) == p); + if (!WIFEXITED(status) || WEXITSTATUS(status) != 77) { + fprintf(stderr, "SetHeapSize failure did not preserve state and abort (status=%d)\n", status); + exit(1); + } +} +static void used_memory(size_t heap_size) { + uint64_t r[31] = {0, I_UsedMem}; + assert(svc_dispatch(0x29, r, 0) == 0); + assert(r[0] == 0 && r[1] == heap_size + DM_GAME_MEM_USED); +} +int main(void) { + page = (size_t)getpagesize(); g_heap_cap = 4 * page; + assert(g_heap_cap < APP_POOL); + g_heap = mmap(NULL, g_heap_cap, PROT_NONE, MAP_PRIVATE | MAP_ANON, -1, 0); + assert(g_heap != MAP_FAILED); + g_as = g_heap; // Skip the multi-GB profile reservation; use only these synthetic pages. + success(2 * page); used_memory(2 * page); // initial growth + success(3 * page); used_memory(3 * page); // further growth + success(page); used_memory(page); // shrink + injected_error = EACCES; errno = EACCES; request(page); // unchanged: no mprotect despite stale errno + assert(calls == 0 && g_heap_cur == page && regs[0] == 0 && regs[1] == (uint64_t)g_heap); + failure(3 * page, ENOMEM); failure(3 * page, EACCES); // failed growth + success(3 * page); // successful retry from the unchanged parent state + failure(page, ENOMEM); failure(page, EACCES); // failed shrink + failure(0, EACCES); // failed release of the whole heap + success(0); used_memory(0); + success(0); // unchanged empty heap + success(page); success(0); // regrowth after release + assert(munmap(g_heap, g_heap_cap) == 0); + puts("SetHeapSize production dispatcher: growth, shrink, unchanged, zero, failure state and fail-fast passed"); + return 0; +}