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:
1 parent
1fbbd1486a
commit
9ba44da80a
3 files changed
+128
-1
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user