diff --git a/.gitignore b/.gitignore index 9162c0d..7aeec05 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ notify.txt src/notify_icon_asset.c refs/* docs/* +*.so diff --git a/Makefile b/Makefile index 50dbdf8..98a39bf 100644 --- a/Makefile +++ b/Makefile @@ -10,14 +10,15 @@ CFLAGS += -DSHADOWMOUNT_VERSION=\"$(VERSION_TAG)\" # Linker LDFLAGS := -flto=thin -Wl,--gc-sections -# Standard Libraries Only -LIBS := -lSceNotification -lSceSystemService -lSceUserService -lSceAppInstUtil -lsqlite3 +# Standard libraries only. +LIBS := -lSceNotification -lSceSystemService -lSceUserService -lSceAppInstUtil -lsqlite3 -lSceIpmi PS5_SCE_STUBS_DIR ?= $(PS5_PAYLOAD_SDK)/src/sce_stubs KERNEL_SYS_STUB_SO := src/libkernel_sys_ext.so KERNEL_SYS_STUB_SRCS := $(PS5_SCE_STUBS_DIR)/libkernel_sys.c src/libkernel_sys_ext.c ASSET_SRCS := src/notify_icon_asset.c src/config_ini_example_asset.c -SRCS := src/main.c $(wildcard src/sm_*.c) $(ASSET_SRCS) +IPMI_SRCS := src/ipmi_symbols.c src/ipmi_client.c src/ipmi_handler.c +SRCS := src/main.c $(wildcard src/sm_*.c) $(IPMI_SRCS) $(ASSET_SRCS) OBJS := $(SRCS:.c=.o) HEADERS := $(wildcard include/*.h) diff --git a/include/ipmi.h b/include/ipmi.h new file mode 100644 index 0000000..af6e9f3 --- /dev/null +++ b/include/ipmi.h @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// +// The IPMI server interface. There is no SDK header for it -- sys/ipmi.h is +// FreeBSD's BMC driver header and unrelated -- so the types are declared here, +// and every offset and size in them was confirmed against a live, working +// registration on hardware. +// +// EventHandler is deliberately not declared as a class. The signatures of its +// methods are known but their order is not, so a hand-written subclass would be +// a guess at the vtable layout. ipmi_symbols.h resolves the base vtable and each +// method by name, and ipmi_handler.c identifies the slots by address. + +#pragma once + +#include +#include + +// A {pointer, length} pair. The client builds an array of these and hands it to +// invokeSyncMethod; the server receives the same array. Confirmed from the +// client marshal in libSceAppContent (0x1300) and from a working client. +typedef struct IpmiDataInfo { + const void *data; + size_t size; +} IpmiDataInfo; + +typedef struct IpmiBufferInfo { + void *data; + size_t size; +} IpmiBufferInfo; + +// The server-side out-argument entry: 24 bytes, not 16. The in and out argument +// arrays a dispatch receives do not share a stride. +// +// The framework fills only `data` and `capacity` and leaves `written` +// uninitialised for the handler. Forgetting it is fatal: the first command with +// an out-arg died inside respondToSyncMethodRequest reading stack garbage as a +// length (IPMIMGR signo=0xa0020320 opt32=0x0232000a). +// +// The client side uses a 16-byte {ptr,size} instead. They are separate +// in-process structs and need not match. +typedef struct IpmiOutBuffer { + void *data; + size_t capacity; + size_t written; +} IpmiOutBuffer; + +// Opaque on purpose. We only ever hold pointers to these and call through the +// vtable slots resolved by address; nothing reads a field. +typedef struct IpmiSession IpmiSession; +typedef struct IpmiEventHandler IpmiEventHandler; + +// IPMI::Server::Config -- 0x38 bytes. Offsets are load-bearing; do not reorder. +typedef struct IpmiServerConfig { + uint64_t unknown00; // +0x00 ctor writes 0xf00, never overwritten + uint64_t poolSize; // +0x08 0x20000 is a known-good value + // +0x10 the event handler. create() returns EINVAL with this null and + // succeeds with an EventHandler* here, which is why create() takes no + // handler argument: the handler travels in the Config. + IpmiEventHandler *eventHandler; + uint8_t flag; // +0x18 must be 1 + char name[16]; // +0x19 service name, NUL padded + // The tail is byte arrays, not scalars: `name` ends at the unaligned offset + // 0x29, so a uint64_t there is aligned up to 0x30 and silently grows the + // struct to 0x40. The assertion below caught exactly that. + uint8_t reserved29[8]; // +0x29 written 0 + uint8_t reserved31; // +0x31 written 0 + // create() reads both of these. gate32 must be zero: it is copied to + // ServerImpl+0x28, and tryDispatch refuses to run on a non-zero value. + // gate33 is only consulted when `flag` is zero. Measured 0/0 in a working + // registration; zeroing the whole Config keeps them that way. + uint8_t gate32; // +0x32 + uint8_t gate33; // +0x33 + uint8_t pad[4]; // to 0x38 +} IpmiServerConfig; + +_Static_assert(sizeof(IpmiServerConfig) == 0x38, + "Config layout is fixed by the ABI; do not resize or reorder"); +_Static_assert(sizeof(IpmiOutBuffer) == 24, + "server out-arg stride is 0x18, confirmed on hardware"); +_Static_assert(offsetof(IpmiServerConfig, gate32) == 0x32, + "create() reads this byte; ServerImpl+0x28 gates tryDispatch"); + +// Config's constructor, which writes 0xf00 to +0x00. Reached through an +// asm-labelled declaration so it can be re-run on the same storage. +void ipmi_server_config_ctor(IpmiServerConfig *cfg) + __asm__("_ZN4IPMI6Server6ConfigC1Ev"); + +// Sizes the working buffer the dispatcher wants; measured 0x20100. Called after +// create(), and the result allocated, before dispatching. +// Returns uint64_t rather than size_t deliberately: if the firmware returns a +// 32-bit value the upper half of RAX is undefined, and the caller checks for +// exactly that rather than trusting it. +uint64_t ipmi_server_config_estimate(const IpmiServerConfig *cfg) + __asm__("_ZNK4IPMI6Server6Config29estimateTempWorkingMemorySizeEv"); + +// create(&out, &cfg, NULL, storage). +// +// `storage` is NOT scratch: create() placement-constructs a ServerImpl into it +// and returns that same pointer as the Server*, so it must outlive the server. +// Measured -- out == storage, and the object occupies 0x30 bytes: vtable +0x00, +// serverKid +0x08, mutex +0x10, status +0x18, temp buffer +0x20, gate +0x28. +// `out` is a Server** in the real declaration; void** here because nothing +// dereferences a Server except through its vtable. +int ipmi_server_create(void **out, const IpmiServerConfig *cfg, void *p3, + void *initBuf) + __asm__("_ZN4IPMI6Server6createEPPS0_PKNS0_6ConfigEPvS6_"); diff --git a/include/ipmi_client.h b/include/ipmi_client.h new file mode 100644 index 0000000..e1a398e --- /dev/null +++ b/include/ipmi_client.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// +// The IPMI client half. Only used to probe whether our own service name is +// already held, but kept general so a second caller inherits the fixes. + +#pragma once + +#include "ipmi.h" +#include "ipmi_symbols.h" + +#include + +typedef struct IpmiClient { + void* handle; + void* storage; + int connectSlot; // all four resolved by address, never by a + int invokeSlot; // hardcoded index + int destroySlot; +} IpmiClient; + +// Creates a client for `name`. Logs every step, including the vtable slot +// numbers, and returns false having said why on any failure. +bool ipmi_client_open(IpmiClient* c, const IpmiSyms* syms, const char* name, + bool dumpVtable); + +// Connects. Logs before calling, so that if connect ever blocks the last line +// in the log is the answer. +bool ipmi_client_connect(IpmiClient* c, const char* name); + +// Destroys and frees. Idempotent, and safe on a client that never connected -- +// the callers that need it most are error paths. +void ipmi_client_close(IpmiClient* c); diff --git a/include/ipmi_handler.h b/include/ipmi_handler.h new file mode 100644 index 0000000..ecc0415 --- /dev/null +++ b/include/ipmi_handler.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// +// Assembles an IPMI::Server::EventHandler without ever declaring one. +// +// A hand-written subclass would be a guess at the vtable layout, and create() +// accepting one is not validation. So we copy the firmware's own EventHandler +// vtable and replace the slots we can name, naming each by comparing its value +// against the address dlsym gave for that exported method. Slots that cannot be +// named keep a logging thunk, so a surprise is visible rather than silent. + +#pragma once + +#include "ipmi.h" +#include "ipmi_symbols.h" + +#include + +typedef struct HandlerBuild { + IpmiEventHandler* handler; // null if the layout was not provable + int slotCount; // virtuals found in the base vtable + bool syncDispatchProven; // the slot we actually need to serve +} HandlerBuild; + +// Reads the base vtable, identifies every slot it can, logs the result as a +// table, and builds our object. Never returns a handler built on a layout it +// could not read. +HandlerBuild handler_build(const IpmiSyms* syms); + +// Logs anything the connect callback recorded. Called from the dispatcher loop, +// because the callback itself must not log: logf_ does klog plus file I/O to +// /data, and it runs inside the window the kernel gives the server to answer a +// connection request. With logging in the callback every connect was refused. +void handler_drain_connect_log(void); diff --git a/include/ipmi_log.h b/include/ipmi_log.h new file mode 100644 index 0000000..c16a94d --- /dev/null +++ b/include/ipmi_log.h @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// +// The logging seam the IPMI files use: a shim onto the existing log_debug(), +// not a second logging system. Both are defined in src/sm_env_ipmi.c. + +#pragma once + +#include + +void logf_(const char *fmt, ...) __attribute__((format(printf, 1, 2))); + +void log_hexdump(const char *label, const void *p, size_t n); diff --git a/include/ipmi_symbols.h b/include/ipmi_symbols.h new file mode 100644 index 0000000..4b427db --- /dev/null +++ b/include/ipmi_symbols.h @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// +// Runtime symbol resolution against the firmware's libSceIpmi. +// +// libSceIpmi exports the functions that occupy the vtable slots we care about, +// so a slot can be identified by comparing its value against the address the +// loader bound that export to -- no offset arithmetic and no waiting for a +// dispatch to arrive at the wrong method. +// +// Taking the address of a member directly would not work: that yields a PLT +// stub inside this payload, not the firmware function. dlsym returns what the +// loader actually resolved. It takes the plain symbol string and NID-hashes it +// internally, and IPMI import NIDs hash the mangled name, so the mangled names +// go in verbatim. + +#pragma once + +#include + +typedef struct IpmiSyms { + // The handler interface we subclass by hand. evhVtable is the vtable object + // itself; the rest are its virtual methods. Both dispatch methods are + // overloaded: one form takes the {ptr,len} descriptor arrays, the other raw + // pointer+length pairs. We implement the descriptor form, which is the + // one a real client has been served on; the raw forms are hooked anyway so a + // dispatch arriving there is visible instead of silent. + void* evhVtable; + void* evhD1; + void* evhD0; + void* evhD2; + void* evhSyncDataInfo; + void* evhSyncRaw; + void* evhAsyncDataInfo; + void* evhAsyncRaw; + void* evhSessionKilled; + + // The concrete Server type create() returns. tryDispatch is the one we + // call; runDispatcher and shutdownDispatcher are resolved only so a vtable + // dump reads as names, because calling runDispatcher makes the process + // unkillable and shutdownDispatcher makes destroy() refuse. See the + // dispatcher loop in sm_env_ipmi.c. + void* srvRunDispatcher; + void* srvShutdownDispatcher; + void* srvTryDispatch; + void* srvCreateSession; + void* srvGetUserData; + void* srvDestroy; + void* srvD0; + void* srvD1; + + // The concrete Session type a dispatch hands us. respondToSyncMethodRequest + // is the reply path and is called on every sync dispatch, located by address + // in that session's own vtable; the rest are resolved so the vtable dump + // reads as names rather than hex. + void* sessRespondSyncBuf; + void* sessRespondSyncRaw; + void* sessGetClientPid; + void* sessGetServer; + void* sessDestroy; + void* sessIsPeerPrivileged; + + // The client half. Resolved so it finds connect and destroy by address + // rather than by a hardcoded vtable index. + void* clientCreate; + void* clientConfigCtor; + void* clientConfigEstimate; + void* cliConnect; + void* cliDisconnect; + void* cliTerminateConnection; + void* cliDestroy; + void* cliInvokeSyncDataInfo; + void* cliInvokeSyncRaw; + void* cliInvokeAsyncDataInfo; + + // Resolved so a vtable dump can print offsets relative to a known export. + void* serverCreate; + void* serverConfigCtor; +} IpmiSyms; + +// Resolves every symbol above and logs each lookup individually. Returns false +// only when the library itself could not be opened -- individual misses are +// reported as null and left for the caller to judge, because which ones matter +// depends on what the caller is about to do. +bool ipmi_syms_resolve(IpmiSyms* s); + +// Reverse lookup: the short name of whatever `addr` is, or NULL. Used to +// annotate vtable dumps so they read as names instead of hex. +const char* ipmi_syms_name(const IpmiSyms* s, const void* addr); + +// Dumps an object's vtable, each slot annotated by ipmi_syms_name and offset +// from Server::create so the dump is comparable across firmwares. +void ipmi_dump_vtable(const IpmiSyms* s, const void* obj, const char* label, int slots); + +// Index of the slot in obj's vtable holding `fn`, or -1. This is how every slot +// this payload calls is chosen, rather than by a hardcoded index. +int ipmi_vtable_slot_of(const void* obj, const void* fn, int slots); diff --git a/include/sm_env_ipmi.h b/include/sm_env_ipmi.h new file mode 100644 index 0000000..4b25646 --- /dev/null +++ b/include/sm_env_ipmi.h @@ -0,0 +1,103 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * + * The environment service: what ShadowMountPlus tells a sandboxed title about + * the console it is running on. A backported title needs to know whether the + * loaded kstuff carries the getSceSysDirPath and trophy patches its NP + * registrations depend on, and it cannot find that out from inside the sandbox. + */ +#ifndef SM_ENV_IPMI_H +#define SM_ENV_IPMI_H + +#include +#include + +/* + * The wire contract. Every client carries its own copy of this block, and a + * copy that drifts from this one does not fail at build time -- it fails on + * hardware, as a struct one side fills and the other misreads. Change this and + * you change every client with it. + */ + +/* Two rules, both learned by breaking them. + * + * The name must start with "Sce". IPMIMGR kills the caller from inside create() + * otherwise, having logged: + * + * [IPMIMGR] ERROR: [Bug #140942] IPMI server name created by the system + * process must be given "Sce" prefix. + * + * A payload started by the ELF loader counts as a system process. Spelled as a + * concatenation so the prefix cannot be dropped without deleting it from this + * line. + * + * It must also fit Config::name[16] including the NUL. The server truncates + * with strncpy(size - 1) and the client with a flat 16-byte memcpy, so a name + * at the boundary registers under one spelling and is looked up under another, + * and every connect answers ESRCH against a service that is registered and + * serving. 14 is the budget, asserted on both sides. */ +#define SMP_ENV_IPMI_SERVICE_PREFIX "Sce" +#define SMP_ENV_IPMI_SERVICE SMP_ENV_IPMI_SERVICE_PREFIX "ShadowMnt" + +/* The one command. A gate that needs a conversation is a gate that can hang + * halfway through one. */ +#define SMP_ENV_IPMI_CMD_QUERY 0x534D5001u + +/* Bumped when the layout or a field's meaning changes. A client that meets a + * reply_version it does not know refuses to judge rather than guessing. + * 3: kstuff reported as measured capabilities, not a variant plus version. */ +#define SMP_ENV_REPLY_VERSION 3u + +/* Which optional SceShellCore patches the loaded kstuff actually applied. These + * are the difference between kstuff-lite and full kstuff, and they are read out + * of ShellCore's live text -- see sm_kstuff_caps.c. */ +#define SMP_KSTUFF_CAP_SYSDIRPATH (1u << 0) +#define SMP_KSTUFF_CAP_TROPHY (1u << 1) + +/* PRESENT: the sysentvec probe recognised kstuff's toggle, so it really is + * loaded. ENABLED: both sysentvecs are on -- this server toggles them itself + * around some launches. CAPS_VALID: kstuff_caps was measured; without it the + * capabilities are unknown, which is not the same as absent. */ +#define SMP_ENV_FLAG_KSTUFF_PRESENT (1u << 0) +#define SMP_ENV_FLAG_KSTUFF_ENABLED (1u << 1) +#define SMP_ENV_FLAG_CAPS_VALID (1u << 2) + +/* major*1000000 + minor*1000 + patch, compared component-wise. */ +#define SMP_ENV_VERSION(maj, min, pat) \ + ((uint32_t)(maj) * 1000000u + (uint32_t)(min) * 1000u + (uint32_t)(pat)) + +/* This build's own version, hand-maintained: bump on every release and never + * let it go backwards. Not derived from SHADOWMOUNT_VERSION because that is + * `git describe` output and tags like `1.6beta16` have no orderable parse. + * A pre-release takes the previous minor with a high patch (1.5.916). */ +#define SMP_ENV_SMP_VERSION SMP_ENV_VERSION(1, 6, 0) /* 1.6 */ + +/* Fixed size, no pointers: this crosses a process boundary as raw bytes. */ +typedef struct SmpEnvReply { + uint32_t reply_version; + uint32_t flags; + uint32_t kstuff_caps; + uint32_t smp_version_num; + char smp_version[32]; +} SmpEnvReply; + +#ifdef __cplusplus +extern "C" { +#endif + +/* Resolve libSceIpmi, register the handler and start the dispatcher thread. + * -> false having logged why. Never fatal: a console with no backported title + * does not need this service at all, so main() logs and carries on. */ +bool sm_env_ipmi_serve(void); + +/* Stop the dispatcher and, only if it really left, destroy the registration. + * A registration must never outlive its dispatcher: a client probing a name + * held by a process that no longer answers reads it free and is killed inside + * create(). Nothing detects that state and only a reboot clears it. */ +void sm_env_ipmi_shutdown(void); + +#ifdef __cplusplus +} +#endif + +#endif /* SM_ENV_IPMI_H */ diff --git a/include/sm_env_ipmi_dispatch.h b/include/sm_env_ipmi_dispatch.h new file mode 100644 index 0000000..0b4c301 --- /dev/null +++ b/include/sm_env_ipmi_dispatch.h @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// +// The one seam ipmi_handler.c needs into the environment service. That file +// owns the vtable machinery and knows nothing about what the methods mean. +// +// Separate from sm_env_ipmi.h on purpose: that header is the wire contract, +// copied verbatim into every client, and must stay free of anything internal to +// this payload. + +#pragma once + +#include "ipmi.h" +#include "sm_env_ipmi.h" + +#include + +// Serves one sync dispatch. -> 0 on success, negative on refusal. Every path +// out must leave a truthful out[i].written: the framework reuses the out +// buffer, so a stale length gets the client killed for the mismatch. +int sm_env_ipmi_dispatch(IpmiSession *session, uint32_t method, + const IpmiDataInfo *in, uint32_t inCount, + IpmiOutBuffer *out, uint32_t outCount); + +// What an unknown or unserviceable method answers with; the value is ours to +// pick because both ends of this service are ours. +#define SM_ENV_IPMI_ENOTSUP (-1) diff --git a/include/sm_kstuff_caps.h b/include/sm_kstuff_caps.h new file mode 100644 index 0000000..e475199 --- /dev/null +++ b/include/sm_kstuff_caps.h @@ -0,0 +1,17 @@ +#ifndef SM_KSTUFF_CAPS_H +#define SM_KSTUFF_CAPS_H + +#include +#include + +// Which optional SceShellCore patches the loaded kstuff actually applied. +// Measured from ShellCore's live text, not inferred from a version number. +#define SM_KSTUFF_CAP_SYSDIRPATH (1u << 0) +#define SM_KSTUFF_CAP_TROPHY (1u << 1) + +// Probe SceShellCore for the capability patches and cache the result. +// -> false when the answer is UNKNOWN (unmapped firmware, ShellCore not found, +// or the read failed); *caps is only meaningful when this returns true. +bool sm_kstuff_probe_caps(uint32_t *caps); + +#endif diff --git a/src/ipmi_client.c b/src/ipmi_client.c new file mode 100644 index 0000000..19cc0d8 --- /dev/null +++ b/src/ipmi_client.c @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ipmi_client.h" + +#include "ipmi_log.h" + +#include +#include +#include + +// Reached through asm-labelled declarations. Only part of Client::Config's +// layout is known, so it is a byte buffer with named offsets rather than an +// invented struct. +void ipmi_cfg_ctor(void* cfg) + __asm__("_ZN4IPMI6Client6ConfigC1Ev"); +uint64_t ipmi_cfg_estimate(void* cfg) + __asm__("_ZN4IPMI6Client6Config24estimateClientMemorySizeEv"); +int ipmi_cli_create(void** out, const void* cfg, void* p3, void* storage) + __asm__("_ZN4IPMI6Client6createEPPS0_PKNS0_6ConfigEPvS6_"); + +typedef int (*ConnectFn)(void* client, void* arg, uint64_t argLen, int* serviceResult); +typedef int (*DestroyFn)(void* client); + +// Negotiated limits. estimateClientMemorySize() reports 0x1000 for this Config, +// so the storage size is generous rather than tight. +static const uint64_t kRequestBufferSize = 0x200u; +static const uint64_t kClientStorageSize = 0xf800u; + + +bool ipmi_client_open(IpmiClient* c, const IpmiSyms* syms, const char* name, + bool dumpVtable) { + memset(c, 0, sizeof(*c)); + // -1, NOT the 0 memset leaves behind: 0 is a valid slot index, so a client + // that failed before resolution would call vt[0] -- a destructor -- on close. + c->connectSlot = -1; + c->invokeSlot = -1; + c->destroySlot = -1; + + if (!syms->clientCreate || !syms->clientConfigCtor) { + logf_(" client: Client::create/Config::Config did not resolve"); + return false; + } + + // Measured: the constructor writes up to +0x4a of this buffer. 0x200 leaves + // room for a firmware whose Config is larger, since a too-small one would be + // a silent overrun of the ctor's own writes. + unsigned char cfg[0x200] __attribute__((aligned(16))); + memset(cfg, 0, sizeof(cfg)); + ipmi_cfg_ctor(cfg); + + /* NUL-terminated, and the same truncation rule the server uses. A flat + * memcpy of 16 copies a name with no terminator when it is exactly 16 long, + * and truncates differently from the server's strncpy(size - 1), so the two + * halves disagree about what the service is called and every connect answers + * ESRCH against a service that is registered and serving. */ + char nameBuf[16]; + memset(nameBuf, 0, sizeof(nameBuf)); + strncpy(nameBuf, name, sizeof(nameBuf) - 1); + memcpy(cfg, nameBuf, sizeof(nameBuf)); // +0x00 name[16] + *(uint64_t*)(cfg + 0x10) = 0; + *(uint64_t*)(cfg + 0x28) = kRequestBufferSize; + *(uint64_t*)(cfg + 0x30) = kClientStorageSize; + + uint64_t storageSize = kClientStorageSize; + if (syms->clientConfigEstimate) { + const uint64_t est = ipmi_cfg_estimate(cfg); + logf_(" estimateClientMemorySize = %#lx (we reserve %#lx)", + (unsigned long)est, (unsigned long)kClientStorageSize); + if (est > 0 && est < 0x100000u && est > storageSize) storageSize = est; + } + + c->storage = malloc(storageSize); + if (!c->storage) { logf_(" client: storage alloc failed"); return false; } + memset(c->storage, 0, storageSize); + + const int rc = ipmi_cli_create(&c->handle, cfg, NULL, c->storage); + logf_(" Client::create(\"%s\") -> rc=%#010x client=%p", name, (unsigned)rc, + c->handle); + if (rc < 0 || !c->handle) { + free(c->storage); + c->storage = NULL; + return false; + } + + if (dumpVtable) ipmi_dump_vtable(syms, c->handle, "client", 20); + + c->connectSlot = ipmi_vtable_slot_of(c->handle, syms->cliConnect, 20); + c->invokeSlot = ipmi_vtable_slot_of(c->handle, syms->cliInvokeSyncDataInfo, 20); + // Resolved here rather than in close(): close() has no syms, and a slot + // number is cheaper to carry than the whole table. + c->destroySlot = ipmi_vtable_slot_of(c->handle, syms->cliDestroy, 20); + logf_(" connect slot = %d (%#x), invokeSyncMethod(DataInfo) slot = %d (%#x)", + c->connectSlot, c->connectSlot < 0 ? 0 : c->connectSlot * 8, + c->invokeSlot, c->invokeSlot < 0 ? 0 : c->invokeSlot * 8); + return c->connectSlot >= 0 && c->invokeSlot >= 0; +} + +bool ipmi_client_connect(IpmiClient* c, const char* name) { + void* const* vt = *(void* const* const*)(c->handle); + int serviceResult = 0; + + // Logged before the call on purpose: if connect() ever blocks, this line is + // the last thing in the log and that is the answer. + logf_(" calling connect() on \"%s\" -- if the log stops here, connect BLOCKS", + name); + const int rc = ((ConnectFn)vt[c->connectSlot])( + c->handle, NULL, 0, &serviceResult); + // rc decodes as 0x80020000 | errno: 0x16 EINVAL, 0x03 ESRCH (no such + // service), 0x0d EACCES (refused). + logf_(" connect -> rc=%#010x serviceResult=%#010x%s", (unsigned)rc, + (unsigned)serviceResult, + (rc == (int)(0x8002000d)) ? " [EACCES: permission]" + : (rc == (int)(0x80020003)) ? " [ESRCH: no such service]" + : ""); + return rc >= 0 && serviceResult >= 0; +} + +void ipmi_client_close(IpmiClient* c) { + if (!c) return; + + if (c->handle) { + void* const* vt = *(void* const* const*)(c->handle); + + /* Destroy, never disconnect. destroy() calls sceIpmiMgrDestroyClient, + * which releases the client kid and everything under it. disconnect() + * makes a blocking request to the peer instead, and the peer we are + * closing against is typically a predecessor midway through exiting, so + * it never returns -- measured as stranded successors that then ignore + * their quit file forever. Dropping the disconnect took that from 5 of + * 10 to 0 of 15. The session the far side keeps is not worth it; that + * process is about to exit, which drops the session anyway. */ + if (c->destroySlot >= 0) { + (void)((DestroyFn)vt[c->destroySlot])(c->handle); + } else { + /* Leak the storage along with the handle. Undestroyed means the + * library still holds a client kid pointing into it, so freeing + * here would hand libSceIpmi a dangling backing buffer -- worse + * than one probe-sized leak. Rejecting the client in open() + * instead is not the answer: the probe would then skip its + * connect, name_is_free() would report the name free without + * having checked it, and create() on a held name kills us. */ + logf_(" client: no destroy slot -- handle %p and storage leaked", + c->handle); + c->storage = NULL; + } + + c->handle = NULL; + } + + /* Ours to free once destroy() has released the kid: storage is our malloc, + * not the library's. NULL when it had to be leaked above. */ + free(c->storage); + c->storage = NULL; + c->connectSlot = c->invokeSlot = c->destroySlot = -1; +} diff --git a/src/ipmi_handler.c b/src/ipmi_handler.c new file mode 100644 index 0000000..49d88b4 --- /dev/null +++ b/src/ipmi_handler.c @@ -0,0 +1,519 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ipmi_handler.h" + +#include "sm_env_ipmi_dispatch.h" +#include "ipmi_log.h" + +#include +#include +#include + +typedef uint64_t u64; + +typedef enum SlotKind { + KIND_UNKNOWN = 0, + KIND_DTOR, + KIND_SYNC_DATAINFO, + KIND_SYNC_RAW, + KIND_ASYNC_DATAINFO, + KIND_ASYNC_RAW, + KIND_SESSION_KILLED, +} SlotKind; + +static const char* kind_name(SlotKind k) { + switch (k) { + case KIND_DTOR: return "~EventHandler"; + case KIND_SYNC_DATAINFO: return "onSyncMethodDispatch(DataInfo)"; + case KIND_SYNC_RAW: return "onSyncMethodDispatch(raw)"; + case KIND_ASYNC_DATAINFO: return "onAsyncMethodDispatch(DataInfo)"; + case KIND_ASYNC_RAW: return "onAsyncMethodDispatch(raw)"; + case KIND_SESSION_KILLED: return "onSessionKilled"; + default: return "UNIDENTIFIED"; + } +} + +// Widest window we are willing to treat as EventHandler's vtable. +// +// Measured: the class has nine virtuals, not the seven its exported methods +// imply, and they are not in export order: +// +// [0x00] ~D1 [0x08] ~D0 [0x10] onSyncMethodDispatch(DataInfo) +// [0x18] onAsyncMethodDispatch(DataInfo) +// [0x20] [0x28] two slots sharing one address outside libSceIpmi -- +// unexported, so unnameable by this method +// [0x30] onSessionKilled [0x38] onSyncMethodDispatch(raw) +// [0x40] onAsyncMethodDispatch(raw), by elimination +// +// A cap of 8 stopped one slot short of that last one and reported it missing. +// Twelve covers it with slack; the scan stops at the last slot it can name, so +// a larger cap cannot run past the end of the vtable. +enum { kMaxSlots = 12 }; + +// Our vtable, laid out as the Itanium ABI wants it: offset-to-top and typeinfo +// first, then the function pointers. The object's vptr points at g_vtable[2]. +// +// Only the slots the scan named are copied. Reading further is not free +// padding: measured, the two words after the last virtual are the next vtable's +// offset-to-top and typeinfo, and the words after those are another class's +// methods. Copying them would install unrelated functions as our own. +static void* g_vtable[2 + kMaxSlots]; +static SlotKind g_kind[kMaxSlots]; + +// The handler object itself. EventHandler is an interface and should carry no +// data, but the padding costs nothing and a wrong guess about that would +// otherwise be a memory corruption rather than a log line. +typedef struct HandlerObject { + void** vptr; + unsigned char reserved[0x40]; +} HandlerObject; +static HandlerObject g_handler; + +// One-shot: the first Session* we are handed gets its vtable dumped, which is +// how SessionImpl's slots get identified for the reply path later. +static const IpmiSyms* g_syms; +static bool g_sessionDumped; + +// What the connect callback saw, captured without touching a file or klog. Read +// and printed later by handler_drain_connect_log() from the resident loop. +typedef struct ConnectRecord { + volatile bool pending; + unsigned calls; + int slot; + u64 srv, cfg, extra; + unsigned char cfgHead[0x40]; + uint64_t memorySize; + uint64_t memorySizeSet; + int createSessionSlot; + int createSessionRc; + u64 session; + bool noSessionSlot; +} ConnectRecord; +static ConnectRecord g_connect; + +// Session memory for createSession. Static and pre-carved, not malloc'd: this +// runs inside the connection window, where the less that happens the better. +// +// One buffer per live session, because sessions overlap as a matter of course. +// Measured on 4.03 over four title launches: every title connects TWICE, about +// two seconds apart, and the second connect lands while the first session is +// still alive -- eight connects, high-water mark of two live at once, never +// fewer. Sharing one buffer meant createSession placement-constructed each new +// session over the live previous one, and all eight sessions duly reported the +// same Session* (0x200949560): two handles, one object. +// +// Four slots for headroom over the observed two. No lock: the same measurement +// showed connect, dispatch and onSessionKilled all arrive on the dispatcher +// thread. Released in onSessionKilled, which the same run showed fires for +// every session (8 created, 8 killed, live back to 0). +enum { kSessionSlots = 4 }; +typedef struct SessionSlot { + unsigned char mem[0x20000] __attribute__((aligned(16))); + void* session; // non-NULL while this buffer backs a live session +} SessionSlot; +static SessionSlot g_sessionSlots[kSessionSlots]; + +static SessionSlot* session_slot_claim(void) { + for (int i = 0; i < kSessionSlots; i++) + if (!g_sessionSlots[i].session) return &g_sessionSlots[i]; + return NULL; +} + +// Matching by pointer is sound only because each live session now has its own +// buffer, and so its own address. It would have been ambiguous before this +// change -- which was the symptom, not a reason to key on something else. +static void session_slot_release(void* session) { + for (int i = 0; i < kSessionSlots; i++) + if (g_sessionSlots[i].session == session) + g_sessionSlots[i].session = NULL; +} + +// Deliberately branch-light and I/O-free: this runs inside the connection +// window. Two memcpys and some stores. +static void capture_connect(int slot, void* self, u64 srv, u64 cfg, u64 extra) { + (void)self; + g_connect.calls++; + g_connect.slot = slot; + g_connect.srv = srv; + g_connect.cfg = cfg; + g_connect.extra = extra; + if (cfg) { + memcpy(g_connect.cfgHead, (const void*)(cfg), + sizeof(g_connect.cfgHead)); + memcpy(&g_connect.memorySize, + (const unsigned char*)(cfg) + 0x148, 8); + } + + // Create the session the connection needs. Our return value reaches the + // client as serviceResult=0, yet without this the client's transport status + // is 1, which libSceIpmi maps to 0x8002000d -- "accepted, but no session + // exists". The framework hands this callback exactly what createSession + // wants: the Server*, the SessionImpl::Config*, and a seeded memorySize. + g_connect.createSessionSlot = -1; + g_connect.createSessionRc = 0; + g_connect.session = 0; + // cfg is written through below, so it is required here as well. Line 106 + // above already treats it as possibly null; a null reaching this branch + // would write eight bytes to 0x148 inside the connection window, where a + // fault kills the process. + SessionSlot* const slot_mem = session_slot_claim(); + g_connect.noSessionSlot = (slot_mem == NULL); + if (srv && cfg && slot_mem && g_syms && g_syms->srvCreateSession) { + // Say how big the buffer is. The framework seeds memorySize with a + // floor of 0x10 and expects the handler to supply both the memory and + // its size. Left at 0x10, createSession succeeded and one command + // dispatched, then IPMIMGR killed the process (signo=0xa0020320 + // opt32=0x02010006) -- a session running off the end of 16 bytes. + const uint64_t have = sizeof(slot_mem->mem); + memcpy((unsigned char*)(cfg) + 0x148, &have, 8); + g_connect.memorySizeSet = have; + + const int slotIdx = ipmi_vtable_slot_of((void*)(srv), + g_syms->srvCreateSession, 24); + g_connect.createSessionSlot = slotIdx; + if (slotIdx >= 0) { + void* const* vt = *(void* const* const*)(srv); + typedef int (*CreateSessionFn)(void* self, void** out, void* cfg, + void* mem); + void* session = NULL; + g_connect.createSessionRc = + ((CreateSessionFn)vt[slotIdx])( + (void*)(srv), &session, + (void*)(cfg), slot_mem->mem); + g_connect.session = (u64)(session); + // Held only once it actually backs a session; a failed create must + // not strand the buffer. + if (g_connect.createSessionRc >= 0 && session) + slot_mem->session = session; + } + } + g_connect.pending = true; +} + +static int64_t slot_dispatch(int slot, void* self, u64 a1, u64 a2, u64 a3, u64 a4, + u64 a5, u64 a6) { + const SlotKind kind = (slot >= 0 && slot < kMaxSlots) ? g_kind[slot] + : KIND_UNKNOWN; + + switch (kind) { + case KIND_DTOR: + // Deliberately does not free anything: the object is static. The + // deleting destructor (~D0) landing here would otherwise call + // operator delete on a global. + logf_("EVH slot[%#04x] ~EventHandler self=%p (no-op: object is static)", + slot * 8, self); + return 0; + + case KIND_SYNC_DATAINFO: { + IpmiSession* session = (IpmiSession*)(a1); + uint32_t method = (uint32_t)(a2); + const IpmiDataInfo* in = (const IpmiDataInfo*)(a3); + uint32_t inCount = (uint32_t)(a4); + IpmiOutBuffer* out = (IpmiOutBuffer*)(a5); + uint32_t outCount = (uint32_t)(a6); + + logf_("SYNC slot[%#04x] session=%p method=%#x inCount=%u outCount=%u", + slot * 8, (void*)session, method, inCount, outCount); + + if (session && !g_sessionDumped) { + g_sessionDumped = true; + ipmi_dump_vtable(g_syms, session, "session", 24); + } + + for (uint32_t i = 0; i < inCount && i < 8; i++) { + logf_(" in[%u] ptr=%p size=%zu", i, in ? in[i].data : NULL, + in ? in[i].size : 0); + if (in && in[i].data) log_hexdump(" data", in[i].data, in[i].size); + } + // Zero `written` for every out entry, first. The framework leaves + // it uninitialised in a buffer it reuses between commands, so a + // command that returns without writing an out-param inherits the + // previous one's length. Measured: an unhandled command with a + // 4-byte out buffer responded with written=8 left over from the + // previous one and the client was killed for the mismatch + // (_ipmimgrRaiseException signo=0xa002031f opt64=0x18). Every path + // out -- answered, refused or unhandled -- must leave a truthful + // length. + for (uint32_t i = 0; i < outCount && out; i++) out[i].written = 0; + for (uint32_t i = 0; i < outCount && i < 8; i++) { + logf_(" out[%u] ptr=%p capacity=%zu", i, + out ? out[i].data : NULL, out ? out[i].capacity : 0); + } + + const int rc = sm_env_ipmi_dispatch(session, method, in, inCount, + out, outCount); + + // Answer the request. Returning does not imply it: the framework + // calls this vtable slot and then returns without replying itself, + // and leaving a request unanswered gets the server killed + // (_ipmimgrRaiseException signo=0xa0020320 opt32=0x02010006). + // Writing into the out buffer is not sufficient on its own. + // Responding comes before logging so the reply does not wait on + // file I/O. + int respondRc = 0; + int respondSlot = -1; + if (session && g_syms && g_syms->sessRespondSyncBuf) { + respondSlot = ipmi_vtable_slot_of(session, + g_syms->sessRespondSyncBuf, 24); + if (respondSlot >= 0) { + void* const* svt = + *(void* const* const*)(session); + typedef int (*RespondFn)(void* self, int result, + const IpmiOutBuffer* out, + uint32_t outCount); + respondRc = ((RespondFn)svt[respondSlot])( + session, rc, out, outCount); + } + } + + logf_(" -> rc=%#010x, out[0].written=%zu, " + "respondToSyncMethodRequest slot=%d rc=%#010x%s", + (unsigned)rc, (outCount && out) ? out[0].written : 0, + respondSlot, + (unsigned)respondRc, + respondSlot < 0 ? " <-- NOT FOUND: the request is unanswered " + "and the kernel will kill us" : ""); + return rc; + } + + case KIND_SYNC_RAW: + // Not implemented on purpose: the descriptor form is what our client + // sends and what a real sandboxed title has been served on, and + // nothing has ever arrived here. A dispatch that did would mean the + // wire shape is not what we measured, which is worth seeing in a log + // and is not worth answering blind. + logf_("SYNC-RAW slot[%#04x] session=%p method=%#x a3=%#lx a4=%#lx " + "a5=%#lx a6=%#lx -- not implemented, refusing", + slot * 8, (void*)a1, (unsigned)a2, (unsigned long)a3, + (unsigned long)a4, (unsigned long)a5, (unsigned long)a6); + return SM_ENV_IPMI_ENOTSUP; + + case KIND_ASYNC_DATAINFO: { + const IpmiDataInfo* in = (const IpmiDataInfo*)(a4); + uint32_t inCount = (uint32_t)(a5); + logf_("ASYNC slot[%#04x] session=%p method=%#x unk=%#x inCount=%u", + slot * 8, (void*)a1, (unsigned)a2, (unsigned)a3, inCount); + for (uint32_t i = 0; i < inCount && i < 8; i++) { + logf_(" in[%u] ptr=%p size=%zu", i, in ? in[i].data : NULL, + in ? in[i].size : 0); + } + // No async command is in scope; refusing is honest and cannot hang + // the caller the way an unhandled command would. + return SM_ENV_IPMI_ENOTSUP; + } + + case KIND_ASYNC_RAW: + logf_("ASYNC-RAW slot[%#04x] session=%p method=%#x unk=%#x a4=%#lx " + "a5=%#lx a6=%#lx -- not implemented, refusing", + slot * 8, (void*)a1, (unsigned)a2, (unsigned)a3, + (unsigned long)a4, (unsigned long)a5, (unsigned long)a6); + return SM_ENV_IPMI_ENOTSUP; + + case KIND_SESSION_KILLED: + logf_("SESSION KILLED slot[%#04x] session=%p", slot * 8, (void*)a1); + session_slot_release((void*)a1); + g_sessionDumped = false; // next session dumps again + return 0; + + default: + // An unnamed slot, accepted rather than refused. Slot 0x20 is on + // the connect path: it arrives as (this, Server*, Session::Config*, + // void* extra) with memorySize seeded to a floor of 0x10, and + // whatever it returns goes straight back to the connecting client. + // While this refused, the client saw connect rc=0x8002000d + // serviceResult=0x80d90009. A callback that gates the connection + // has to succeed or nothing else ever runs; an unknown command is + // the opposite, and is refused. + // + // Not hooking it is not the safer option either: 0x20 and 0x28 + // share one address we cannot name or vet. + capture_connect(slot, self, a1, a2, a3); + return 0; + } +} + +// One thunk per slot so that the slot index is known without reading any +// per-call state -- the firmware tells us nothing about which slot it entered. +#define SLOT_THUNK(n) \ + static int64_t evh_slot##n(void* self, u64 a1, u64 a2, u64 a3, \ + u64 a4, u64 a5, u64 a6) { \ + return slot_dispatch(n, self, a1, a2, a3, a4, a5, a6); \ + } +SLOT_THUNK(0) SLOT_THUNK(1) SLOT_THUNK(2) SLOT_THUNK(3) +SLOT_THUNK(4) SLOT_THUNK(5) SLOT_THUNK(6) SLOT_THUNK(7) +SLOT_THUNK(8) SLOT_THUNK(9) SLOT_THUNK(10) SLOT_THUNK(11) +#undef SLOT_THUNK + +static void* const kThunks[kMaxSlots] = { + (void*)evh_slot0, (void*)evh_slot1, (void*)evh_slot2, (void*)evh_slot3, + (void*)evh_slot4, (void*)evh_slot5, (void*)evh_slot6, (void*)evh_slot7, + (void*)evh_slot8, (void*)evh_slot9, (void*)evh_slot10, (void*)evh_slot11, +}; + +// Bit per known EventHandler method, so that a slot whose address matches more +// than one of them (identical bodies folded to one address by the linker) can +// be reported as ambiguous rather than silently resolved to whichever we +// checked first. +enum { + M_D1 = 1 << 0, M_D0 = 1 << 1, M_D2 = 1 << 2, + M_SYNC_DI = 1 << 3, M_SYNC_RAW = 1 << 4, + M_ASYNC_DI = 1 << 5, M_ASYNC_RAW = 1 << 6, M_KILLED = 1 << 7, + M_DTORS = M_D1 | M_D0 | M_D2, +}; + +static unsigned match_mask(const IpmiSyms* s, const void* v) { + unsigned m = 0; + if (!v) return 0; + if (v == s->evhD1) m |= M_D1; + if (v == s->evhD0) m |= M_D0; + if (v == s->evhD2) m |= M_D2; + if (v == s->evhSyncDataInfo) m |= M_SYNC_DI; + if (v == s->evhSyncRaw) m |= M_SYNC_RAW; + if (v == s->evhAsyncDataInfo) m |= M_ASYNC_DI; + if (v == s->evhAsyncRaw) m |= M_ASYNC_RAW; + if (v == s->evhSessionKilled) m |= M_KILLED; + return m; +} + +static SlotKind kind_from_mask(unsigned m) { + if (!m) return KIND_UNKNOWN; + // Destructors routinely share one address (D1 and D2 are the same code), + // so a mask that is entirely destructors is still an unambiguous answer. + if ((m & ~(unsigned)M_DTORS) == 0) return KIND_DTOR; + switch (m) { + case M_SYNC_DI: return KIND_SYNC_DATAINFO; + case M_SYNC_RAW: return KIND_SYNC_RAW; + case M_ASYNC_DI: return KIND_ASYNC_DATAINFO; + case M_ASYNC_RAW: return KIND_ASYNC_RAW; + case M_KILLED: return KIND_SESSION_KILLED; + default: return KIND_UNKNOWN; // ambiguous: two names, one address + } +} + + +void handler_drain_connect_log(void) { + if (!g_connect.pending) return; + g_connect.pending = false; + + uint32_t clientPid = 0, maxOut = 0, numEventFlag = 0, numMsgQueue = 0; + memcpy(&clientPid, g_connect.cfgHead + 0x00, 4); + memcpy(&maxOut, g_connect.cfgHead + 0x08, 4); + memcpy(&numEventFlag, g_connect.cfgHead + 0x38, 4); + memcpy(&numMsgQueue, g_connect.cfgHead + 0x40, 4); + + logf_("CONNECT callback (drained) slot[%#04x] call#%u srv=%#lx cfg=%#lx " + "extra=%#lx -- returned 0 with NO logging inside the callback", + g_connect.slot * 8, g_connect.calls, (unsigned long)g_connect.srv, + (unsigned long)g_connect.cfg, (unsigned long)g_connect.extra); + logf_(" SessionImpl::Config clientPid=%u maxOutstanding=%u " + "numEventFlag=%u numMsgQueue=%u memorySize=%#lx", + clientPid, maxOut, numEventFlag, numMsgQueue, + (unsigned long)g_connect.memorySize); + logf_(" memorySize seeded %#lx -> set to %#lx before createSession", + (unsigned long)g_connect.memorySize, + (unsigned long)g_connect.memorySizeSet); + logf_(" createSession slot=%d rc=%#010x session=%#lx%s", + g_connect.createSessionSlot, (unsigned)g_connect.createSessionRc, + (unsigned long)g_connect.session, + g_connect.createSessionSlot < 0 + ? " <-- NOT FOUND in the Server vtable" + : (g_connect.session ? " <-- a session exists now" + : " <-- no session was produced")); + if (g_connect.noSessionSlot) + logf_(" no free session buffer -- all %d in use, so this connect was " + "refused rather than given a buffer another session is on", + kSessionSlots); +} + +HandlerBuild handler_build(const IpmiSyms* syms) { + HandlerBuild out = {NULL, 0, false}; + g_syms = syms; + + if (!syms->evhVtable) { + logf_("the EventHandler vtable symbol did not resolve; the layout cannot " + "be measured and this does not guess it"); + return out; + } + + void** ztv = (void**)(syms->evhVtable); + + // Locate the address point. A _ZTV symbol normally points at the start of + // the vtable object -- offset-to-top, then typeinfo, then the methods -- so + // the address point is +2 slots, but some toolchains export the address + // point itself. Rather than assume either, find the first slot that is one + // of the methods we resolved by name. + int ap = -1; + for (int i = 0; i < 6 && ap < 0; i++) { + if (match_mask(syms, ztv[i])) ap = i; + } + if (ap < 0) { + logf_("none of the exported EventHandler methods appears in the first six " + "words at %p -- either the vtable symbol is not what we think, or " + "every method folded to an address we did not resolve. Refusing to " + "build a handler on that.", syms->evhVtable); + for (int i = 0; i < 6; i++) logf_(" ztv[%#04x] = %p", i * 8, ztv[i]); + return out; + } + logf_("EventHandler vtable %p, address point +%#x", syms->evhVtable, ap * 8); + + void** base = ztv + ap; + + // Length: run to the last slot that matches something we resolved. Stopping + // at the first miss would truncate on an unexported virtual; running to a + // fixed count would install a thunk over whatever follows the vtable. + int slots = 0; + unsigned seen = 0; + for (int i = 0; i < kMaxSlots; i++) { + const unsigned m = match_mask(syms, base[i]); + if (m) { slots = i + 1; seen |= m; } + } + out.slotCount = slots; + + logf_("---- EventHandler vtable order, measured (%d slots)", slots); + for (int i = 0; i < slots; i++) { + const unsigned m = match_mask(syms, base[i]); + g_kind[i] = kind_from_mask(m); + const char* sym = ipmi_syms_name(syms, base[i]); + logf_(" [%#04x] %p %-32s%s", i * 8, base[i], kind_name(g_kind[i]), + (g_kind[i] == KIND_UNKNOWN && sym) ? " (ambiguous address)" : ""); + } + for (int i = slots; i < kMaxSlots; i++) g_kind[i] = KIND_UNKNOWN; + + const unsigned wanted = M_SYNC_DI | M_SYNC_RAW | M_ASYNC_DI | M_ASYNC_RAW | + M_KILLED; + if ((seen & wanted) != wanted) { + logf_(" note: not every exported method was located in the vtable " + "(seen=%#x wanted=%#x); unidentified slots log rather than " + "answering", seen & wanted, wanted); + } + + // Carry the offset-to-top and typeinfo across when the symbol pointed at + // the start of the vtable object rather than at its address point. Nothing + // here reads them, but the real pair is more honest than whatever happened + // to precede the methods. + g_vtable[0] = (ap >= 2) ? ztv[ap - 2] : NULL; + g_vtable[1] = (ap >= 1) ? ztv[ap - 1] : NULL; + for (int i = 0; i < slots && i < kMaxSlots; i++) g_vtable[2 + i] = kThunks[i]; + + memset(&g_handler, 0, sizeof(g_handler)); + g_handler.vptr = &g_vtable[2]; + + logf_("handler object=%p vptr=%p (offset-to-top=%p typeinfo=%p)", + (void*)&g_handler, (void*)g_handler.vptr, g_vtable[0], g_vtable[1]); + for (int i = 0; i < slots; i++) { + logf_(" ours [%#04x] = %p %s", i * 8, g_vtable[2 + i], + kind_name(g_kind[i])); + } + + for (int i = 0; i < slots; i++) { + if (g_kind[i] == KIND_SYNC_DATAINFO) out.syncDispatchProven = true; + } + if (!out.syncDispatchProven) { + logf_("the descriptor-form sync dispatch slot was not identified; the " + "service would come up and log whatever arrives, but refuse every " + "command"); + } + + out.handler = (IpmiEventHandler*)(&g_handler); + return out; +} diff --git a/src/ipmi_symbols.c b/src/ipmi_symbols.c new file mode 100644 index 0000000..751fa88 --- /dev/null +++ b/src/ipmi_symbols.c @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ipmi_symbols.h" + +#include "ipmi_log.h" + +#include +#include +#include +#include + +typedef struct SymEntry { + const char* mangled; + const char* shortName; + size_t field; // byte offset into IpmiSyms +} SymEntry; + +#define SYM(field, mangled, shortName) \ + { mangled, shortName, offsetof(IpmiSyms, field) } + +// Keep the mangled strings byte-exact: a typo shows up as a null lookup and a +// refusal to dispatch, not as a wrong answer. +static const SymEntry kSyms[] = { + SYM(evhVtable, + "_ZTVN4IPMI6Server12EventHandlerE", "EventHandler::vtable"), + SYM(evhD1, + "_ZN4IPMI6Server12EventHandlerD1Ev", "EventHandler::~D1"), + SYM(evhD0, + "_ZN4IPMI6Server12EventHandlerD0Ev", "EventHandler::~D0"), + SYM(evhD2, + "_ZN4IPMI6Server12EventHandlerD2Ev", "EventHandler::~D2"), + SYM(evhSyncDataInfo, + "_ZN4IPMI6Server12EventHandler20onSyncMethodDispatchEPNS_7SessionEjPKNS_8DataInfoEjPNS_10BufferInfoEj", + "EventHandler::onSyncMethodDispatch(DataInfo)"), + SYM(evhSyncRaw, + "_ZN4IPMI6Server12EventHandler20onSyncMethodDispatchEPNS_7SessionEjPvmmS4_m", + "EventHandler::onSyncMethodDispatch(raw)"), + SYM(evhAsyncDataInfo, + "_ZN4IPMI6Server12EventHandler21onAsyncMethodDispatchEPNS_7SessionEjjPKNS_8DataInfoEj", + "EventHandler::onAsyncMethodDispatch(DataInfo)"), + SYM(evhAsyncRaw, + "_ZN4IPMI6Server12EventHandler21onAsyncMethodDispatchEPNS_7SessionEjjPvmS4_m", + "EventHandler::onAsyncMethodDispatch(raw)"), + SYM(evhSessionKilled, + "_ZN4IPMI6Server12EventHandler15onSessionKilledEPNS_7SessionE", + "EventHandler::onSessionKilled"), + + SYM(srvRunDispatcher, + "_ZN4IPMI4impl10ServerImpl13runDispatcherEPvm", "ServerImpl::runDispatcher"), + SYM(srvShutdownDispatcher, + "_ZN4IPMI4impl10ServerImpl18shutdownDispatcherEv", "ServerImpl::shutdownDispatcher"), + SYM(srvTryDispatch, + "_ZN4IPMI4impl10ServerImpl11tryDispatchEPvm", "ServerImpl::tryDispatch"), + SYM(srvCreateSession, + "_ZN4IPMI4impl10ServerImpl13createSessionEPPNS_7SessionEPvS5_", + "ServerImpl::createSession"), + SYM(srvGetUserData, + "_ZN4IPMI4impl10ServerImpl11getUserDataEv", "ServerImpl::getUserData"), + SYM(srvDestroy, + "_ZN4IPMI4impl10ServerImpl7destroyEv", "ServerImpl::destroy"), + SYM(srvD0, + "_ZN4IPMI4impl10ServerImplD0Ev", "ServerImpl::~D0"), + SYM(srvD1, + "_ZN4IPMI4impl10ServerImplD1Ev", "ServerImpl::~D1"), + + SYM(sessRespondSyncBuf, + "_ZN4IPMI4impl11SessionImpl26respondToSyncMethodRequestEiPKNS_10BufferInfoEj", + "SessionImpl::respondToSyncMethodRequest(BufferInfo)"), + SYM(sessRespondSyncRaw, + "_ZN4IPMI4impl11SessionImpl26respondToSyncMethodRequestEiPKvm", + "SessionImpl::respondToSyncMethodRequest(raw)"), + SYM(sessGetClientPid, + "_ZN4IPMI4impl11SessionImpl12getClientPidEv", "SessionImpl::getClientPid"), + SYM(sessGetServer, + "_ZN4IPMI4impl11SessionImpl9getServerEv", "SessionImpl::getServer"), + SYM(sessDestroy, + "_ZN4IPMI4impl11SessionImpl7destroyEv", "SessionImpl::destroy"), + SYM(sessIsPeerPrivileged, + "_ZNK4IPMI4impl11SessionImpl16isPeerPrivilegedEv", "SessionImpl::isPeerPrivileged"), + + SYM(clientCreate, + "_ZN4IPMI6Client6createEPPS0_PKNS0_6ConfigEPvS6_", "Client::create"), + SYM(clientConfigCtor, + "_ZN4IPMI6Client6ConfigC1Ev", "Client::Config::Config"), + SYM(clientConfigEstimate, + "_ZN4IPMI6Client6Config24estimateClientMemorySizeEv", + "Client::Config::estimateClientMemorySize"), + SYM(cliConnect, + "_ZN4IPMI4impl10ClientImpl7connectEPKvmPi", "ClientImpl::connect"), + SYM(cliDisconnect, + "_ZN4IPMI4impl10ClientImpl10disconnectEv", "ClientImpl::disconnect"), + SYM(cliTerminateConnection, + "_ZN4IPMI4impl10ClientImpl19terminateConnectionEv", + "ClientImpl::terminateConnection"), + SYM(cliDestroy, + "_ZN4IPMI4impl10ClientImpl7destroyEv", "ClientImpl::destroy"), + SYM(cliInvokeSyncDataInfo, + "_ZN4IPMI4impl10ClientImpl16invokeSyncMethodEjPKNS_8DataInfoEjPiPNS_10BufferInfoEj", + "ClientImpl::invokeSyncMethod(DataInfo)"), + SYM(cliInvokeSyncRaw, + "_ZN4IPMI4impl10ClientImpl16invokeSyncMethodEjPKvmPiPvPmm", + "ClientImpl::invokeSyncMethod(raw)"), + SYM(cliInvokeAsyncDataInfo, + "_ZN4IPMI4impl10ClientImpl17invokeAsyncMethodEjPKNS_8DataInfoEjPjPKNS_6Client12EventNotifeeE", + "ClientImpl::invokeAsyncMethod(DataInfo)"), + + SYM(serverCreate, + "_ZN4IPMI6Server6createEPPS0_PKNS0_6ConfigEPvS6_", "Server::create"), + SYM(serverConfigCtor, + "_ZN4IPMI6Server6ConfigC1Ev", "Server::Config::Config"), +}; + +#undef SYM + +static const size_t kSymCount = sizeof(kSyms) / sizeof(kSyms[0]); + +static void** field_of(IpmiSyms* s, size_t off) { + return (void**)((char*)(s) + off); +} + +static void* const* field_of_const(const IpmiSyms* s, size_t off) { + return (void* const*)((const char*)(s) + off); +} + +static void* open_libipmi(void) { + // Measured: RTLD_NOLOAD always answers null here even though this payload + // links against the library, and the plain load always succeeds. + void* h = dlopen("libSceIpmi.sprx", RTLD_LAZY); + logf_("dlopen(\"libSceIpmi.sprx\", RTLD_LAZY) -> %p", h); + if (!h) { + const char* err = dlerror(); + if (err) logf_(" dlerror: %s", err); + } + return h; +} + + +bool ipmi_syms_resolve(IpmiSyms* s) { + memset(s, 0, sizeof(*s)); + + void* h = open_libipmi(); + if (!h) { + logf_("libSceIpmi could not be opened; every slot identification below " + "would be a guess, so the service will not register"); + return false; + } + + int found = 0; + for (size_t i = 0; i < kSymCount; i++) { + void* addr = dlsym(h, kSyms[i].mangled); + *field_of(s, kSyms[i].field) = addr; + if (addr) found++; + logf_(" %-52s = %p%s", kSyms[i].shortName, addr, + addr ? "" : " <-- MISSING"); + } + logf_("resolved %d/%zu libSceIpmi symbols", found, kSymCount); + return true; +} + +const char* ipmi_syms_name(const IpmiSyms* s, const void* addr) { + if (!addr) return NULL; + for (size_t i = 0; i < kSymCount; i++) { + if (*field_of_const(s, kSyms[i].field) == addr) return kSyms[i].shortName; + } + return NULL; +} + +void ipmi_dump_vtable(const IpmiSyms* s, const void* obj, const char* label, + int slots) { + if (!obj) return; + void* const* vt = *(void* const* const*)(obj); + logf_("%s vtable = %p", label, (void*)vt); + // Offsets are printed relative to Server::create rather than to a module + // base, so no per-firmware constant is involved and a wrong one cannot make + // the dump quietly misleading. + for (int i = 0; i < slots; i++) { + const char* name = ipmi_syms_name(s, vt[i]); + if (s->serverCreate && vt[i]) { + logf_(" %s vtbl[%#04x] = %p create%+ld %s", label, i * 8, vt[i], + (long)((intptr_t)(vt[i]) - (intptr_t)(s->serverCreate)), + name ? name : ""); + } else { + logf_(" %s vtbl[%#04x] = %p %s", label, i * 8, vt[i], + name ? name : ""); + } + } +} + +int ipmi_vtable_slot_of(const void* obj, const void* fn, int slots) { + if (!obj || !fn) return -1; + void* const* vt = *(void* const* const*)(obj); + for (int i = 0; i < slots; i++) { + if (vt[i] == fn) return i; + } + return -1; +} diff --git a/src/main.c b/src/main.c index 8dbb098..2a60bb9 100644 --- a/src/main.c +++ b/src/main.c @@ -10,7 +10,9 @@ #include "sm_shellcore_flags.h" #include "sm_config_mount.h" #include "sm_game_lifecycle.h" +#include "sm_env_ipmi.h" #include "sm_kstuff.h" +#include "sm_kstuff_caps.h" #include "sm_mount_device.h" #include "sm_filesystem.h" #include "sm_image.h" @@ -473,6 +475,16 @@ int main(void) { log_debug(" [SHELLFLAG] monitor unavailable"); sm_mdbg_init(); sm_kstuff_init(); + // Probe eagerly so the answer is in the log from boot rather than only after + // a title asks. Only a positive is cached, so if kstuff loads after us this + // reading is provisional and the next ask re-probes -- see + // sm_kstuff_probe_caps. + (void)sm_kstuff_probe_caps(NULL); + // After sm_kstuff_init: the service reports that probe's answer. Never fatal + // -- a console with no backported title does not need this service at all. + if (!sm_env_ipmi_serve()) + log_debug(" [ENVSVC] environment service unavailable; a backported title " + "that gates on kstuff will see unknown capabilities"); if (!refresh_game_lifecycle_watcher()) log_debug(" [GAME] lifecycle watcher unavailable"); @@ -517,6 +529,9 @@ int main(void) { sm_shellcore_flags_stop(); stop_game_lifecycle_watcher(); sm_scanner_shutdown(); + // Before sm_kstuff_shutdown: the dispatcher can be inside a query that reads + // the kstuff probe state, so the service has to stop answering first. + sm_env_ipmi_shutdown(); sm_kstuff_shutdown(); sm_mdbg_shutdown(); cleanup_kstuff_noautomount_files(); diff --git a/src/sm_env_ipmi.c b/src/sm_env_ipmi.c new file mode 100644 index 0000000..53de851 --- /dev/null +++ b/src/sm_env_ipmi.c @@ -0,0 +1,407 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// +// The environment service: its identity, the one command it answers, and a +// lifecycle that never lets a registration outlive its dispatcher. The IPMI +// machinery it sits on is in ipmi_symbols.c, ipmi_client.c and ipmi_handler.c. + +#include "ipmi.h" +#include "ipmi_client.h" +#include "ipmi_handler.h" +#include "ipmi_log.h" +#include "ipmi_symbols.h" +#include "sm_env_ipmi_dispatch.h" +#include "sm_kstuff.h" +#include "sm_kstuff_caps.h" +#include "sm_log.h" + +#include +#include +#include +#include +#include +#include +#include + +#ifndef SHADOWMOUNT_VERSION +#define SHADOWMOUNT_VERSION "unknown" +#endif + +// --------------------------------------------------------------------------- +// The logging shim the IPMI files use. One logging system, not two. +// --------------------------------------------------------------------------- + +void logf_(const char *fmt, ...) { + char line[1024]; + va_list ap; + + va_start(ap, fmt); + vsnprintf(line, sizeof(line), fmt, ap); + va_end(ap); + + log_debug(" [ENVSVC] %s", line); +} + +void log_hexdump(const char *label, const void *p, size_t n) { + const unsigned char *b = (const unsigned char *)(p); + char line[128]; + + if (!b) { + log_debug(" [ENVSVC] %s: (null)", label ? label : ""); + return; + } + for (size_t off = 0; off < n; off += 16) { + size_t used = 0; + for (size_t i = 0; i < 16 && off + i < n; i++) { + const int wrote = snprintf(line + used, sizeof(line) - used, "%02x ", + b[off + i]); + if (wrote <= 0 || (size_t)wrote >= sizeof(line) - used) + break; + used += (size_t)wrote; + } + log_debug(" [ENVSVC] %s +%04zx %s", label ? label : "", off, line); + } +} + +// tryDispatch takes the working buffer sized by estimateTempWorkingMemorySize() +// on the same Config. Omitting it kills the dispatcher thread while main carries +// on, leaving the service registered and unable to ever answer. +typedef int (*TryDispatchFn)(void *self, void *buf, uint64_t size); +typedef int (*DestroyFn)(void *self); + +static IpmiSyms g_syms; +static bool g_syms_ok; +// A Server* in the real declaration; nothing here reaches it except through its +// vtable, so void* is the honest type. It points into g_srv_storage. +static void *g_srv; +static void *g_work_buf; +static uint64_t g_work_size; +static volatile bool g_disp_stop; +static volatile bool g_disp_alive; + +// Poll tryDispatch, never runDispatcher. runDispatcher checks its shutdown flag +// only before each receive, so one already asleep in receivePacket survives +// SIGKILL, keeps the name, and blocks every client forever. +static const unsigned kPollMs = 10; + +static void *dispatcher_thread(void *arg) { + (void)arg; + + // Never name this thread. thr_set_name writes p_comm, the process name, so a + // worker naming itself renames the whole payload -- it stops being findable as + // shadowmountplus.elf, and you cannot kill what you cannot find. + TryDispatchFn tryDispatch = (TryDispatchFn)g_syms.srvTryDispatch; + // Taken once, deliberately. Re-reading g_srv every iteration raced teardown: + // it cleared g_srv before signalling the stop, so a dispatcher already past + // its !g_disp_stop check called tryDispatch(NULL) -- a null this into a + // firmware virtual. g_srv is not volatile either, so that read was a data + // race as well as a lifetime one. Teardown waits for g_disp_alive to clear + // before it destroys anything, so this reference cannot outlive its object. + void *const srv = g_srv; + + g_disp_alive = true; + if (!tryDispatch) { + logf_("no tryDispatch symbol; refusing to fall back to runDispatcher -- an " + "unkillable process is worse than an unserved one"); + g_disp_alive = false; + return NULL; + } + + logf_("polling tryDispatch every %ums", kPollMs); + while (!g_disp_stop) { + const int rc = tryDispatch(srv, g_work_buf, g_work_size); + if (rc != 0) { + logf_("tryDispatch rc=%#010x -- stopping", (unsigned)rc); + break; + } + // Print whatever the connect callback captured. It cannot log from inside + // the connection window itself. + handler_drain_connect_log(); + usleep(kPollMs * 1000); + } + + g_disp_alive = false; + return NULL; +} + +// The probe runs on its own thread because connect() can block forever against +// a wedged predecessor and libSceIpmi's connect has no timeout. A probe that +// does not report back counts as held: creating on a held name kills us. +typedef struct ProbeResult { + volatile bool done; + volatile bool connected; +} ProbeResult; +static ProbeResult g_probe = {false, false}; + +static void *probe_thread(void *arg) { + (void)arg; + + IpmiClient c; + if (ipmi_client_open(&c, &g_syms, SMP_ENV_IPMI_SERVICE, false)) + g_probe.connected = ipmi_client_connect(&c, SMP_ENV_IPMI_SERVICE); + ipmi_client_close(&c); + g_probe.done = true; + return NULL; +} + +static const int kProbeTimeoutSecs = 5; + +// -> true when the name is ours to take. +static bool name_is_free(void) { + g_probe.done = g_probe.connected = false; + + pthread_t pt; + if (pthread_create(&pt, NULL, probe_thread, NULL) != 0) { + logf_("could not start the probe thread -- assuming the name is held"); + return false; + } + pthread_detach(pt); + + for (int i = 0; i < kProbeTimeoutSecs && !g_probe.done; i++) + sleep(1); + + if (!g_probe.done) { + logf_("connect to " SMP_ENV_IPMI_SERVICE " did not return in %ds. A " + "predecessor is wedged -- it holds the name, cannot be killed, and " + "every client that connects to it blocks forever. Reboot the console; " + "nothing this payload can do will clear it.", + kProbeTimeoutSecs); + return false; + } + + if (g_probe.connected) { + logf_("another instance already holds " SMP_ENV_IPMI_SERVICE + " -- not registering. Creating a server on a held name does not fail, " + "it kills this process from inside create(). Stop the old " + "ShadowMountPlus and start this one again."); + return false; + } + + logf_("nobody holds " SMP_ENV_IPMI_SERVICE " -- the name is ours"); + return true; +} + +// The invariant: a registration must never outlive its dispatcher. Every exit +// path comes through here, including the failure paths inside serve() itself. +static void server_teardown(const char *why) { + void *const srv = g_srv; + + if (!srv) + return; + + // Ask, then wait for the dispatcher to actually be out. Destroying the server + // while tryDispatch is inside it is a use-after-free, and the window is a + // whole poll interval wide. + g_disp_stop = true; + for (int i = 0; i < 200 && g_disp_alive; i++) + usleep(10 * 1000); + + if (g_disp_alive) { + // Do not destroy. destroy() succeeds only on status == 0; a dispatch in + // flight either refuses or wedges this process unkillably. Leaving it + // undestroyed is harmless -- process exit releases the name. + // g_srv deliberately left set: the registration really is still live, and + // a later serve() must see that rather than trying to register a second + // server on a name this process still holds. + logf_("%s: dispatcher is still inside tryDispatch -- not destroying; " + "process exit will release the name", + why); + return; + } + + // Only now that the dispatcher is provably out is nothing reading it. + g_srv = NULL; + + // Resolve destroy by address in this object's own vtable: the slot is what + // the instance actually implements. shutdownDispatcher is deliberately not + // called -- it sets status |= 4, which only one more tryDispatch would clear, + // and destroy would then refuse and leave the name held. + void *const *vt = *(void *const *const *)(srv); + const int dsSlot = + g_syms.srvDestroy ? ipmi_vtable_slot_of(srv, g_syms.srvDestroy, 24) : -1; + if (dsSlot >= 0) + (void)((DestroyFn)vt[dsSlot])(srv); + else + logf_("no destroy slot resolved -- the name may still be held. If the next " + "start dies inside create(), reboot."); + + free(g_work_buf); + g_work_buf = NULL; + logf_("%s: " SMP_ENV_IPMI_SERVICE " destroyed (slot=%d)", why, dsSlot); +} + + +// --------------------------------------------------------------------------- +// The one command. +// --------------------------------------------------------------------------- + +int sm_env_ipmi_dispatch(IpmiSession *session, uint32_t method, + const IpmiDataInfo *in, uint32_t inCount, + IpmiOutBuffer *out, uint32_t outCount) { + (void)session; + (void)in; + (void)inCount; + + if (method != SMP_ENV_IPMI_CMD_QUERY) { + logf_("unknown method %#x -- refusing", method); + return SM_ENV_IPMI_ENOTSUP; + } + + if (outCount < 1 || !out || !out[0].data || + out[0].capacity < sizeof(SmpEnvReply)) { + logf_("QUERY with no room for the reply (outCount=%u capacity=%zu, need " + "%zu) -- refusing", + outCount, (outCount && out) ? out[0].capacity : 0, + sizeof(SmpEnvReply)); + return SM_ENV_IPMI_ENOTSUP; + } + + SmpEnvReply reply; + memset(&reply, 0, sizeof(reply)); + reply.reply_version = SMP_ENV_REPLY_VERSION; + // Both forms of our own version: the comparable one a gate tests against, and + // the git tag a human reads. Sending both makes a forgotten + // SMP_ENV_SMP_VERSION bump visible instead of silent. + reply.smp_version_num = SMP_ENV_SMP_VERSION; + (void)strlcpy(reply.smp_version, SHADOWMOUNT_VERSION, + sizeof(reply.smp_version)); + + // Is kstuff loaded, and is it on right now. + if (sm_kstuff_is_supported()) { + reply.flags |= SMP_ENV_FLAG_KSTUFF_PRESENT; + if (sm_kstuff_is_enabled()) + reply.flags |= SMP_ENV_FLAG_KSTUFF_ENABLED; + } + + // What it actually patched, read out of ShellCore's live text. Without + // CAPS_VALID the capabilities are unknown, not absent. + uint32_t caps = 0u; + if (sm_kstuff_probe_caps(&caps)) { + reply.kstuff_caps = caps; + reply.flags |= SMP_ENV_FLAG_CAPS_VALID; + } + + memcpy(out[0].data, &reply, sizeof(reply)); + + // A truthful length, always. The framework reuses the out buffer and leaves + // `written` uninitialised; respondToSyncMethodRequest reads it and IPMIMGR + // kills the client over a mismatch. + out[0].written = sizeof(reply); + + logf_("QUERY -> flags=%#x caps=%#x smp=%s", (unsigned)reply.flags, + (unsigned)reply.kstuff_caps, reply.smp_version); + return 0; +} + +// --------------------------------------------------------------------------- +// Lifecycle. +// --------------------------------------------------------------------------- + +bool sm_env_ipmi_serve(void) { + _Static_assert(sizeof(SMP_ENV_IPMI_SERVICE) <= 14, + "Config::name is char[16] with the NUL; the client and server " + "halves truncate differently, so 14 is the budget"); + _Static_assert(sizeof(SmpEnvReply) == 48, + "SmpEnvReply crosses a process boundary as raw bytes; " + "every client's copy must agree byte for byte"); + + if (g_srv) + return true; + + if (!g_syms_ok && !(g_syms_ok = ipmi_syms_resolve(&g_syms))) { + logf_("libSceIpmi is unreadable -- not registering"); + return false; + } + + if (!name_is_free()) + return false; + + const HandlerBuild build = handler_build(&g_syms); + if (!build.handler) { + logf_("EventHandler layout not measurable -- refusing to guess it"); + return false; + } + if (!build.syncDispatchProven) { + logf_("the sync-dispatch slot was not proven; the service would register " + "but never serve. Refusing -- a name held by something that cannot " + "answer is worse than no service at all."); + return false; + } + + // The same rule, one symbol earlier. ipmi_syms_resolve() succeeds as long as + // the library opened; an individual slot may still be null. The dispatcher + // polls tryDispatch and will not fall back to runDispatcher, so without this + // symbol the thread exits on its first iteration while the registration + // stays. Checked before create() so there is nothing to tear down. + if (!g_syms.srvTryDispatch) { + logf_("ServerImpl::tryDispatch did not resolve; the service would register " + "but never serve. Refusing."); + return false; + } + + // Zeroed before the constructor runs, which is also what keeps cfg.gate32 and + // cfg.gate33 at 0 -- create() reads both. See IpmiServerConfig. + static IpmiServerConfig cfg; + memset(&cfg, 0, sizeof(cfg)); + ipmi_server_config_ctor(&cfg); + cfg.poolSize = 0x20000; // known-good pool size + cfg.eventHandler = build.handler; // +0x10, load-bearing: null gives EINVAL + cfg.flag = 1; + memset(cfg.name, 0, sizeof(cfg.name)); + strncpy(cfg.name, SMP_ENV_IPMI_SERVICE, sizeof(cfg.name) - 1); + + // Not scratch: create() constructs the ServerImpl into this and returns it as + // the Server*, so it has to outlive the registration -- hence static. The + // object measures 0x30 bytes; this is roomy on purpose, since being short + // here would corrupt the server itself. + static unsigned char g_srv_storage[0x1000]; + memset(g_srv_storage, 0, sizeof(g_srv_storage)); + + logf_("calling Server::create for " SMP_ENV_IPMI_SERVICE " -- if the log " + "stops here with an IPMIMGR exception, the name was taken after all"); + const int rc = ipmi_server_create(&g_srv, &cfg, NULL, g_srv_storage); + logf_("Server::create -> rc=%#010x srv=%p", rc, (void *)g_srv); + if (rc < 0 || !g_srv) { + g_srv = NULL; + return false; + } + + // Size the working buffer off the same Config that built the server; + // measured 0x20100. The return type is not published, so an implausible value + // is a decoding problem, not a genuine request for that much memory. + const uint64_t rawEstimate = ipmi_server_config_estimate(&cfg); + g_work_size = rawEstimate; + if (g_work_size == 0 || g_work_size > 0x1000000u) + g_work_size = rawEstimate & 0xffffffffu; + if (g_work_size == 0 || g_work_size > 0x1000000u) { + logf_("estimateTempWorkingMemorySize returned %#llx, implausible -- using " + "0x20000", + (unsigned long long)rawEstimate); + g_work_size = 0x20000; + } + + g_work_buf = malloc(g_work_size); + if (!g_work_buf) { + logf_("could not allocate %#llx bytes for the dispatcher", + (unsigned long long)g_work_size); + server_teardown("no working buffer"); + return false; + } + memset(g_work_buf, 0, g_work_size); + + g_disp_stop = false; + pthread_t th; + if (pthread_create(&th, NULL, dispatcher_thread, NULL) != 0) { + logf_("could not start the dispatcher thread -- unregistering rather than " + "holding a name nothing will answer"); + server_teardown("dispatcher thread would not start"); + return false; + } + pthread_detach(th); + + logf_(SMP_ENV_IPMI_SERVICE " registered and serving"); + return true; +} + +void sm_env_ipmi_shutdown(void) { + server_teardown("shutdown"); +} diff --git a/src/sm_kstuff_caps.c b/src/sm_kstuff_caps.c new file mode 100644 index 0000000..a97bdea --- /dev/null +++ b/src/sm_kstuff_caps.c @@ -0,0 +1,173 @@ +#include "sm_platform.h" + +#include "sm_kstuff_caps.h" +#include "sm_log.h" +#include "sm_runtime.h" + +// Measured: the kernel reports "SceShellCore", with no ".elf" -- unlike its +// siblings SceSysCore.elf and mini-syscore.elf. Getting this wrong makes the +// probe report "not running", which is indistinguishable from a real answer of +// "no patches" unless you check. +static const char *const k_shellcore_name = "SceShellCore"; +#define SHELLCORE_MAIN_MODULE_HANDLE 0u + +// The two SceShellCore patches kstuff-lite applies and full kstuff does not. +// Offsets are kstuff-lite's own retail tables; on a testkit or devkit they miss +// and the signature match below then correctly reports "not patched". +typedef struct { + uint32_t fw; + uint32_t sysdir_off; + uint32_t trophy_off; + uint8_t sysdir_len; +} shellcore_cap_row_t; + +// getSceSysDirPath is NOP-ed: a 6-byte NOP before 7.00, a 2-byte one after. +// The trophy fix flips one conditional jump to an unconditional 0xEB. +static const uint8_t k_sysdir_wide[6] = {0x66, 0x0f, 0x1f, 0x44, 0x00, 0x00}; +static const uint8_t k_sysdir_short[2] = {0x66, 0x90}; +#define TROPHY_PATCH_BYTE 0xEBu + +static const shellcore_cap_row_t g_cap_rows[] = { + {0x02500000, 0x3b271c, 0x7d8584, 6}, // 2.50 + {0x03000000, 0x3fc24c, 0x8b5634, 6}, // 3.00 + {0x03100000, 0x3fc28c, 0x8b5674, 6}, // 3.10 + {0x03200000, 0x3fc33c, 0x8b5924, 6}, // 3.20 + {0x03210000, 0x3fc33c, 0x8b5924, 6}, // 3.21 + {0x04000000, 0x43db4c, 0x8337a7, 6}, // 4.00 + {0x04020000, 0x43db4c, 0x8337a7, 6}, // 4.02 + {0x04030000, 0x43db4c, 0x8337a7, 6}, // 4.03 + {0x04500000, 0x43e29c, 0x834117, 6}, // 4.50 + {0x04510000, 0x43e29c, 0x834127, 6}, // 4.51 + {0x05000000, 0x4a3e7c, 0x8e2c87, 6}, // 5.00 + {0x05020000, 0x4a3e6c, 0x8e2c77, 6}, // 5.02 + {0x05100000, 0x4a5d9c, 0x8e5647, 6}, // 5.10 + {0x05500000, 0x4a5d9c, 0x8e6057, 6}, // 5.50 + {0x06000000, 0x4d3bec, 0x92e937, 6}, // 6.00 + {0x06020000, 0x4d3bec, 0x92e8d7, 6}, // 6.02 + {0x06500000, 0x4d3c5c, 0x92f107, 6}, // 6.50 + {0x07000000, 0x579656, 0x9e7e96, 2}, // 7.00 + {0x07010000, 0x579656, 0x9e7e96, 2}, // 7.01 + {0x07200000, 0x579676, 0x9e8776, 2}, // 7.20 + {0x07400000, 0x57e166, 0x9f3e66, 2}, // 7.40 + {0x07600000, 0x57e166, 0x9f7446, 2}, // 7.60 + {0x07610000, 0x57e166, 0x9f7446, 2}, // 7.61 + {0x08000000, 0x5a5f53, 0xa4969d, 2}, // 8.00 + {0x08200000, 0x5a7023, 0xa50b0d, 2}, // 8.20 + {0x08400000, 0x5a7023, 0xa50b0d, 2}, // 8.40 + {0x08600000, 0x5a6e03, 0xa5099d, 2}, // 8.60 + {0x09000000, 0x5da91a, 0xab2c91, 2}, // 9.00 + {0x09050000, 0x5da91a, 0xab2c91, 2}, // 9.05 + {0x09200000, 0x5da63a, 0xab29d1, 2}, // 9.20 + {0x09400000, 0x5dad0a, 0xab3121, 2}, // 9.40 + {0x09600000, 0x5dad7a, 0xabb4f1, 2}, // 9.60 + {0x10000000, 0x5d8511, 0xaa9cc1, 2}, // 10.00 + {0x10010000, 0x5d8511, 0xaa9cc1, 2}, // 10.01 + {0x10200000, 0x5d8511, 0xaadf81, 2}, // 10.20 + {0x10400000, 0x5d8461, 0xaadfa1, 2}, // 10.40 + {0x10600000, 0x5d9cf1, 0xaaf831, 2}, // 10.60 + {0x11000000, 0x638caa, 0xaec74a, 2}, // 11.00 + {0x11200000, 0x638e1a, 0xaecada, 2}, // 11.20 + {0x11400000, 0x639dfa, 0xaee68a, 2}, // 11.40 + {0x11600000, 0x64186a, 0xaf63ea, 2}, // 11.60 + {0x12000000, 0x6557aa, 0xb1b02a, 2}, // 12.00 + {0x12020000, 0x6557aa, 0xb1b02a, 2}, // 12.02 + {0x12200000, 0x6557aa, 0xb1beba, 2}, // 12.20 + {0x12400000, 0x6557aa, 0xb1beba, 2}, // 12.40 + {0x12600000, 0x6569fa, 0xb21d1a, 2}, // 12.60 + {0x12700000, 0x6569fa, 0xb21d1a, 2}, // 12.70 +}; + +static bool g_caps_probed = false; +static bool g_caps_valid = false; +static uint32_t g_caps = 0; + +static const shellcore_cap_row_t *find_cap_row(void) { + uint32_t fw = kernel_get_fw_version() & 0xffff0000u; + for (size_t i = 0; i < sizeof(g_cap_rows) / sizeof(g_cap_rows[0]); i++) { + if (g_cap_rows[i].fw == fw) + return &g_cap_rows[i]; + } + return NULL; +} + +static bool run_probe(uint32_t *caps_out) { + const shellcore_cap_row_t *row = find_cap_row(); + if (!row) { + log_debug(" [KCAPS] no patch offsets for fw 0x%08x", kernel_get_fw_version()); + return false; + } + + pid_t pid = find_pid_by_name(k_shellcore_name, false); + if (pid <= 0) { + log_debug(" [KCAPS] SceShellCore not running"); + return false; + } + + intptr_t base = kernel_dynlib_mapbase_addr(pid, SHELLCORE_MAIN_MODULE_HANDLE); + if (base <= 0) { + log_debug(" [KCAPS] no mapbase for pid=%ld", (long)pid); + return false; + } + + // Match the whole patch signature, not just its first byte: on a testkit or + // devkit these retail offsets land somewhere unrelated, and a partial match + // there would report a capability the running kstuff does not have. + uint8_t sysdir[6]; + uint8_t trophy = 0; + if (kernel_proc_copyout(pid, base + row->sysdir_off, sysdir, row->sysdir_len) || + kernel_proc_copyout(pid, base + row->trophy_off, &trophy, 1)) { + log_debug(" [KCAPS] copyout failed from pid=%ld", (long)pid); + return false; + } + + const uint8_t *want = row->sysdir_len == 6 ? k_sysdir_wide : k_sysdir_short; + uint32_t caps = 0; + if (memcmp(sysdir, want, row->sysdir_len) == 0) + caps |= SM_KSTUFF_CAP_SYSDIRPATH; + if (trophy == TROPHY_PATCH_BYTE) + caps |= SM_KSTUFF_CAP_TROPHY; + + log_debug(" [KCAPS] sysdirpath=%s trophy=%s", + (caps & SM_KSTUFF_CAP_SYSDIRPATH) ? "yes" : "no", + (caps & SM_KSTUFF_CAP_TROPHY) ? "yes" : "no"); + *caps_out = caps; + return true; +} + +bool sm_kstuff_probe_caps(uint32_t *caps) { + // Only a positive answer is cached. + // + // ShellCore is patched once at kstuff load and never un-patched, so a + // positive can never go stale. A zero is different: it may only mean kstuff + // has not run yet, which depends on autoload ordering we do not control. This + // used to latch whichever answer came first, and a console with kstuff-lite + // correctly installed then refused to launch a backported title, the gate + // reporting sysdirpath=no trophy=no with CAPS_VALID set -- a probe that + // succeeded and honestly saw an unpatched ShellCore, cached for the boot. + // + // Confirmed with kstuff-lite loading after this payload: the startup probe + // reads caps=0x0, and the next probe 11s later reads caps=0x3. Load order no + // longer matters. + // + // Deliberately not time-throttled: the callers are our own startup and the + // one query a title makes during module init, so a retry costs one + // find_pid_by_name plus two small copyouts, and only until the answer turns + // positive. + if (!g_caps_probed || !g_caps_valid || g_caps == 0) { + uint32_t probed = 0; + if (run_probe(&probed)) { + g_caps = probed; + g_caps_valid = true; + } else if (!g_caps_probed) { + // Keep any earlier successful reading rather than downgrading it: a probe + // that fails later (ShellCore momentarily unfindable) is not evidence + // that the patches went away. + g_caps_valid = false; + } + g_caps_probed = true; + } + + if (caps && g_caps_valid) + *caps = g_caps; + return g_caps_valid; +}