Files
NX2navi/tests/system_event_check.c
chrg6 d8c046acc4 Destroy a movie decoder's system events and free their table entries when the decoder goes.
The review of the system-event change (Ev-1) found the table still filling. Entries in host memory now count as live
for good in the full-table scan, but their objects are freed: ov_movie2.c ov_dtor freed the Video with in_ev and out_ev
still bound, so each decoder left two dead entries that nothing reclaimed (AUDIT F-1-08 measured about two entries per
reload of a save, which E-4's verifier tied to the decoder pair), and their four kernel handles stayed open. A long
session would end with "kern_bind_system_event: table full" once no dead guest entry was left to take.

kernel.c gains kern_unbind_system_event(c, ev). On a guest thread it calls the SDK's nn::os::DestroySystemEvent, which
zeroes the state byte and closes both handles, and then removes the entry under g_se_lock (the last entry takes its
place). The destroy comes first, so a signal still in flight finds the event destroyed or its handle no longer naming it
and skips it, rather than finding no entry and binding again on a host thread. Off a guest thread, or when the SDK lacks
the function (logged once), only the entry goes. ov_dtor calls it for each bound event after finalize has joined the
decoder and convert threads, then frees the Video. net_offline.c frees its HostTask with a bound event in the same way;
that file is outside this change and is handed to the coordinator.

Not reproduced before the change in a game run. tests/system_event_check.c fills the table with live guest events and
the movie's two host-memory events, unbinds one: it leaves the table, its handles are closed and its state byte is 0,
the other keeps its entry, and a second unbind changes nothing; with the table full again and the last host entry
unbound, a new guest bind succeeds where it would have ended with "table full". The check passes in both modes,
movie_planes_check passes, and the compile check of every runtime file (host.h changed) passes, adding no warnings in
kernel.c or ov_movie2.c with -Wall -Wextra.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 18:41:46 -05:00

268 lines
12 KiB
C

// runtime/kernel.c: the system-event table (kern_bind_system_event, kern_signal_system_event, kern_clear_system_event)
// with the platform layer linked and the rest of the runtime stubbed. Built and run by tests/system_event_check.py;
// prints each failed check and exits 1 on any.
// (no argument) a kept handle the guest closed (DestroySystemEvent) is neither signalled nor cleared, also when its
// number went to an unrelated event or the state byte reads nonzero again; a host thread signalling
// while a guest thread destroys and binds the event again never ends the process; with the table full,
// a new bind takes a destroyed guest-memory entry and keeps the entries in host memory, which a host
// thread then still signals; kern_system_event_live tells live guest events from destroyed ones;
// kern_unbind_system_event destroys a host-memory event and frees its entry, so a full table takes a
// new guest bind once the host entries are unbound
// full every entry live: the next bind ends with "table full" (the script checks the exit code and text)
#include "kernel.c"
#include <stdarg.h>
static int fails;
#define CHECK(c, ...) do { if (!(c)) { fails++; printf("FAIL line %d: ", __LINE__); printf(__VA_ARGS__); printf("\n"); } } while (0)
// ---- the runtime kernel.c reaches
unsigned rt_trace;
uint64_t rt_guest_start_ns;
volatile uint32_t prof_replay_tid, prof_main_tid;
// Guest memory is this block; anything else (the "host-memory" events below) is not, as memory.c answers.
static uint8_t guest[600][0x40] __attribute__((aligned(16)));
int mem_readable(uint64_t a, uint64_t n) {
uint64_t lo = (uint64_t)(uintptr_t)guest, hi = lo + sizeof guest;
return a >= lo && a + n <= hi;
}
const char *cfg_get(const char *name) { (void)name; return NULL; }
const char *cfg_value(const char *name) { (void)name; return "0"; }
int cfg_bool(const char *name) { (void)name; return 0; }
int rt_ab(void) { return 0; }
int rt_knob(const char *name, const char *def) { (void)name; return strcmp(def, "0") != 0; }
int64_t frame_step_world_us(void) { return -1; }
void ipc_handle_closed(int kind, void *obj) { (void)kind, (void)obj; }
const char *ldr_describe(uint64_t a, char *buf, int n) { snprintf(buf, (size_t)n, "%#llx", (unsigned long long)a); return buf; }
// The SDK's functions the table calls: 1 CreateSystemEvent, 2 GetWritableHandleOfSystemEvent (kern_bind_system_event),
// 3 DestroySystemEvent (kern_unbind_system_event).
uint64_t ldr_symbol(const char *name) {
return strstr(name, "CreateSystemEvent") ? 1 : strstr(name, "DestroySystemEvent") ? 3 : 2;
}
void perf_main_thread(void) {}
void perf_size(PerfSizes *s, const char *name, uint64_t count, uint64_t limit, uint64_t bytes) {
(void)s, (void)name, (void)count, (void)limit, (void)bytes; // kern_sizes is not called here
}
int rt_linked_modules(RtModule *out, int max) { (void)out, (void)max; return 0; }
const RtGuestLayout *mem_layout(void) { return NULL; }
void *mem_kernel_alloc(uint64_t size, uint64_t align) {
void *p = plat_aligned_alloc(size, align < 64 ? 64 : align);
if (!p) rt_fatal("out of memory");
memset(p, 0, size);
return p;
}
void mem_kernel_free(void *p, uint64_t size) { (void)size; plat_aligned_free(p); }
void rt_log(const char *fmt, ...) { (void)fmt; }
void rt_logf(const char *fmt, ...) { (void)fmt; }
void rt_fatal(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
printf("FATAL: ");
vprintf(fmt, ap);
printf("\n");
va_end(ap);
fflush(stdout);
_Exit(3);
}
void rt_dispatch(Cpu *c) { (void)c; rt_fatal("rt_dispatch in the test"); }
void rt_svc(Cpu *c, uint32_t imm, uint64_t pc) { (void)pc; kern_svc(c, imm); }
static void destroy(void *p);
// CreateSystemEvent(ev, mode, ipc) makes a kernel event, keeps its writable handle at +0 of the SystemEventType and its
// readable one at +4, and sets the state byte; GetWritableHandleOfSystemEvent(ev) reads +0; DestroySystemEvent(ev) as
// destroy below.
uint64_t rt_call(Cpu *c, uint64_t fn, int n, ...) {
(void)c;
va_list ap;
va_start(ap, n);
uint64_t ev = va_arg(ap, uint64_t);
va_end(ap);
uint8_t *p = (uint8_t *)(uintptr_t)ev;
if (fn == 1) {
uint32_t wh = kern_event_new(), rh = kern_event_readable(wh);
memcpy(p, &wh, 4);
memcpy(p + 4, &rh, 4);
__atomic_store_n(p + SE_STATE, 1, __ATOMIC_RELEASE);
return 0;
}
if (fn == 3) {
destroy(p);
return 0;
}
uint32_t wh;
memcpy(&wh, p, 4);
return wh;
}
// ---- helpers
static RtThread me; // the main thread plays a guest thread for the binds
static Cpu cpu;
static uint64_t ev_of(void *p) { return (uint64_t)(uintptr_t)p; }
static uint32_t wh_of(const void *p) { uint32_t h; memcpy(&h, p, 4); return h; }
static uint32_t rh_of(const void *p) { uint32_t h; memcpy(&h, (const uint8_t *)p + 4, 4); return h; }
static void bind(void *p) { kern_bind_system_event(&cpu, ev_of(p), 0); }
// nn::os::DestroySystemEvent: the state byte first, then both handles (SVC 0x16).
static void destroy(void *p) {
__atomic_store_n((uint8_t *)p + SE_STATE, 0, __ATOMIC_RELEASE);
kern_handle_close(rh_of(p));
kern_handle_close(wh_of(p));
}
static int signaled(uint32_t wh) {
lock();
Event *e = h_peek(wh, O_EVENT_W);
int s = e ? e->signaled : -1;
unlock();
return s;
}
static void stale_and_reuse(void) {
uint8_t *ev = guest[0];
bind(ev);
uint32_t wh = wh_of(ev);
kern_signal_system_event(ev_of(ev));
CHECK(signaled(wh) == 1, "a live bound event is signalled");
kern_clear_system_event(ev_of(ev));
CHECK(signaled(wh) == 0, "a live bound event is cleared");
CHECK(kern_system_event_live(ev_of(ev)), "kern_system_event_live: a bound event in guest memory is live");
destroy(ev);
CHECK(!kern_system_event_live(ev_of(ev)), "kern_system_event_live: a destroyed event is not");
kern_signal_system_event(ev_of(ev)); // state byte 0: skipped before the kernel lock
// The byte reads nonzero again with the handles closed (the memory reused, or the SDK made the event again
// without the binder): the signal must neither end the process nor reach anything.
ev[SE_STATE] = 1;
kern_signal_system_event(ev_of(ev));
kern_clear_system_event(ev_of(ev));
// The closed number goes to the next new event at once: the stale entry must not set or clear it.
uint32_t other = kern_event_new();
CHECK(other == wh, "the next event takes the closed number (%#x, closed %#x)", other, wh);
kern_signal_system_event(ev_of(ev));
CHECK(signaled(other) == 0, "an unrelated event that took the closed number is not signalled by the stale entry");
lock();
((Event *)h_peek(other, O_EVENT_W))->signaled = 1;
unlock();
kern_clear_system_event(ev_of(ev));
CHECK(signaled(other) == 1, "an unrelated event that took the closed number is not cleared by the stale entry");
kern_handle_close(other);
// Bound again after the destroy (state byte 0 first, as the guest leaves it): signals again.
ev[SE_STATE] = 0;
bind(ev);
kern_signal_system_event(ev_of(ev));
CHECK(signaled(wh_of(ev)) == 1, "the event bound again is signalled");
destroy(ev);
}
// A host thread signals in a loop while the guest thread destroys the event and binds it again (the override called
// again after a DestroySystemEvent), as gfx_present.c vsync_main does with a vsync event.
static volatile int stop;
static volatile long signals;
static uint8_t *race_ev;
static void signaller(void *arg) {
(void)arg;
while (!stop) {
kern_signal_system_event(ev_of(race_ev));
kern_clear_system_event(ev_of(race_ev));
__atomic_add_fetch(&signals, 1, __ATOMIC_RELAXED);
}
}
static void race(void) {
race_ev = guest[1];
bind(race_ev);
void *t = plat_thread_new(signaller, NULL, "signaller");
uint64_t t0 = plat_now_ns();
long rounds = 0;
while (plat_now_ns() - t0 < 2000000000ull || rounds < 1000) {
destroy(race_ev);
bind(race_ev);
rounds++;
}
stop = 1;
plat_thread_join(t);
printf("race: %ld destroy and bind rounds against %ld signals, no fatal\n", rounds, signals);
destroy(race_ev);
}
// ov_movie2.c update_events on a movie decoder thread (a host thread).
static uint8_t *movie_in, *movie_out;
static void decoder(void *arg) {
(void)arg;
kern_signal_system_event(ev_of(movie_in));
kern_signal_system_event(ev_of(movie_out));
}
static void full_table(int all_live) {
int n = (int)(sizeof g_se / sizeof *g_se);
// the guest entries 0 and 1 of the checks above are destroyed; fill the rest of the table with live ones and the
// movie's two events, which live in host memory
for (int i = 2; i < n - 2; i++) bind(guest[i]);
movie_in = calloc(1, 0x40), movie_out = calloc(1, 0x40);
bind(movie_in);
bind(movie_out);
CHECK(g_nse == n, "the table is full (%d of %d)", g_nse, n);
if (all_live) {
bind(guest[0]); // the two destroyed guest entries are made again: every entry is live
bind(guest[1]);
printf("full: binding one more with every entry live\n");
bind(guest[n]);
printf("full: no fatal\n");
return;
}
bind(guest[n]);
CHECK(se_find(ev_of(movie_in), NULL) && se_find(ev_of(movie_out), NULL),
"the movie's host-memory events keep their entries (in %s, out %s)",
se_find(ev_of(movie_in), NULL) ? "kept" : "taken", se_find(ev_of(movie_out), NULL) ? "kept" : "taken");
CHECK(se_find(ev_of(guest[n]), NULL) != 0, "the new event is bound");
CHECK(se_find(ev_of(guest[0]), NULL) == 0, "it took the first destroyed guest entry");
bind(guest[n + 1]);
CHECK(se_find(ev_of(guest[1]), NULL) == 0, "the next takes the second destroyed guest entry");
void *t = plat_thread_new(decoder, NULL, "decoder");
plat_thread_join(t);
CHECK(signaled(wh_of(movie_in)) == 1 && signaled(wh_of(movie_out)) == 1,
"a host thread signals the movie's events with the table full");
}
// ov_movie2.c ov_dtor with the table full of live guest events and the movie's two host-memory events: each unbound
// event is destroyed (its handles closed) and leaves the table, the other keeps its entry, and once the last host
// entry is gone a new guest bind finds room (the full-table scan keeps host entries, so it would end with "table full").
static void unbind_host(void) {
int n = (int)(sizeof g_se / sizeof *g_se);
CHECK(g_nse == n, "the table is full before the unbinds (%d of %d)", g_nse, n);
uint32_t in_w = wh_of(movie_in), in_r = rh_of(movie_in);
kern_unbind_system_event(&cpu, ev_of(movie_in));
CHECK(se_find(ev_of(movie_in), NULL) == 0, "an unbound event leaves the table");
CHECK(g_nse == n - 1, "the table has one entry fewer (%d)", g_nse);
CHECK(se_find(ev_of(movie_out), NULL) != 0, "the other host-memory event keeps its entry");
CHECK(movie_in[SE_STATE] == 0 && signaled(in_w) == -1, "the unbound event is destroyed: state 0, handle %#x closed",
in_w);
lock();
int r_open = h_peek(in_r, O_EVENT_R) != NULL;
unlock();
CHECK(!r_open, "its readable handle %#x is closed too", in_r);
kern_unbind_system_event(&cpu, ev_of(movie_in)); // a second unbind finds nothing and leaves the table alone
CHECK(g_nse == n - 1, "a second unbind changes nothing (%d)", g_nse);
// Full again with live guest entries and one host entry; the host entry unbound, the next guest bind takes its room.
int next = n + 2;
bind(guest[next++]);
CHECK(g_nse == n, "full again (%d)", g_nse);
kern_unbind_system_event(&cpu, ev_of(movie_out));
bind(guest[next]);
CHECK(se_find(ev_of(guest[next]), NULL) != 0 && g_nse == n,
"a guest bind succeeds once the host entry is unbound (%d entries)", g_nse);
free(movie_in), free(movie_out);
movie_in = movie_out = NULL;
}
int main(int argc, char **argv) {
setvbuf(stdout, NULL, _IONBF, 0);
me.cur = &cpu;
tcur = &me;
int full = argc > 1 && !strcmp(argv[1], "full");
stale_and_reuse();
race();
full_table(full);
if (full) return 0;
unbind_host();
printf(fails ? "system_event_check: %d failed\n" : "system_event_check: ok\n", fails);
return fails ? 1 : 0;
}