Fail fast when SetHeapSize protection changes fail

Apply PR #127 and require the regression test's abort handler to observe a validated production fatal path. Unrelated assertion failures can no longer pass the injected-failure cases.

Co-authored-by: nocmt <nocmtone@gmail.com>
This commit is contained in:
seeddsandnocmt committed 2026-10-25 11:19:22 +08:00
1 parent 1fbbd1486a
commit 9ba44da80a
3 files changed
+128 -1

No files matched your search

+1
View File
@@ -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;
+7 -1
View File
@@ -4,7 +4,8 @@ B = build
# tests that use the game profile need GAME=<base game title ID> (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=<base game title ID>: $(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)
+120
View File
@@ -0,0 +1,120 @@
// Exercise the production SVC dispatcher, including its fatal path, without game files.
#include "../dm.h"
#include <assert.h>
#include <errno.h>
#include <signal.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>
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;
}