Files
NX2navi/runtime/ipc.c
chrg6 411dd5fc01 Answer the game's csrng requests with random bytes from the host.
The game imports the SDK's cryptographic random call, which the SDK serves through the csrng service, and the service manager ended the game on any service it does not know ("sm: service csrng is not implemented"). csrng now exists: GenerateRandomBytes, its one command, fills the request's buffer from the host's generator, and any other command stops with its name. No start or session logged on our own PCs opens the service, so nothing changes there; a contributor's traced start did.

The IPC probe opens csrng and asks twice: both 32-byte buffers come back filled, each differently, and no session is left over.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
(cherry picked from commit 7591a7b65627f74d53d8276c403524b71d11acbe)
2026-11-11 00:39:50 -05:00

1063 lines
50 KiB
C

// Contains code Copyright (c) 2026 MacNativeGames contributors (MIT; see THIRD_PARTY_NOTICES.md).
// Message framing per the HIPC, CMIF and TIPC wire layouts.
// In-process Horizon IPC: SVCs 0x15, 0x1F, 0x21, 0x22, the sm: service manager
// and the service table. The SDK client code runs unchanged: requests are read from the calling thread's message
// buffer (TLR, or the user buffer of 0x22) and answered in place. Unknown ports, services and commands stop with their
// name and id instead of returning a fake success. Also --probe-ipc.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "audren.h"
#include "host.h"
#include "ipc.h"
#include "ov.h"
#include "nfp.h"
#include "platform/platform.h"
#define CMIF_IN_MAGIC 0x49434653u // "SFCI"
#define CMIF_OUT_MAGIC 0x4f434653u // "SFCO"
#define POINTER_BUFFER_SIZE 0x8000
#define R_INVALID_SIZE 0xCA01u
#define R_INVALID_ADDRESS 0xCC01u
enum { T_CLOSE = 2, T_REQUEST = 4, T_CONTROL = 5, T_REQUEST_CTX = 6, T_CONTROL_CTX = 7, T_TIPC_CLOSE = 15,
T_TIPC_BASE = 16 };
// The application's lifecycle (as the system's applet manager runs it): one state-changed
// event per applet, signalled while a message is waiting and cleared by the server once the queue drains (Horizon
// does not clear it on a wait). An *application* learns it has focus through FocusStateChanged (15) followed by
// GetCurrentFocusState == InFocus; ChangeIntoForeground (1) is the library-applet message and an application
// ignores it, which is what parked nn::oe::InitializeApplet on the event.
enum { AM_MSG_FOCUS_STATE_CHANGED = 15, AM_MSG_OPERATION_MODE_CHANGED = 30, AM_MSG_PERFORMANCE_MODE_CHANGED = 31 };
enum { AM_FOCUS_IN = 1, AM_FOCUS_NOT_IN = 2, AM_FOCUS_BACKGROUND = 3 };
#define AM_R_NO_MESSAGES 0x680u // the "no messages" result this SDK checks for
static uint32_t g_applet_event, g_applet_event_w;
static PlatMutex g_applet_lock = PLAT_MUTEX_INIT;
static uint32_t g_applet_msgs[32];
static unsigned g_applet_head, g_applet_count;
static uint8_t g_applet_focus = AM_FOCUS_IN;
void ipc_applet_push_message(uint32_t msg) {
plat_mutex_lock(&g_applet_lock);
if (g_applet_count < sizeof g_applet_msgs / sizeof *g_applet_msgs) {
g_applet_msgs[(g_applet_head + g_applet_count++) % (sizeof g_applet_msgs / sizeof *g_applet_msgs)] = msg;
} else {
rt_log("applet: message queue full, dropping %u", msg);
}
plat_mutex_unlock(&g_applet_lock);
if (g_applet_event_w) kern_event_signal(g_applet_event_w);
}
static int applet_pop_message(uint32_t *out) {
plat_mutex_lock(&g_applet_lock);
int ok = g_applet_count != 0;
if (ok) {
*out = g_applet_msgs[g_applet_head];
g_applet_head = (g_applet_head + 1) % (sizeof g_applet_msgs / sizeof *g_applet_msgs);
g_applet_count--;
}
int drained = g_applet_count == 0;
plat_mutex_unlock(&g_applet_lock);
if (drained && g_applet_event_w) kern_event_clear(g_applet_event_w);
return ok;
}
static IpcSession *stub_object(const char *name);
static void applet_stub_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs);
// A stub object the applet proxy commands are answered on: the interfaces exist, their calls succeed.
// The getters below each hand out one of those objects, which is what the SDK stores and dereferences.
static void applet_proxy_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs) {
if (rt_trace & RT_TRACE_IPC) rt_log("applet proxy: command %u", rq->cmd);
static const struct { unsigned cmd; const char *name; } kinds[] = {
{0, "common state getter"}, {1, "self controller"}, {2, "window controller"},
{3, "audio controller"}, {4, "display controller"}, {10, "process winding controller"},
{11, "library applet creator"}, {20, "application functions"},
{40, "nx2 interface"}, {50, "nx2 interface"}, {60, "graphics scale controller"},
{70, "nx2 interface"}, {80, "nx2 interface"}, {90, "nx2 interface"},
{1000, "debug functions"},
};
for (unsigned k = 0; k < sizeof kinds / sizeof *kinds; k++) {
if (kinds[k].cmd != rq->cmd) continue;
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;
IPC_OUT(rs, id);
rs->domain = 1;
rs->domain_objects = 1;
rs->result = 0;
return;
}
rs->result = 0;
}
// The interfaces themselves. Only the calls the game makes while booting are modelled: the common state getter
// hands out the applet's state-changed event, and everything else reports success.
static void applet_stub_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs) {
if (rt_trace & RT_TRACE_IPC) rt_log("%s: command %u", s->name, rq->cmd);
int common = !strcmp(s->name, "common state getter");
if (common && rq->cmd == 0) { // GetEventHandle: the applet state-changed event
if (!g_applet_event) ipc_applet_event_init();
s->event = g_applet_event;
rs->copy[rs->ncopy++] = s->event;
rs->result = 0;
return;
}
if (common && rq->cmd == 1) { // ReceiveMessage: the next queued applet message, else "no messages"
uint32_t msg;
if (applet_pop_message(&msg)) {
if (rt_trace & RT_TRACE_IPC) rt_log("applet: ReceiveMessage -> %u", msg);
IPC_OUT(rs, msg);
rs->result = 0;
return;
}
rs->result = AM_R_NO_MESSAGES;
return;
}
if (common && rq->cmd == 5) { // GetOperationMode (matches the oe override)
uint8_t mode = !ov_handheld();
IPC_OUT(rs, mode);
rs->result = 0;
return;
}
if (common && rq->cmd == 6) { // GetPerformanceMode: boost when docked
uint32_t mode = !ov_handheld();
IPC_OUT(rs, mode);
rs->result = 0;
return;
}
if (common && rq->cmd == 9) { // GetCurrentFocusState
uint8_t focus = g_applet_focus;
IPC_OUT(rs, focus);
rs->result = 0;
return;
}
if (!strcmp(s->name, "graphics scale controller") && rq->cmd == 10) { // GetCurrentGraphicsScale
uint32_t scale[3] = {10, 0, 0}; // see ov_oe_scaling_parameters
IPC_OUT(rs, scale);
rs->result = 0;
return;
}
if (rt_trace & RT_TRACE_IPC) rt_log("%s: command %u answered with success and no data", s->name, rq->cmd);
rs->result = 0;
}
static IpcSession *stub_object(const char *name) {
IpcSession *o = calloc(1, sizeof *o);
if (!o) return NULL;
o->name = name;
o->handler = applet_stub_handler;
o->refs = 1;
return o;
}
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;
}
// appletOE: the application-environment service. nn::oe::Initialize opens it; the calls that matter for
// boot answer through symbol overrides (focus, performance, language), so a permissive handler that
// reports success and writes nothing is enough until a command needs real data — then it is added here.
static void applet_oe_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs) {
if (rt_trace & RT_TRACE_IPC) rt_log("appletOE: command %u", rq->cmd);
if (rq->cmd == 0 || rq->cmd == 10) { // OpenApplicationProxy: hand the client a domain object
IpcSession *proxy = calloc(1, sizeof *proxy);
if (!proxy) { rs->result = IPC_R_UNKNOWN_COMMAND; return; }
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
IPC_OUT(rs, id);
rs->domain = 1;
rs->domain_objects = 1;
}
rs->result = 0;
}
// aud:u: the Switch 2 audio entry point (nn::audio::CreateUserClientSessionByHipc). It hands out the Switch 1
// managers as sub-objects (command ids inferred from the calls the SDK makes):
// 2 -> IAudioRendererManager (the audren:u manager), 1 -> IAudioOutManager (not served by this host).
static void aud_user_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs) {
(void)s;
if (rt_trace & RT_TRACE_IPC) rt_log("aud:u: command %u", rq->cmd);
if (rq->cmd == 2) {
rs->move[rs->nmove++] = ipc_session_new("IAudioRendererManager", audren_manager_handler, NULL, NULL);
rs->result = 0;
return;
}
rt_fatal("aud:u: command %u is not implemented (1 is GetAudioOutManager: no audout service here)", rq->cmd);
}
static void csrng_fill(uint64_t addr, uint64_t size) {
if (!size) return;
if (!plat_random(ipc_guest(addr, size), (size_t)size)) {
ipc_backtrace();
rt_fatal("csrng: no random bytes from the system");
}
}
// csrng: the SDK's cryptographic random source. GenerateRandomBytes (0) is its one command; the host's generator fills
// the output buffer. The SDK sends it as a B buffer (a contributor's trace: one B buffer, no others); a C buffer is
// filled too, with its reply entry, for a caller that lets the buffer type be chosen. No start or session logged on
// our own PCs opens the service, but the game imports the call, and without the service a request for it ended the
// game ("sm: service csrng is not implemented").
static void csrng_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs) {
if (rt_trace & RT_TRACE_IPC)
rt_log("csrng: command %u (x%u a%u b%u w%u c%u)", rq->cmd, rq->nx, rq->na, rq->nb, rq->nw, rq->nc);
if (rq->cmd != 0) ipc_unimplemented(s, rq);
for (unsigned i = 0; i < rq->nb; i++) csrng_fill(rq->b[i].addr, rq->b[i].size);
for (unsigned i = 0; i < rq->nc; i++)
if (rq->c[i].size && (i >= rq->nb || !rq->b[i].size)) {
csrng_fill(rq->c[i].addr, rq->c[i].size);
rs->xout[rs->nxout++] = (IpcBuffer){rq->c[i].addr, rq->c[i].size, i};
}
rs->result = 0;
}
// Services sm: hands out. Anything else is a named fatal.
static const struct { const char *name; IpcHandler handler; } services[] = {
{"audren:u", audren_manager_handler},
{"nfp:user", nfp_user_manager_handler},
{"appletOE", applet_oe_handler},
{"aud:u", aud_user_handler},
{"auddmg:u", audio_dmg_handler},
{"audsmx:u", audio_sysmix_handler},
{"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
long ipc_sessions_live(void) { return plat_cas32(&g_live, 0, 0); }
uint64_t ipc_sessions_released(void) { return (uint64_t)plat_cas64(&g_released, 0, 0); }
// ---- sessions
uint32_t ipc_session_new(const char *name, IpcHandler handler, void *state, void (*close)(void *state)) {
IpcSession *s = calloc(1, sizeof *s);
if (!s) rt_fatal("ipc: session allocation");
*s = (IpcSession){name, handler, state, close, 1};
plat_inc32(&g_live);
return kern_handle_new(KERN_SESSION, s);
}
static uint32_t session_clone(IpcSession *s) {
plat_inc32(&s->refs);
return kern_handle_new(KERN_SESSION, s);
}
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);
}
// ---- helpers for services
static int readable(uint64_t a, uint64_t n) { return mem_host_readable(a, n); }
void ipc_backtrace(void) {
const Cpu *c = t_req;
if (!c) return;
char d[160];
rt_logf(" ipc request from %s\n", ldr_describe(c->x[30], d, sizeof d));
uint64_t fp = c->x[29];
for (int n = 0; n < 24 && fp && !(fp & 7) && readable(fp, 16); n++) {
uint64_t next = LD64(fp), lr = LD64(fp + 8);
rt_logf(" guest frame %2d: %s\n", n, ldr_describe(lr, d, sizeof d));
if (next <= fp || next - fp > (64u << 20)) break;
fp = next;
}
}
void *ipc_guest(uint64_t addr, uint64_t size) {
if ((!addr && size) || addr + size < addr || addr + size > RT_ADDRESS_LIMIT) {
ipc_backtrace();
rt_fatal("ipc: client buffer %#llx+%#llx", (unsigned long long)addr, (unsigned long long)size);
}
return (void *)(uintptr_t)addr;
}
const void *ipc_arg(const IpcRequest *rq, uint32_t off, uint32_t n) {
if (off + n > rq->data_size) {
ipc_backtrace();
rt_fatal("ipc: command %u argument %u+%u beyond %u bytes", rq->cmd, off, n, rq->data_size);
}
return rq->data + off;
}
void ipc_out(IpcResponse *rs, const void *p, uint32_t n) {
if (rs->data_size + n > sizeof rs->data) rt_fatal("ipc: response larger than %u bytes", (unsigned)sizeof rs->data);
memcpy(rs->data + rs->data_size, p, n);
rs->data_size += n;
}
void ipc_unimplemented(const IpcSession *s, const IpcRequest *rq) {
fflush(stdout);
rt_logf("ipc: %s command %u (type %u, data %u bytes, x%u a%u b%u w%u c%u, copy %u move %u)\n", s->name,
rq->cmd, rq->type, rq->data_size, rq->nx, rq->na, rq->nb, rq->nw, rq->nc, rq->ncopy, rq->nmove);
ipc_backtrace();
rt_fatal("ipc: %s command %u is not implemented", s->name, rq->cmd);
}
// ---- HIPC framing
static uint64_t static_addr(const uint32_t *d) {
return d[1] | ((uint64_t)((d[0] >> 12) & 0xf) << 32) | ((uint64_t)((d[0] >> 6) & 0x3f) << 36);
}
static uint64_t buffer_addr(const uint32_t *d) {
return d[1] | ((uint64_t)((d[2] >> 28) & 0xf) << 32) | ((uint64_t)((d[2] >> 2) & 0x3fffff) << 36);
}
static uint64_t buffer_size(const uint32_t *d) { return d[0] | ((uint64_t)((d[2] >> 24) & 0xf) << 32); }
static void parse(const uint32_t *m, IpcRequest *rq, const uint32_t **data_words, unsigned *ndata) {
memset(rq, 0, sizeof *rq);
rq->type = m[0] & 0xffff;
rq->nx = (m[0] >> 16) & 0xf;
rq->na = (m[0] >> 20) & 0xf;
rq->nb = (m[0] >> 24) & 0xf;
rq->nw = (m[0] >> 28) & 0xf;
*ndata = m[1] & 0x3ff;
unsigned recv_mode = (m[1] >> 10) & 0xf, special = m[1] >> 31;
if (rq->nx > IPC_MAX_BUF || rq->na > IPC_MAX_BUF || rq->nb > IPC_MAX_BUF || rq->nw > IPC_MAX_BUF)
rt_fatal("ipc: too many buffers (x%u a%u b%u w%u)", rq->nx, rq->na, rq->nb, rq->nw);
const uint32_t *p = m + 2;
if (special) {
uint32_t h = *p++;
rq->has_pid = h & 1;
rq->ncopy = (h >> 1) & 0xf;
rq->nmove = (h >> 5) & 0xf;
if (rq->ncopy > 8 || rq->nmove > 8) rt_fatal("ipc: too many handles (copy %u move %u)", rq->ncopy, rq->nmove);
if (rq->has_pid) {
memcpy(&rq->pid, p, 8);
p += 2;
}
for (unsigned i = 0; i < rq->ncopy; i++) rq->copy[i] = *p++;
for (unsigned i = 0; i < rq->nmove; i++) rq->move[i] = *p++;
}
for (unsigned i = 0; i < rq->nx; i++, p += 2) rq->x[i] = (IpcBuffer){static_addr(p), p[0] >> 16, p[0] & 0x3f};
IpcBuffer *lists[3] = {rq->a, rq->b, rq->w};
unsigned counts[3] = {rq->na, rq->nb, rq->nw};
for (unsigned k = 0; k < 3; k++)
for (unsigned i = 0; i < counts[k]; i++, p += 3) lists[k][i] = (IpcBuffer){buffer_addr(p), buffer_size(p), i};
*data_words = p;
const uint32_t *recv = p + *ndata;
rq->nc = recv_mode == 2 ? 1 : recv_mode > 2 ? recv_mode - 2 : 0;
if (rq->nc > IPC_MAX_BUF) rt_fatal("ipc: %u receive buffers", rq->nc);
for (unsigned i = 0; i < rq->nc; i++, recv += 2)
rq->c[i] = (IpcBuffer){recv[0] | ((uint64_t)(recv[1] & 0xffff) << 32), recv[1] >> 16, i};
if ((const uint8_t *)recv - (const uint8_t *)m > IPC_MSG_BYTES) rt_fatal("ipc: request exceeds the message buffer");
}
static void respond(uint32_t *m, int tipc, const IpcResponse *rs) {
uint32_t out[IPC_MSG_BYTES / 4];
memset(out, 0, sizeof out);
unsigned special = rs->ncopy || rs->nmove;
uint32_t *p = out + 2;
if (special) {
*p++ = (rs->ncopy << 1) | (rs->nmove << 5);
for (unsigned i = 0; i < rs->ncopy; i++) *p++ = rs->copy[i];
for (unsigned i = 0; i < rs->nmove; i++) *p++ = rs->move[i];
}
for (unsigned i = 0; i < rs->nxout; i++, p += 2) {
uint64_t a = rs->xout[i].addr;
p[0] = (rs->xout[i].index & 0x3f) | (uint32_t)(((a >> 36) & 0x3f) << 6) | (uint32_t)(((a >> 32) & 0xf) << 12) |
(uint32_t)(rs->xout[i].size << 16);
p[1] = (uint32_t)a;
}
uint8_t *words = (uint8_t *)p, *payload;
unsigned header = tipc ? 4 : 16;
payload = tipc ? words : (uint8_t *)out + (((words - (uint8_t *)out) + 15) & ~15);
if (rs->domain && !tipc) { // CMIF domain reply: 16-byte header with the objects appended, then SFCO
uint32_t dh[4] = {rs->domain_objects, 0, 0, 0};
memcpy(payload, dh, 16);
payload += 16;
}
unsigned used = (unsigned)(payload - words) + header + rs->data_size;
unsigned nwords = tipc ? (used + 3) / 4 : (used + 16 + 3) / 4; // CMIF keeps alignment slack
if ((size_t)(words - (uint8_t *)out) + nwords * 4 > IPC_MSG_BYTES) rt_fatal("ipc: response exceeds the message buffer");
if (tipc) memcpy(payload, &rs->result, 4);
else {
uint32_t h[4] = {CMIF_OUT_MAGIC, 0, rs->result, 0};
memcpy(payload, h, 16);
}
memcpy(payload + header, rs->data, rs->data_size);
out[0] = (rs->nxout & 0xf) << 16;
out[1] = (nwords & 0x3ff) | ((special || rs->domain) ? 1u << 31 : 0);
memcpy(m, out, IPC_MSG_BYTES);
}
// CMIF control requests.
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
s->is_domain = 1;
s->root = s;
if (s->nobjects == 0) { s->objects[0] = s; s->nobjects = 1; }
return;
case 1: { // CopyFromCurrentDomain(object id): a handle to that object of the domain
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; }
IpcSession *o = s->objects[id - 1];
plat_inc32(&o->refs);
rs->move[rs->nmove++] = kern_handle_new(KERN_SESSION, o);
return;
}
case 3: { uint16_t n = POINTER_BUFFER_SIZE; IPC_OUT(rs, n); return; } // QueryPointerBufferSize
case 2: case 4: rs->move[rs->nmove++] = session_clone(s); return; // CloneCurrentObject(Ex)
default:
ipc_backtrace();
rt_fatal("ipc: %s control command %u is not implemented (domains are not served)", s->name, rq->cmd);
}
}
static void dispatch(IpcSession *s, uint32_t *m) {
IpcRequest rq;
const uint32_t *words;
unsigned ndata;
parse(m, &rq, &words, &ndata);
if (rq.type == T_CLOSE || rq.type == T_TIPC_CLOSE) return;
plat_inc64(&g_requests);
IpcResponse rs;
memset(&rs, 0, sizeof rs);
int tipc = rq.type >= T_TIPC_BASE;
IpcSession *target = s;
const uint8_t *bytes = (const uint8_t *)words, *end = bytes + ndata * 4;
if (tipc) {
rq.cmd = rq.type - T_TIPC_BASE;
rq.data = bytes;
rq.data_size = (uint32_t)(end - bytes);
} else if (rq.type == T_REQUEST || rq.type == T_REQUEST_CTX || rq.type == T_CONTROL || rq.type == T_CONTROL_CTX) {
const uint8_t *payload = (const uint8_t *)m + (((bytes - (const uint8_t *)m) + 15) & ~15);
IpcSession *dom = s->root ? s->root : s; // ids belong to the client's domain, not to this object
if ((s->is_domain || dom->is_domain) && (rq.type == T_REQUEST || rq.type == T_REQUEST_CTX)) {
// Domain request: a 16-byte domain header names the object (1-based), then the usual SFCI payload.
uint32_t dh[4];
memcpy(dh, payload, 16);
payload += 16;
rs.domain = 1;
uint32_t dcmd = dh[0] & 0xff, obj = dh[1];
if (dcmd == 2) return; // CloseVirtualHandle: nothing to answer
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];
}
uint32_t h[4];
memcpy(h, payload, 16);
if (h[0] != CMIF_IN_MAGIC) {
ipc_backtrace();
rt_fatal("ipc: %s: CMIF magic %#x", s->name, h[0]);
}
rq.cmd = h[2];
rq.data = payload + 16;
rq.data_size = payload + 16 <= end ? (uint32_t)(end - payload - 16) : 0;
} else {
fflush(stdout);
rt_logf("ipc: %s message type %u:", s->name, rq.type);
for (unsigned i = 0; i < 16; i++) rt_logf(" %08x", m[i]);
rt_log_raw("\n", 1);
ipc_backtrace();
rt_fatal("ipc: %s message type %u is not implemented", s->name, rq.type);
}
if (rq.type == T_CONTROL || rq.type == T_CONTROL_CTX) control(s, &rq, &rs);
else target->handler(target, &rq, &rs);
if (rt_trace & RT_TRACE_IPC) {
char d[160];
rt_log("ipc: %s %s %u -> %#x (copy %u move %u) from %s", s->name,
tipc ? "tipc" : rq.type == T_CONTROL || rq.type == T_CONTROL_CTX ? "control" : "cmd", rq.cmd,
rs.result, rs.ncopy, rs.nmove, t_req ? ldr_describe(t_req->x[30], d, sizeof d) : "host");
}
respond(m, tipc, &rs);
}
// ---- sm: service manager (CMIF, and TIPC as the SDK's sm client speaks it since 12.0.0)
static void sm_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs) {
switch (rq->cmd) {
case 0: return; // RegisterClient / Initialize (pid)
case 1: { // GetServiceHandle(name)
char name[9] = {0};
memcpy(name, ipc_arg(rq, 0, 8), 8);
for (unsigned i = 0; i < sizeof services / sizeof *services; i++)
if (!strcmp(services[i].name, name)) {
rs->move[rs->nmove++] = ipc_session_new(services[i].name, services[i].handler, NULL, NULL);
rt_log("ipc: opened service %s", services[i].name);
return;
}
fflush(stdout);
ipc_backtrace();
rt_fatal("sm: service \"%s\" is not implemented", name);
}
default: ipc_unimplemented(s, rq);
}
}
// ---- SVCs
int ipc_svc(Cpu *c, uint32_t imm) {
uint64_t *x = c->x;
switch (imm) {
case 0x1F: { // ConnectToNamedPort(x1 name -> w1 handle)
char name[13] = {0};
const char *p = (const char *)(uintptr_t)x[1];
if (!p || !readable(x[1], 1)) rt_fatal("ConnectToNamedPort: name at %#llx", (unsigned long long)x[1]);
for (int i = 0; i < 12 && p[i]; i++) name[i] = p[i];
if (strcmp(name, "sm:")) {
t_req = c;
ipc_backtrace();
rt_fatal("ConnectToNamedPort(\"%s\"): only sm: is served", name);
}
x[0] = 0;
x[1] = ipc_session_new("sm:", sm_handler, NULL, NULL);
return 1;
}
case 0x21: case 0x22: { // SendSyncRequest(w0 handle) / WithUserBuffer(x0 buffer, x1 size, w2 handle)
uint32_t h = (uint32_t)(imm == 0x21 ? x[0] : x[2]);
IpcSession *s = kern_handle_get(h, KERN_SESSION);
if (!s) {
rt_log("ipc: svc %#x on %#x, not a session handle", imm, h);
x[0] = IPC_R_BAD_HANDLE;
return 1;
}
uint32_t *m = (uint32_t *)(uintptr_t)(imm == 0x21 ? c->tpidrro : x[0]);
if (!m || (imm == 0x22 && x[1] < IPC_MSG_BYTES))
rt_fatal("ipc: svc %#x message buffer %p size %#llx", imm, (void *)m, (unsigned long long)x[1]);
const Cpu *outer = t_req;
t_req = c;
dispatch(s, m);
t_req = outer;
x[0] = 0;
return 1;
}
case 0x15: { // CreateTransferMemory(x1 address, x2 size, w3 permission -> w1 handle)
if ((x[1] & 0xfff) || x[1] + x[2] < x[1] || x[1] + x[2] > RT_ADDRESS_LIMIT) {
x[0] = R_INVALID_ADDRESS;
return 1;
}
if (!x[2] || (x[2] & 0xfff)) {
x[0] = R_INVALID_SIZE;
return 1;
}
IpcTransferMemory *t = malloc(sizeof *t);
if (!t) rt_fatal("ipc: transfer memory allocation");
*t = (IpcTransferMemory){x[1], x[2], (uint32_t)x[3]};
x[0] = 0;
x[1] = kern_handle_new(KERN_TMEM, t);
return 1;
}
}
return 0;
}
// ---- --probe-ipc
// Hand-built CMIF/TIPC requests in the guest main thread's TLR go through rt_svc, the entry the lifted SVC wrappers
// use: sm: -> audren:u -> GetWorkBufferSize -> an audio renderer with the null mixer, whose frame event the probe
// times while it sends RequestUpdate every frame. A child process checks the unknown-service fatal.
#define PROBE_FRAMES 200
#define PROBE_CHILD_ENV "NX2NAVI_PROBE_IPC_CHILD"
#define PROBE_UNKNOWN_SERVICE "lm"
#define H_CURRENT_PROCESS 0xFFFF8001u
static int failures;
#define CHECK(cond, ...) do { if (!(cond)) { failures++; printf(" FAIL: " __VA_ARGS__); putchar('\n'); } } while (0)
typedef struct {
unsigned type, cmd, pid, ncopy, na, nb;
uint32_t copy[4];
IpcBuffer a[2], b[2];
const void *data;
uint32_t size;
} Msg;
typedef struct {
uint32_t result, copy[8], move[8];
unsigned ncopy, nmove;
const uint8_t *data;
} Reply;
static uint32_t sv(Cpu *c, uint32_t imm, uint64_t a0, uint64_t a1, uint64_t a2, uint64_t a3) {
c->x[0] = a0;
c->x[1] = a1;
c->x[2] = a2;
c->x[3] = a3;
rt_svc(c, imm, 0);
return (uint32_t)c->x[0];
}
static uint32_t *put_buffer(uint32_t *p, IpcBuffer b) {
p[0] = (uint32_t)b.size;
p[1] = (uint32_t)b.addr;
p[2] = (uint32_t)(((b.addr >> 36) & 0x3fffff) << 2) | (uint32_t)(((b.size >> 32) & 0xf) << 24) |
(uint32_t)(((b.addr >> 32) & 0xf) << 28);
return p + 3;
}
static void msg_build(uint32_t *m, const Msg *q) {
memset(m, 0, IPC_MSG_BYTES);
int tipc = q->type >= T_TIPC_BASE, special = q->pid || q->ncopy;
m[0] = q->type | q->na << 20 | q->nb << 24;
uint32_t *p = m + 2;
if (special) {
*p++ = (q->pid ? 1u : 0u) | q->ncopy << 1;
if (q->pid) p += 2; // the kernel fills the process id in
for (unsigned i = 0; i < q->ncopy; i++) *p++ = q->copy[i];
}
for (unsigned i = 0; i < q->na; i++) p = put_buffer(p, q->a[i]);
for (unsigned i = 0; i < q->nb; i++) p = put_buffer(p, q->b[i]);
uint8_t *words = (uint8_t *)p;
unsigned ndata;
if (tipc) {
memcpy(words, q->data, q->size);
ndata = (q->size + 3) / 4;
} else {
uint8_t *payload = (uint8_t *)m + (((words - (uint8_t *)m) + 15) & ~15);
uint32_t h[4] = {CMIF_IN_MAGIC, 0, q->cmd, 0};
memcpy(payload, h, 16);
if (q->size) memcpy(payload + 16, q->data, q->size);
ndata = (16 + 16 + q->size + 3) / 4; // the 16-byte alignment pad is reserved in the data size
}
m[1] = ndata | (special ? 1u << 31 : 0);
}
static void msg_parse(const uint32_t *m, int tipc, Reply *r) {
memset(r, 0, sizeof *r);
unsigned nx = (m[0] >> 16) & 0xf;
const uint32_t *p = m + 2;
if (m[1] >> 31) {
uint32_t h = *p++;
if (h & 1) p += 2;
r->ncopy = (h >> 1) & 0xf;
r->nmove = (h >> 5) & 0xf;
for (unsigned i = 0; i < r->ncopy; i++) r->copy[i] = *p++;
for (unsigned i = 0; i < r->nmove; i++) r->move[i] = *p++;
}
p += 2 * nx;
if (tipc) {
r->result = p[0];
r->data = (const uint8_t *)(p + 1);
return;
}
const uint8_t *payload = (const uint8_t *)m + ((((const uint8_t *)p - (const uint8_t *)m) + 15) & ~15);
uint32_t h[4];
memcpy(h, payload, 16);
r->result = h[0] == CMIF_OUT_MAGIC ? h[2] : 0xFFFFFFFFu;
r->data = payload + 16;
}
// One request on the TLR message buffer; the kernel result of SendSyncRequest, the service result in r->result.
static uint32_t call(Cpu *c, uint32_t h, const Msg *q, Reply *r) {
uint32_t *m = (uint32_t *)(uintptr_t)c->tpidrro;
msg_build(m, q);
uint32_t k = sv(c, 0x21, h, 0, 0, 0);
if (k) return k;
msg_parse(m, q->type >= T_TIPC_BASE, r);
return 0;
}
static uint32_t cmd(Cpu *c, uint32_t h, unsigned id, const void *data, uint32_t size, Reply *r) {
Msg q = {.type = T_REQUEST, .cmd = id, .data = data, .size = size};
uint32_t k = call(c, h, &q, r);
return k ? k : r->result;
}
static uint32_t close_handle(Cpu *c, uint32_t h) { return sv(c, 0x16, h, 0, 0, 0); }
static uint32_t connect_sm(Cpu *c) {
static const char port[] = "sm:";
uint32_t r = sv(c, 0x1F, 0, (uint64_t)(uintptr_t)port, 0, 0);
CHECK(r == 0 && c->x[1], "ConnectToNamedPort(sm:) %#x", r);
return (uint32_t)c->x[1];
}
static uint32_t get_service(Cpu *c, uint32_t sm, const char *name, int tipc) {
char n[8] = {0};
memcpy(n, name, strlen(name));
Msg q = {.type = tipc ? T_TIPC_BASE + 1 : T_REQUEST, .cmd = 1, .data = n, .size = 8};
Reply r;
uint32_t k = call(c, sm, &q, &r);
CHECK(!k && !r.result && r.nmove == 1, "%s GetServiceHandle(%s): kernel %#x result %#x move %u",
tipc ? "TIPC" : "CMIF", name, k, r.result, r.nmove);
return r.nmove ? r.move[0] : 0;
}
// A NX2navi-like renderer: 48 kHz, 240-sample (5 ms) frames, final mix plus submixes, two sinks, REV11.
static const AudrenParameter g_param = {
.sample_rate = 48000, .sample_count = 240, .mix_buffer_count = 24, .submix_count = 8, .voice_count = 192,
.sink_count = 2, .effect_count = 12, .revision = AUDREN_REV(11)};
typedef struct {
uint8_t *in, *out, *perf;
uint32_t in_size, out_total, voices_out;
int16_t *pcm;
} Update;
static void update_build(Update *u, int first) {
const AudrenParameter *p = &g_param;
uint32_t npools = (uint32_t)p->effect_count + 4u * (uint32_t)p->voice_count, mc = (uint32_t)p->submix_count + 1;
AudrenHeader h = {.revision = p->revision, .behavior = 0x10, .memory_pools = npools * sizeof(AudrenPoolIn),
.voices = (uint32_t)p->voice_count * sizeof(AudrenVoiceIn),
.effects = (uint32_t)p->effect_count * sizeof(AudrenEffectIn), .mixes = mc * sizeof(AudrenMixIn),
.sinks = (uint32_t)p->sink_count * sizeof(AudrenSinkIn), .performance = 0x10};
h.total = sizeof h + h.behavior + h.memory_pools + h.voice_resources + h.voices + h.effects + h.splitters +
h.mixes + h.sinks + h.performance + h.render_info;
u->in_size = h.total;
memset(u->in, 0, h.total);
uint8_t *w = u->in;
memcpy(w, &h, sizeof h);
w += sizeof h + h.behavior + h.memory_pools + h.voice_resources;
for (int i = 0; i < p->voice_count; i++) {
AudrenVoiceIn v = {0}; // the client leaves the id of an unused slot zero
if (i == 0) { // one PCM16 voice with a 100 ms buffer: consumed after 20 frames
v = (AudrenVoiceIn){.id = 0, .is_new = (uint8_t)first, .in_use = 1, .play_state = PLAY_STARTED,
.sample_format = FMT_PCM16, .sample_rate = 48000, .channel_count = 1, .pitch = 1,
.volume = 1, .mix_id = 0, .splitter_id = 0xffffffffu, .wave_buffer_count = 1};
v.wave_buffers[0] = (AudrenWaveBuffer){.address = (uint64_t)(uintptr_t)u->pcm, .size = 4800 * 2,
.end = 4800, .sent = (uint8_t)!first};
for (int k = 1; k < 4; k++) v.wave_buffers[k].sent = 1;
}
memcpy(w, &v, sizeof v);
w += sizeof v;
}
w += h.effects + h.splitters;
AudrenMixIn mix = {.volume = 1, .sample_rate = 48000, .buffer_count = 6, .in_use = 1, .dest_mix_id = 0x7fffffff,
.dest_splitter_id = 0xffffffffu};
memcpy(w, &mix, sizeof mix);
w += h.mixes;
AudrenSinkIn sink = {.type = SINK_DEVICE, .in_use = 1, .input_count = 2, .inputs = {0, 1}};
memcpy(w, &sink, sizeof sink);
u->out_total = (uint32_t)sizeof h + npools * sizeof(AudrenPoolOut) + (uint32_t)p->voice_count * sizeof(AudrenVoiceOut) +
(uint32_t)p->effect_count * 0x90u + (uint32_t)p->sink_count * sizeof(AudrenSinkOut) + 0x10 +
AUDREN_BEHAVIOR_OUT + AUDREN_RENDER_INFO_OUT;
u->voices_out = (uint32_t)sizeof h + npools * sizeof(AudrenPoolOut);
}
static uint32_t request_update(Cpu *c, uint32_t ren, Update *u, uint64_t *frames, AudrenVoiceOut *voice0) {
Msg q = {.type = T_REQUEST, .cmd = 4, .na = 1, .nb = 2};
q.a[0] = (IpcBuffer){(uint64_t)(uintptr_t)u->in, u->in_size, 0};
q.b[0] = (IpcBuffer){(uint64_t)(uintptr_t)u->out, 0x8000, 0};
q.b[1] = (IpcBuffer){(uint64_t)(uintptr_t)u->perf, 0x100, 1};
Reply r;
uint32_t k = call(c, ren, &q, &r);
if (k || r.result) return k ? k : r.result;
AudrenHeader o;
memcpy(&o, u->out, sizeof o);
if (o.revision != g_param.revision || o.total != u->out_total) return 0xFFFFFFFFu;
memcpy(frames, u->out + o.total - AUDREN_RENDER_INFO_OUT, 8);
memcpy(voice0, u->out + u->voices_out, sizeof *voice0);
return 0;
}
static void probe_renderer(Cpu *c, uint32_t audren) {
Reply r;
// GetWorkBufferSize(AudioRendererParameterInternal) -> u64
uint32_t res = cmd(c, audren, 1, &g_param, sizeof g_param, &r);
uint64_t work = 0;
if (!res) memcpy(&work, r.data, 8);
printf("audren:u GetWorkBufferSize(48000 Hz x240, %d voices, %d submixes, %d effects, REV11) -> result %#x, "
"%#llx bytes\n", g_param.voice_count, g_param.submix_count, g_param.effect_count, res,
(unsigned long long)work);
CHECK(!res && work >= 0x1000 && work < (256u << 20) && !(work & 0xfff), "GetWorkBufferSize");
if (res || !work) return;
// OpenAudioRenderer(param, pad, work size, aruid; pid; copy transfer memory, current process)
uint8_t *wb = mem_kernel_alloc(work, 0x1000);
uint32_t tm = 0;
res = sv(c, 0x15, 0, (uint64_t)(uintptr_t)wb, work, 0);
tm = (uint32_t)c->x[1];
CHECK(!res, "CreateTransferMemory %#x", res);
struct { AudrenParameter p; uint32_t pad; uint64_t work, aruid; } open = {g_param, 0, work, 0};
Msg q = {.type = T_REQUEST, .cmd = 0, .pid = 1, .ncopy = 2, .copy = {tm, H_CURRENT_PROCESS}, .data = &open,
.size = sizeof open};
long live0 = ipc_sessions_live();
uint64_t released0 = ipc_sessions_released();
uint32_t k = call(c, audren, &q, &r);
CHECK(!k && !r.result && r.nmove == 1 && audren_renderers_live() == 1, "OpenAudioRenderer: kernel %#x result %#x",
k, r.result);
if (k || r.result || !r.nmove) return;
uint32_t ren = r.move[0];
int32_t rate = 0, count = 0;
if (!cmd(c, ren, 0, NULL, 0, &r)) memcpy(&rate, r.data, 4);
if (!cmd(c, ren, 1, NULL, 0, &r)) memcpy(&count, r.data, 4);
CHECK(rate == 48000 && count == 240, "GetSampleRate %d, GetSampleCount %d", rate, count);
res = cmd(c, ren, 7, NULL, 0, &r); // QuerySystemEvent
uint32_t ev = !res && r.ncopy == 1 ? r.copy[0] : 0;
CHECK(ev, "QuerySystemEvent: result %#x, %u copy handles", res, r.ncopy);
Update u = {mem_kernel_alloc(0x40000, 64), mem_kernel_alloc(0x8000, 64), mem_kernel_alloc(0x100, 64), 0, 0, 0,
mem_kernel_alloc(4800 * 2, 64)};
for (int i = 0; i < 4800; i++) u.pcm[i] = (int16_t)(i * 7);
uint64_t frames = ~0ull;
AudrenVoiceOut v0 = {0};
update_build(&u, 1);
res = request_update(c, ren, &u, &frames, &v0);
CHECK(!res && frames == 0, "RequestUpdate before Start: %#x, %llu frames", res, (unsigned long long)frames);
update_build(&u, 0);
CHECK(!cmd(c, ren, 5, NULL, 0, &r), "Start");
uint32_t state = 1;
if (!cmd(c, ren, 3, NULL, 0, &r)) memcpy(&state, r.data, 4);
CHECK(state == 0, "GetState after Start: %u", state);
// Every frame, as the game's audio thread does: wait for the event, clear it, RequestUpdate.
uint32_t hs[1] = {ev};
sv(c, 0x17, ev, 0, 0, 0);
uint64_t t0 = 0, t1 = 0, f0 = 0, f1 = 0, worst = 0, prev = 0;
int waits_ok = 0, updates_ok = 0;
for (int i = 0; i < PROBE_FRAMES; i++) {
uint32_t w = sv(c, 0x18, 0, (uint64_t)(uintptr_t)hs, 1, 1000000000ull);
uint64_t now = kern_now_ns();
waits_ok += w == 0;
sv(c, 0x17, ev, 0, 0, 0);
uint64_t f = 0;
updates_ok += request_update(c, ren, &u, &f, &v0) == 0;
if (i == 0) t0 = now, f0 = f;
if (i && now - prev > worst) worst = now - prev;
prev = t1 = now;
f1 = f;
}
double period = (double)(t1 - t0) / (PROBE_FRAMES - 1) / 1e6, expect = 1e3 * g_param.sample_count / g_param.sample_rate;
double rendered = (double)(f1 - f0), elapsed = (double)(t1 - t0) / 1e6 / expect;
printf("render tick: %d waits on the frame event (%d ok), %d RequestUpdate (%d ok); mean period %.3f ms (expect "
"%.3f), worst gap %.3f ms; frames rendered %llu over %.1f periods\n", PROBE_FRAMES, waits_ok, PROBE_FRAMES,
updates_ok, period, expect, (double)worst / 1e6, (unsigned long long)(f1 - f0), elapsed);
CHECK(waits_ok == PROBE_FRAMES && updates_ok == PROBE_FRAMES, "event waits / updates");
CHECK(period > expect * 0.9 && period < expect * 1.1, "frame event period %.3f ms", period);
CHECK(rendered > elapsed - 3 && rendered < elapsed + 3, "frames rendered %.0f vs elapsed %.1f", rendered, elapsed);
printf("voice 0 (PCM16, 4800 samples): played %llu, wave buffers consumed %u\n",
(unsigned long long)v0.played_samples, v0.wave_buffers_consumed);
CHECK(v0.played_samples == 4800 && v0.wave_buffers_consumed == 1, "voice clock with the null mixer");
CHECK(!cmd(c, ren, 6, NULL, 0, &r), "Stop");
if (!cmd(c, ren, 3, NULL, 0, &r)) memcpy(&state, r.data, 4);
CHECK(state == 1, "GetState after Stop: %u", state);
res = close_handle(c, ren);
long live1 = ipc_sessions_live();
uint64_t released1 = ipc_sessions_released();
printf("CloseHandle(IAudioRenderer) -> %#x: sessions %ld -> %ld, released +%llu, renderers running %ld\n", res,
live0 + 1, live1, (unsigned long long)(released1 - released0), audren_renderers_live());
CHECK(!res && live1 == live0 && released1 == released0 + 1 && audren_renderers_live() == 0, "renderer close");
CHECK(!close_handle(c, ev) && !close_handle(c, tm), "close event and transfer memory");
mem_kernel_free(u.in, 0x40000);
mem_kernel_free(u.out, 0x8000);
mem_kernel_free(u.perf, 0x100);
mem_kernel_free(u.pcm, 4800 * 2);
mem_kernel_free(wb, work);
}
static void probe_main(Cpu *c) {
Reply r;
uint32_t sm = connect_sm(c);
Msg reg = {.type = T_REQUEST, .cmd = 0, .pid = 1, .data = (const uint64_t[]){0}, .size = 8};
uint32_t k = call(c, sm, &reg, &r);
CHECK(!k && !r.result, "sm: RegisterClient: kernel %#x result %#x", k, r.result);
uint32_t audren = get_service(c, sm, "audren:u", 0);
uint32_t dup = get_service(c, sm, "audren:u", 1);
printf("sm: connected, RegisterClient ok, GetServiceHandle(audren:u) -> %#x (CMIF), %#x (TIPC)\n", audren, dup);
uint64_t released0 = ipc_sessions_released();
CHECK(!close_handle(c, dup) && ipc_sessions_released() == released0 + 1, "CloseHandle(TIPC audren:u)");
Msg qpb = {.type = T_CONTROL, .cmd = 3};
k = call(c, audren, &qpb, &r);
uint16_t pbs = 0;
if (!k && !r.result) memcpy(&pbs, r.data, 2);
CHECK(pbs == POINTER_BUFFER_SIZE, "QueryPointerBufferSize %#x", pbs);
CHECK(sv(c, 0x21, 0x0fff, 0, 0, 0) == IPC_R_BAD_HANDLE, "SendSyncRequest on a bad handle");
if (audren) probe_renderer(c, audren);
// csrng: GenerateRandomBytes into a B buffer, as the SDK sends it; two requests give different bytes
uint32_t csrng = get_service(c, sm, "csrng", 0);
uint8_t *rnd = mem_kernel_alloc(64, 16);
memset(rnd, 0, 64);
for (int i = 0; csrng && i < 2; i++) {
Msg q = {.type = T_REQUEST, .cmd = 0, .nb = 1, .b[0] = {(uint64_t)(uintptr_t)(rnd + 32 * i), 32, 0}};
k = call(c, csrng, &q, &r);
CHECK(!k && !r.result, "csrng GenerateRandomBytes: kernel %#x result %#x", k, r.result);
}
static const uint8_t zero[32];
CHECK(memcmp(rnd, zero, 32) && memcmp(rnd + 32, zero, 32) && memcmp(rnd, rnd + 32, 32),
"csrng: the buffers were not filled with random bytes");
printf("csrng: GenerateRandomBytes filled two buffers of 32 bytes, each differently\n");
mem_kernel_free(rnd, 64);
CHECK(!csrng || !close_handle(c, csrng), "close csrng");
CHECK(!close_handle(c, audren) && !close_handle(c, sm), "close audren:u and sm:");
printf("sessions alive at the end: %ld, released %llu\n", ipc_sessions_live(),
(unsigned long long)ipc_sessions_released());
CHECK(ipc_sessions_live() == 0, "sessions leaked");
}
static void child_main(Cpu *c) {
uint32_t sm = connect_sm(c);
get_service(c, sm, PROBE_UNKNOWN_SERVICE, 0);
printf("child: GetServiceHandle(%s) returned\n", PROBE_UNKNOWN_SERVICE);
}
// Re-run this executable with the same arguments and PROBE_CHILD_ENV set; it must stop with the named fatal.
static void probe_unknown_service(void) {
static char out[65536];
int code = plat_run_self(PROBE_CHILD_ENV, "1", out, sizeof out, 30000);
if (code < 0) rt_fatal("probe-ipc: cannot start the child");
const char *want = "FATAL: sm: service \"" PROBE_UNKNOWN_SERVICE "\" is not implemented";
const char *line = strstr(out, want);
printf("child GetServiceHandle(%s): exit code %d, %s\n", PROBE_UNKNOWN_SERVICE, code,
line ? "named fatal seen" : "named fatal MISSING");
if (line) printf(" child: %.*s\n", (int)strcspn(line, "\r\n"), line);
CHECK(code == 70 && line, "unknown service fatal (child output: %s)", out);
}
// ---- the game's own nn::audio client: main's PLT stubs -> lifted sdk -> SVCs -> this file
#define SYM_INIT_PARAM "_ZN2nn5audio32InitializeAudioRendererParameterEPNS0_22AudioRendererParameterE"
#define SYM_WORK_SIZE "_ZN2nn5audio30GetAudioRendererWorkBufferSizeERKNS0_22AudioRendererParameterE"
#define SYM_OPEN "_ZN2nn5audio17OpenAudioRendererEPNS0_19AudioRendererHandleEPNS_2os11SystemEventERKNS0_22AudioRendererParameterEPvm"
#define SYM_CONFIG_SIZE "_ZN2nn5audio36GetAudioRendererConfigWorkBufferSizeERKNS0_22AudioRendererParameterE"
#define SYM_CONFIG_INIT "_ZN2nn5audio29InitializeAudioRendererConfigEPNS0_19AudioRendererConfigERKNS0_22AudioRendererParameterEPvm"
#define SYM_START "_ZN2nn5audio18StartAudioRendererENS0_19AudioRendererHandleE"
#define SYM_STOP "_ZN2nn5audio17StopAudioRendererENS0_19AudioRendererHandleE"
#define SYM_UPDATE "_ZN2nn5audio26RequestUpdateAudioRendererENS0_19AudioRendererHandleEPKNS0_19AudioRendererConfigE"
#define SYM_FRAMES "_ZN2nn5audio33GetAudioRendererElapsedFrameCountEPKNS0_19AudioRendererConfigE"
#define SYM_CLOSE "_ZN2nn5audio18CloseAudioRendererENS0_19AudioRendererHandleE"
#define SYM_WAIT_EVENT "_ZN2nn2os15WaitSystemEventEPNS0_15SystemEventTypeE"
static int revision_number_of(uint32_t rev) { return (int)(rev >> 24) - '0'; }
static uint64_t stub(const char *sym) {
uint64_t a = ldr_plt_stub("main", sym);
if (!a) rt_fatal("probe-ipc: no PLT stub for %s in main", sym);
return a;
}
// Without nn::init::Start the SDK's own initialisation has not run; the probe sets up the two things the audio client
// path reads (found by running it):
// - the "current thread object" at TLR+0x1F8 is the kernel's zeroed placeholder. SdkMutex compares its lock tag with
// ThreadType+0x1B0 (sdk+0x13186c), which nnosInitialize sets to the thread's handle; a zero there makes every free
// mutex look owned by the caller (__cxa_guard_acquire in nn::sm::Initialize aborts);
// - sdk's full DT_INIT needs nn::os initialised (its TLS slot constructor reaches ThreadManager), which is the boot
// sequence's job. The audio client only needs its two sf heap constructors: 16 KiB ExpHeaps on static buffers,
// stored at sdk+0xd37e90 and sdk+0xd3bec0 (CreateAudioRendererManagerByHipc reads the second). The 1.4.3 SDK creates
// that heap lazily; its OpenAudioRenderer takes nn::audio::detail::g_RendererResourceSemaphore first, which the
// constructor at sdk+0x221fb0 initializes (two renderers).
static void sdk_prepare(Cpu *c) {
Cpu *again;
ST32(LD64(c->tpidrro + 0x1F8) + 0x1B0, kern_main_thread(&again));
const RtLoaded *sdk = ldr_module(2);
static const uint64_t ctors_121[] = {0x1dcbd0, 0x1de8a0}, ctors_143[] = {0x221fb0};
const uint64_t *heap_ctors = RT_V143 ? ctors_143 : ctors_121;
unsigned nctors = RT_V143 ? sizeof ctors_143 / sizeof *ctors_143 : sizeof ctors_121 / sizeof *ctors_121;
for (unsigned k = 0; k < nctors; k++) {
int found = 0;
for (uint64_t e = 0; e < sdk->init_array_size; e += 8)
found |= LD64(sdk->init_array + e) == sdk->base + heap_ctors[k];
if (!found) rt_fatal("probe-ipc: sdk+%#llx is not an sdk constructor", (unsigned long long)heap_ctors[k]);
rt_call(c, sdk->base + heap_ctors[k], 0);
}
}
static void probe_sdk_client(Cpu *c) {
unsigned trace = rt_trace;
rt_trace |= RT_TRACE_IPC; // log the requests the SDK sends during set-up
sdk_prepare(c);
uint64_t requests0 = (uint64_t)plat_cas64(&g_requests, 0, 0);
uint64_t released0 = ipc_sessions_released();
int32_t *param = mem_kernel_alloc(0x100, 64);
rt_call(c, stub(SYM_INIT_PARAM), 1, (uint64_t)(uintptr_t)param);
printf("sdk: InitializeAudioRendererParameter -> %d Hz x%d, mix buffers %d, revision word %#x (REV%d)\n", param[0],
param[1], param[2], (uint32_t)param[13], revision_number_of((uint32_t)param[13]));
CHECK(param[0] == 48000 && param[1] == 240 && (uint32_t)param[13] == AUDREN_REV(RT_V143 ? 15 : 11),
"SDK default parameter (NX2navi 1.2.1 builds REV11 requests, 1.4.3 REV15)");
param[2] = g_param.mix_buffer_count, param[3] = g_param.submix_count, param[4] = g_param.voice_count;
param[5] = g_param.sink_count, param[6] = g_param.effect_count;
uint64_t work = rt_call(c, stub(SYM_WORK_SIZE), 1, (uint64_t)(uintptr_t)param);
printf("sdk: GetAudioRendererWorkBufferSize -> %#llx (service %#x + client part)\n", (unsigned long long)work,
AUDREN_WORK_BUFFER_BYTES);
CHECK(work >= AUDREN_WORK_BUFFER_BYTES && work < (64u << 20), "SDK GetAudioRendererWorkBufferSize %#llx",
(unsigned long long)work);
uint8_t *wb = mem_kernel_alloc(work, 0x1000), *event = mem_kernel_alloc(0x100, 64);
uint64_t *handle = mem_kernel_alloc(0x40, 64); // AudioRendererHandle: 16 bytes, passed in x0 and x1
uint32_t r = (uint32_t)rt_call(c, stub(SYM_OPEN), 5, (uint64_t)(uintptr_t)handle, (uint64_t)(uintptr_t)event,
(uint64_t)(uintptr_t)param, (uint64_t)(uintptr_t)wb, work);
printf("sdk: OpenAudioRenderer -> %#x, renderers running %ld\n", r, audren_renderers_live());
CHECK(!r && handle[0] && audren_renderers_live() == 1, "SDK OpenAudioRenderer %#x", r);
if (r) return;
uint64_t csize = rt_call(c, stub(SYM_CONFIG_SIZE), 1, (uint64_t)(uintptr_t)param);
uint8_t *cbuf = mem_kernel_alloc(csize, 0x1000), *config = mem_kernel_alloc(0x200, 64);
rt_call(c, stub(SYM_CONFIG_INIT), 4, (uint64_t)(uintptr_t)config, (uint64_t)(uintptr_t)param,
(uint64_t)(uintptr_t)cbuf, csize);
rt_call(c, stub(SYM_START), 2, handle[0], handle[1]);
uint64_t t0 = 0, t1 = 0;
int bad = 0, f0 = 0, f1 = 0;
for (int i = 0; i < PROBE_FRAMES; i++) {
rt_call(c, stub(SYM_WAIT_EVENT), 1, (uint64_t)(uintptr_t)event);
uint64_t now = kern_now_ns();
bad += rt_call(c, stub(SYM_UPDATE), 3, handle[0], handle[1], (uint64_t)(uintptr_t)config) != 0;
int f = (int)rt_call(c, stub(SYM_FRAMES), 1, (uint64_t)(uintptr_t)config);
if (!i) t0 = now, f0 = f;
t1 = now, f1 = f;
rt_trace = trace; // the first update is logged, the rest is quiet
}
double period = (double)(t1 - t0) / (PROBE_FRAMES - 1) / 1e6;
printf("sdk: %d x WaitSystemEvent + RequestUpdateAudioRenderer: %d failed, mean period %.3f ms, elapsed frames "
"%d -> %d\n", PROBE_FRAMES, bad, period, f0, f1);
CHECK(!bad && period > 4.5 && period < 5.5 && f1 - f0 >= PROBE_FRAMES - 4 && f1 - f0 <= PROBE_FRAMES + 2,
"SDK render loop");
rt_call(c, stub(SYM_STOP), 2, handle[0], handle[1]);
rt_call(c, stub(SYM_CLOSE), 2, handle[0], handle[1]);
uint64_t requests = (uint64_t)plat_cas64(&g_requests, 0, 0) - requests0;
printf("sdk: CloseAudioRenderer: renderers running %ld, sessions released +%llu; %llu requests served for the "
"lifted client (sm: kept open by the SDK: %ld session)\n", audren_renderers_live(),
(unsigned long long)(ipc_sessions_released() - released0), (unsigned long long)requests,
ipc_sessions_live());
// The 1.2.1 SDK closes the renderer, device and manager sessions and keeps sm; the 1.4.3 SDK keeps the
// audren:u manager it created lazily, beside sm.
CHECK(audren_renderers_live() == 0 && ipc_sessions_released() - released0 >= (RT_V143 ? 1u : 3u) &&
ipc_sessions_live() == (RT_V143 ? 2 : 1),
"SDK CloseAudioRenderer");
rt_trace = trace;
}
static void guest_main(Cpu *c) {
probe_main(c);
probe_sdk_client(c);
}
int probe_ipc(void) {
if (getenv(PROBE_CHILD_ENV)) {
kern_run_on_guest_main(child_main);
return 0;
}
ldr_load(rt_config()->exefs);
kern_run_on_guest_main(guest_main);
probe_unknown_service();
RtStats s = rt_stats();
printf("stats: %llu SVCs, %llu threads, %llu host calls\n", (unsigned long long)s.svcs,
(unsigned long long)s.threads, (unsigned long long)s.host_calls);
if (failures) {
printf("probe-ipc: FAILED (%d failures)\n", failures);
return 1;
}
printf("probe-ipc: passed\n");
return 0;
}
// Create the applet state-changed event at boot and queue the launch message: the application starts in focus
// (the applet manager: SetFocusState(InFocus) -> FocusStateChanged for an application).
void ipc_applet_event_init(void) {
if (!g_applet_event) {
g_applet_event_w = kern_event_new();
g_applet_event = kern_event_readable(g_applet_event_w);
g_applet_focus = AM_FOCUS_IN;
ipc_applet_push_message(AM_MSG_FOCUS_STATE_CHANGED);
}
}