diff --git a/runtime/ipc.c b/runtime/ipc.c index c93b1d91..6a1c07f0 100644 --- a/runtime/ipc.c +++ b/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, diff --git a/runtime/ipc.h b/runtime/ipc.h index 58cb8f41..74e0820f 100644 --- a/runtime/ipc.h +++ b/runtime/ipc.h @@ -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 diff --git a/tests/ipc_session_check.c b/tests/ipc_session_check.c index 79e0cf22..17b76c20 100644 --- a/tests/ipc_session_check.c +++ b/tests/ipc_session_check.c @@ -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 @@ -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; }