mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 17:09:57 -04:00
Free closed IPC domain objects and give their ids out again, and keep a domain alive while its objects are.
A domain's objects[32] only grew: CloseVirtualHandle returned without freeing anything, so a client that opens and closes sub-objects (the applet proxy getters, about 17 at boot) would get IPC_R_UNKNOWN_COMMAND for its 33rd, which the SDK aborts on. The handlers took the new object's id from nobjects, the table had no lock (two threads could race on nobjects), and a sub-object reached through its own handle (CopyFromCurrentDomain) kept a root pointer that the domain's last CloseHandle had freed. The domain table now has a lock in ipc.c (g_domains), taken only for sessions in a domain and never across a handler or a close callback. ipc_domain_add gives out the first free slot and returns the id the handlers write back; the table holds one reference on each object and each object one on its domain (held_root), counted in the domain's subs. CloseVirtualHandle clears the slot and drops the table's reference; it never closes the domain's own id. A request takes a reference on its target object under the lock for its length, and a cleared id is answered with IPC_R_BAD_HANDLE instead of being dereferenced; CopyFromCurrentDomain answers the same. When a domain's remaining references are all its objects', its client has closed it: the table lets the objects go, each object nothing else holds is freed and lets go of the domain, and the domain is freed with the last one. Domain objects now count in ipc_sessions_live from the moment they are added, since their last reference counts them out. Not reproduced before the change: no log here has TRACE=ipc, so the getters per return to the title are not counted. ipc_session_check gains a domain section: appletOE converted to a domain, 100 rounds of a getter and its CloseVirtualHandle each get id 3 with the table at 3 slots, a closed id gets a bad-handle result, the domain's own id survives a CloseVirtualHandle, a proxy with a handle of its own keeps the closed domain until that handle closes and then both are freed, and closing a domain frees its proxy and five getters. The compile check of every runtime file passes with no new warnings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
112
runtime/ipc.c
112
runtime/ipc.c
@@ -83,9 +83,8 @@ static void applet_proxy_handler(IpcSession *s, const IpcRequest *rq, IpcRespons
|
||||
IpcSession *root = s->root ? s->root : s; // object ids belong to the client's whole domain
|
||||
IpcSession *o = stub_object(kinds[k].name);
|
||||
if (!o) { rs->result = IPC_R_UNKNOWN_COMMAND; return; }
|
||||
o->root = root;
|
||||
if (!ipc_domain_add(root, o)) { free(o); rs->result = IPC_R_UNKNOWN_COMMAND; return; }
|
||||
uint32_t id = (uint32_t)root->nobjects;
|
||||
uint32_t id = ipc_domain_add(root, o);
|
||||
if (!id) { free(o); rs->result = IPC_R_UNKNOWN_COMMAND; return; }
|
||||
IPC_OUT(rs, id);
|
||||
rs->domain = 1;
|
||||
rs->domain_objects = 1;
|
||||
@@ -155,10 +154,30 @@ static IpcSession *stub_object(const char *name) {
|
||||
return o;
|
||||
}
|
||||
|
||||
// Domain tables, their counts of their objects' references on them and their closed flags (IpcSession). Taken only for
|
||||
// sessions in a domain; never held across a handler or a close callback.
|
||||
static PlatMutex g_domains = PLAT_MUTEX_INIT;
|
||||
static volatile int32_t g_live;
|
||||
|
||||
// The first free slot: a closed object's id is given out again, so a client that opens and closes objects in a loop
|
||||
// never runs out of the 32.
|
||||
unsigned ipc_domain_add(IpcSession *domain, IpcSession *object) {
|
||||
if (domain->nobjects >= (int)(sizeof domain->objects / sizeof *domain->objects)) return 0;
|
||||
domain->objects[domain->nobjects++] = object;
|
||||
return (unsigned)domain->nobjects;
|
||||
unsigned id = 0;
|
||||
plat_mutex_lock(&g_domains);
|
||||
int i = 0, max = (int)(sizeof domain->objects / sizeof *domain->objects);
|
||||
while (i < domain->nobjects && domain->objects[i]) i++;
|
||||
if (!domain->dom_closed && i < max) {
|
||||
domain->objects[i] = object;
|
||||
if (i == domain->nobjects) domain->nobjects++;
|
||||
object->root = domain;
|
||||
object->held_root = domain;
|
||||
plat_inc32(&domain->refs);
|
||||
domain->subs++;
|
||||
plat_inc32(&g_live); // a live server object from here; its last reference counts it out
|
||||
id = (unsigned)i + 1;
|
||||
}
|
||||
plat_mutex_unlock(&g_domains);
|
||||
return id;
|
||||
}
|
||||
|
||||
// appletOE: the application-environment service. nn::oe::Initialize opens it; the calls that matter for
|
||||
@@ -172,9 +191,8 @@ static void applet_oe_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *
|
||||
proxy->name = "applet proxy";
|
||||
proxy->handler = applet_proxy_handler;
|
||||
proxy->refs = 1;
|
||||
proxy->root = s;
|
||||
if (!ipc_domain_add(s, proxy)) { free(proxy); rs->result = IPC_R_UNKNOWN_COMMAND; return; }
|
||||
uint32_t id = (uint32_t)s->nobjects; // the object's id inside the domain, written into the reply data
|
||||
uint32_t id = ipc_domain_add(s, proxy); // the object's id inside the domain, written into the reply data
|
||||
if (!id) { free(proxy); rs->result = IPC_R_UNKNOWN_COMMAND; return; }
|
||||
IPC_OUT(rs, id);
|
||||
rs->domain = 1;
|
||||
rs->domain_objects = 1;
|
||||
@@ -233,7 +251,6 @@ static const struct { const char *name; IpcHandler handler; } services[] = {
|
||||
{"csrng", csrng_handler},
|
||||
};
|
||||
|
||||
static volatile int32_t g_live;
|
||||
static volatile int64_t g_released, g_requests;
|
||||
static _Thread_local const Cpu *t_req; // guest context of the request being served
|
||||
|
||||
@@ -257,19 +274,46 @@ static uint32_t session_clone(IpcSession *s) {
|
||||
// kern_handle_hold's callback: a request's reference, dropped through ipc_handle_closed once the request is answered.
|
||||
static void session_hold(void *o) { plat_inc32(&((IpcSession *)o)->refs); }
|
||||
|
||||
// Drops one reference on a session; sub: the one an object of a domain held on that domain. A session in no domain
|
||||
// takes no lock here. A domain whose remaining references are all its objects' has lost its client: its table lets the
|
||||
// objects go, and each object that nothing else holds is closed and lets go of the domain in turn.
|
||||
static void session_release(IpcSession *s, int sub) {
|
||||
IpcSession *drop[32];
|
||||
int ndrop = 0;
|
||||
int32_t n;
|
||||
if (!sub && !s->root) {
|
||||
n = plat_dec32(&s->refs);
|
||||
} else {
|
||||
plat_mutex_lock(&g_domains);
|
||||
if (sub) s->subs--;
|
||||
n = plat_dec32(&s->refs);
|
||||
if (n && n == s->subs && s->root == s && !s->dom_closed) {
|
||||
s->dom_closed = 1;
|
||||
for (int i = 0; i < s->nobjects; i++) {
|
||||
if (s->objects[i] && s->objects[i] != s) drop[ndrop++] = s->objects[i];
|
||||
s->objects[i] = NULL;
|
||||
}
|
||||
}
|
||||
plat_mutex_unlock(&g_domains);
|
||||
}
|
||||
for (int i = 0; i < ndrop; i++) session_release(drop[i], 0);
|
||||
if (n) return;
|
||||
if (rt_trace & RT_TRACE_IPC) rt_log("ipc: %s closed", s->name);
|
||||
if (s->close) s->close(s->state);
|
||||
IpcSession *root = s->held_root;
|
||||
free(s);
|
||||
plat_dec32(&g_live);
|
||||
plat_inc64(&g_released);
|
||||
if (root) session_release(root, 1);
|
||||
}
|
||||
|
||||
void ipc_handle_closed(int kind, void *obj) {
|
||||
if (kind == KERN_TMEM) {
|
||||
free(obj);
|
||||
return;
|
||||
}
|
||||
if (kind != KERN_SESSION) rt_fatal("ipc_handle_closed: handle kind %d", kind);
|
||||
IpcSession *s = obj;
|
||||
if (plat_dec32(&s->refs)) return;
|
||||
if (rt_trace & RT_TRACE_IPC) rt_log("ipc: %s closed", s->name);
|
||||
if (s->close) s->close(s->state);
|
||||
free(s);
|
||||
plat_dec32(&g_live);
|
||||
plat_inc64(&g_released);
|
||||
session_release(obj, 0);
|
||||
}
|
||||
|
||||
// ---- helpers for services
|
||||
@@ -409,15 +453,20 @@ static void respond(uint32_t *m, int tipc, const IpcResponse *rs) {
|
||||
static void control(IpcSession *s, const IpcRequest *rq, IpcResponse *rs) {
|
||||
switch (rq->cmd) {
|
||||
case 0: // ConvertCurrentObjectToDomain: the session answers domain-tagged requests from now on
|
||||
plat_mutex_lock(&g_domains);
|
||||
s->is_domain = 1;
|
||||
s->root = s;
|
||||
if (s->nobjects == 0) { s->objects[0] = s; s->nobjects = 1; }
|
||||
plat_mutex_unlock(&g_domains);
|
||||
return;
|
||||
case 1: { // CopyFromCurrentDomain(object id): a handle to that object of the domain
|
||||
case 1: { // CopyFromCurrentDomain(object id): a handle to that object of the domain (a closed id is a bad handle)
|
||||
uint32_t id = rq->data_size >= 4 ? *(const uint32_t *)(const void *)rq->data : 0;
|
||||
if (!id || id > (uint32_t)s->nobjects) { rs->result = IPC_R_UNKNOWN_COMMAND; return; }
|
||||
plat_mutex_lock(&g_domains);
|
||||
IpcSession *o = s->objects[id - 1];
|
||||
plat_inc32(&o->refs);
|
||||
if (o) plat_inc32(&o->refs);
|
||||
plat_mutex_unlock(&g_domains);
|
||||
if (!o) { rs->result = IPC_R_BAD_HANDLE; return; }
|
||||
rs->move[rs->nmove++] = kern_handle_new(KERN_SESSION, o);
|
||||
return;
|
||||
}
|
||||
@@ -439,7 +488,7 @@ static void dispatch(IpcSession *s, uint32_t *m) {
|
||||
IpcResponse rs;
|
||||
memset(&rs, 0, sizeof rs);
|
||||
int tipc = rq.type >= T_TIPC_BASE;
|
||||
IpcSession *target = s;
|
||||
IpcSession *target = s, *held = NULL; // held: a domain object the request holds a reference on
|
||||
const uint8_t *bytes = (const uint8_t *)words, *end = bytes + ndata * 4;
|
||||
if (tipc) {
|
||||
rq.cmd = rq.type - T_TIPC_BASE;
|
||||
@@ -455,13 +504,31 @@ static void dispatch(IpcSession *s, uint32_t *m) {
|
||||
payload += 16;
|
||||
rs.domain = 1;
|
||||
uint32_t dcmd = dh[0] & 0xff, obj = dh[1];
|
||||
if (dcmd == 2) return; // CloseVirtualHandle: nothing to answer
|
||||
if (dcmd == 2) { // CloseVirtualHandle: the object leaves the domain (never the domain's own); no answer
|
||||
IpcSession *o = NULL;
|
||||
plat_mutex_lock(&g_domains);
|
||||
if (obj >= 2 && obj <= (uint32_t)dom->nobjects && dom->objects[obj - 1] != dom) {
|
||||
o = dom->objects[obj - 1];
|
||||
dom->objects[obj - 1] = NULL;
|
||||
}
|
||||
plat_mutex_unlock(&g_domains);
|
||||
if (o) session_release(o, 0);
|
||||
return;
|
||||
}
|
||||
if (obj == 0) obj = 1; // this SDK addresses the domain's own object as 0
|
||||
if (obj > (uint32_t)dom->nobjects) {
|
||||
ipc_backtrace();
|
||||
rt_fatal("ipc: %s domain object %u (of %d) does not exist [hdr %08x %08x %08x %08x] [msg %08x %08x %08x %08x]", s->name, obj, dom->nobjects, dh[0], dh[1], dh[2], dh[3], m[0], m[1], m[2], m[3]);
|
||||
}
|
||||
target = dom->objects[obj - 1];
|
||||
plat_mutex_lock(&g_domains);
|
||||
target = held = dom->objects[obj - 1];
|
||||
if (held) plat_inc32(&held->refs);
|
||||
plat_mutex_unlock(&g_domains);
|
||||
if (!target) { // an id the client closed
|
||||
rs.result = IPC_R_BAD_HANDLE;
|
||||
respond(m, 0, &rs);
|
||||
return;
|
||||
}
|
||||
}
|
||||
uint32_t h[4];
|
||||
memcpy(h, payload, 16);
|
||||
@@ -482,6 +549,7 @@ static void dispatch(IpcSession *s, uint32_t *m) {
|
||||
}
|
||||
if (rq.type == T_CONTROL || rq.type == T_CONTROL_CTX) control(s, &rq, &rs);
|
||||
else target->handler(target, &rq, &rs);
|
||||
if (held) session_release(held, 0);
|
||||
if (rt_trace & RT_TRACE_IPC) {
|
||||
char d[160];
|
||||
rt_log("ipc: %s %s %u -> %#x (copy %u move %u) from %s", s->name,
|
||||
|
||||
@@ -41,8 +41,11 @@ typedef struct {
|
||||
typedef struct IpcSession IpcSession;
|
||||
typedef void (*IpcHandler)(IpcSession *s, const IpcRequest *rq, IpcResponse *rs);
|
||||
|
||||
// One server object. Every kernel handle to it (the first one, CloneCurrentObject copies) holds a reference; the
|
||||
// last CloseHandle calls close(state).
|
||||
// One server object. Every kernel handle to it (the first one, CloneCurrentObject copies) holds a reference, and so
|
||||
// does a request on it while it runs; the last reference calls close(state). A domain's table holds one on each of
|
||||
// its objects, and each object one on its domain (held_root), until the client closes the object's id
|
||||
// (CloseVirtualHandle) or its last handle to the domain itself. ipc.c's domain lock guards objects, nobjects, subs
|
||||
// and dom_closed.
|
||||
struct IpcSession {
|
||||
const char *name;
|
||||
IpcHandler handler;
|
||||
@@ -51,12 +54,16 @@ struct IpcSession {
|
||||
int32_t refs;
|
||||
int is_domain; // ConvertCurrentObjectToDomain ran: requests carry a domain header
|
||||
IpcSession *root; // the domain session every object id belongs to (objects share it)
|
||||
IpcSession *objects[32]; // object 1 is this session, then what the service handed out
|
||||
int nobjects;
|
||||
IpcSession *objects[32]; // object 1 is this session, then what the service handed out; NULL: closed
|
||||
int nobjects; // slots used so far (closed ones are given out again)
|
||||
uint32_t event; // readable event handed to the client (applet state changes)
|
||||
IpcSession *held_root; // the domain this object holds a reference on (ipc_domain_add)
|
||||
int32_t subs; // as a domain: the references its objects hold on it
|
||||
int dom_closed; // as a domain: its client closed it and its table let the objects go
|
||||
};
|
||||
|
||||
// Register an object inside a domain (1-based id returned, 0 when full).
|
||||
// Register an object inside a domain: the table takes the object's first reference (the caller's), the object one on
|
||||
// the domain. The 1-based id, 0 when the table is full or the domain closed.
|
||||
unsigned ipc_domain_add(IpcSession *domain, IpcSession *object);
|
||||
|
||||
uint32_t ipc_session_new(const char *name, IpcHandler handler, void *state, void (*close)(void *state)); // handle
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
// session a request whose handler blocks while another thread closes the session's only handle: the session's
|
||||
// close callback runs once, after the handler returned, never while it runs
|
||||
// tmem kern_handle_copy hands out the transfer memory's fields while the handle is open, and 0 after its close
|
||||
// domain appletOE as a domain: 100 rounds of a getter and its CloseVirtualHandle reuse one id; a closed id is
|
||||
// answered with a bad-handle result; the domain's own id is never closed; an object with a handle of its
|
||||
// own (CopyFromCurrentDomain) keeps the domain after the client closes it, and the last handle frees all
|
||||
#include "ipc.c"
|
||||
|
||||
#include <stdarg.h>
|
||||
@@ -142,10 +145,86 @@ static void tmem_check(void) {
|
||||
T_CHECK(!kern_handle_copy(h, KERN_TMEM, ©, sizeof copy), "a closed transfer memory handle copies nothing");
|
||||
}
|
||||
|
||||
// A CMIF domain request on h: domain command dcmd (1 send, 2 close) to object obj, command cmd. *result and *id (the
|
||||
// reply's first data word) are filled for a send; a close has no answer.
|
||||
static uint32_t dom_call(Cpu *c, uint32_t h, uint32_t dcmd, uint32_t obj, uint32_t cmd, uint32_t *result,
|
||||
uint32_t *id) {
|
||||
uint32_t *m = (uint32_t *)(uintptr_t)c->tpidrro;
|
||||
memset(m, 0, IPC_MSG_BYTES);
|
||||
m[0] = T_REQUEST;
|
||||
uint8_t *words = (uint8_t *)(m + 2), *payload = (uint8_t *)m + (((words - (uint8_t *)m) + 15) & ~15);
|
||||
uint32_t dh[4] = {dcmd, obj, 0, 0}, sh[4] = {CMIF_IN_MAGIC, 0, cmd, 0};
|
||||
memcpy(payload, dh, 16);
|
||||
memcpy(payload + 16, sh, 16);
|
||||
m[1] = (16 + 16 + 16 + 3) / 4; // the alignment pad, the domain header and the CMIF header
|
||||
uint32_t k = sv(c, 0x21, h, 0, 0, 0);
|
||||
if (k || dcmd != 1) return k;
|
||||
const uint8_t *rp = (const uint8_t *)m + 16 + 16; // no handles in these answers: words at 8, payload at 16
|
||||
uint32_t oh[4];
|
||||
memcpy(oh, rp, 16);
|
||||
*result = oh[0] == CMIF_OUT_MAGIC ? oh[2] : 0xFFFFFFFFu;
|
||||
memcpy(id, rp + 16, 4);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void domain_check(void) {
|
||||
long live0 = ipc_sessions_live();
|
||||
static uint32_t msg[IPC_MSG_BYTES / 4];
|
||||
static Cpu c;
|
||||
c.tpidrro = (uint64_t)(uintptr_t)msg;
|
||||
uint32_t h = ipc_session_new("appletOE", applet_oe_handler, NULL, NULL);
|
||||
Msg conv = {.type = T_CONTROL, .cmd = 0};
|
||||
Reply r;
|
||||
T_CHECK(call(&c, h, &conv, &r) == 0, "ConvertCurrentObjectToDomain");
|
||||
IpcSession *root = kern_handle_get(h, KERN_SESSION);
|
||||
uint32_t res = 0, proxy = 0, id = 0;
|
||||
T_CHECK(!dom_call(&c, h, 1, 0, 0, &res, &proxy) && !res && proxy == 2, "OpenApplicationProxy gives object 2 (%u)", proxy);
|
||||
int bad = 0;
|
||||
for (int i = 0; i < 100; i++) {
|
||||
uint32_t got = 0;
|
||||
dom_call(&c, h, 1, proxy, 0, &res, &got); // the common state getter
|
||||
bad += res != 0 || got != 3;
|
||||
if (i == 0) id = got;
|
||||
dom_call(&c, h, 2, got, 0, &res, &got); // CloseVirtualHandle
|
||||
}
|
||||
T_CHECK(!bad, "100 getters opened and closed, each answered with id 3 (%d not)", bad);
|
||||
T_CHECK(root->nobjects == 3 && root->subs == 1, "the table stays at 3 slots (%d), one object open (%d)", root->nobjects,
|
||||
root->subs);
|
||||
T_CHECK(ipc_sessions_live() == live0 + 2, "closed getters are freed (%ld live, %ld before)", ipc_sessions_live(), live0);
|
||||
uint32_t got = 0;
|
||||
dom_call(&c, h, 1, id, 0, &res, &got);
|
||||
T_CHECK(res == IPC_R_BAD_HANDLE, "a closed id is answered with a bad handle (%#x)", res);
|
||||
dom_call(&c, h, 2, 1, 0, &res, &got); // the domain's own id
|
||||
T_CHECK(root->objects[0] == root, "CloseVirtualHandle never closes the domain's own id");
|
||||
// A handle of the proxy's own, then the client closes the domain: the proxy keeps it until its handle closes.
|
||||
uint32_t pid = proxy;
|
||||
Msg copy = {.type = T_CONTROL, .cmd = 1, .data = &pid, .size = 4};
|
||||
T_CHECK(call(&c, h, ©, &r) == 0 && r.nmove == 1, "CopyFromCurrentDomain hands out a handle");
|
||||
uint32_t ph = r.nmove ? r.move[0] : 0;
|
||||
kern_handle_close(h);
|
||||
T_CHECK(ipc_sessions_live() == live0 + 2 && root->dom_closed,
|
||||
"the domain stays while its object has a handle (%ld live), its table closed", ipc_sessions_live());
|
||||
IpcSession *po = kern_handle_get(ph, KERN_SESSION);
|
||||
T_CHECK(po && po->root == root && po->refs == 1, "the object reached through its own handle still names the domain");
|
||||
kern_handle_close(ph);
|
||||
T_CHECK(ipc_sessions_live() == live0, "the last handle frees the object and the domain (%ld live, %ld before)",
|
||||
ipc_sessions_live(), live0);
|
||||
// A domain whose client closes it with objects that have no handle of their own: all are freed at once.
|
||||
h = ipc_session_new("appletOE", applet_oe_handler, NULL, NULL);
|
||||
call(&c, h, &conv, &r);
|
||||
dom_call(&c, h, 1, 0, 0, &res, &proxy);
|
||||
for (int i = 0; i < 5; i++) dom_call(&c, h, 1, proxy, (unsigned)i, &res, &got);
|
||||
T_CHECK(ipc_sessions_live() == live0 + 7, "a domain, its proxy and five getters (%ld live)", ipc_sessions_live());
|
||||
kern_handle_close(h);
|
||||
T_CHECK(ipc_sessions_live() == live0, "closing the domain frees its objects (%ld live, %ld before)", ipc_sessions_live(),
|
||||
live0);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
session_check();
|
||||
tmem_check();
|
||||
domain_check();
|
||||
printf(fails ? "ipc_session_check: %d failed\n" : "ipc_session_check: ok\n", fails);
|
||||
return fails ? 1 : 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user