From 190880ebfcd9f65616d6fcc0a81178d7ebe3a4c5 Mon Sep 17 00:00:00 2001 From: sparky3387 Date: Sun, 6 Sep 2026 12:36:48 +1000 Subject: [PATCH 1/2] Add an environment IPMI service reporting kstuff's ShellCore capabilities A backported title needs to know whether the loaded kstuff carries the getSceSysDirPath patch its NP registrations depend on, and it cannot find out from inside its sandbox. Asking for a version does not work, and not because kstuff is gone: the loader exits, but a micro-ELF stays resident. That resident half carries no version string, its kekcall interface has no call that would return one, and the ShellCore patches are applied by the loader, so it never learns they happened. A version would not be trustworthy even if it existed -- the kstuff-lite-dr fork numbers above 1.10 and carries neither patch, so a minimum-version check waves through the exact build that does not work. What is knowable is the patch itself, and both patches are readable straight out of SceShellCore's live text. No release of full kstuff carries either one, and in kstuff-lite they arrive together in v1.07. sm_kstuff_caps.c reads them: a per-firmware offset table for 2.50 through 12.70, matched against the whole patch signature so a testkit or devkit layout cannot produce a false positive. sm_env_ipmi.c serves that over IPMI as "SceShadowMnt", one argument-free query, alongside ShadowMountPlus's own version so a title can require a minimum build. The service is never fatal: a console with no backported title does not need it, so a failure is logged and startup continues. Only a positive capability reading is cached. ShellCore is never un-patched, so a positive cannot go stale, but a zero may only mean kstuff has not run yet -- which depends on autoload ordering we do not control. Latching the first answer made a console with kstuff-lite correctly installed refuse to launch a backported title, the gate reporting sysdirpath=no trophy=no with CAPS_VALID set. Confirmed with kstuff-lite autoloading after this payload: the startup probe reads caps=0x0 and the next probe eleven seconds later reads caps=0x3. The IPMI layer is plain C. It describes a C++ ABI, but describing one needs no C++ compiler: a mangled name reached through an asm label and a vtable slot found by address work the same from C, so there is no cxx_rt.cpp, no separate CXXFLAGS and no compile-with-CXX-link-with-CC rule. Everything asserted in these files was confirmed against a live, working registration on hardware. Worth stating explicitly, because each one is a mistake that is easy to make and expensive to find: - Server::create's fourth argument is not scratch. It placement-constructs the ServerImpl into it and returns that pointer as the Server*, so it has to outlive the registration. Measured: out == storage, and the object is 0x30 bytes. - Config+0x32 is copied to ServerImpl+0x28, and tryDispatch refuses to run on a non-zero value, so it is a named field with an offset assertion rather than padding. - The EventHandler vtable ends after nine virtuals, and the words past the end belong to the next class, so only the slots the scan can name are copied. - Poll tryDispatch, never runDispatcher: runDispatcher checks its shutdown flag only before each receive, so one already asleep survives SIGKILL, keeps the service name and blocks every client forever. - 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(), and only a reboot clears that. - The service name must start with "Sce" and fit Config::name[16] including the NUL. IPMIMGR kills the caller from inside create() over the prefix, and the server and client halves truncate differently at the boundary, so a long name registers under one spelling and is looked up under another. --- .gitignore | 1 + Makefile | 7 +- include/ipmi.h | 106 ++++++++ include/ipmi_client.h | 32 +++ include/ipmi_handler.h | 33 +++ include/ipmi_log.h | 12 + include/ipmi_symbols.h | 96 +++++++ include/sm_env_ipmi.h | 103 ++++++++ include/sm_env_ipmi_dispatch.h | 26 ++ include/sm_kstuff_caps.h | 17 ++ src/ipmi_client.c | 144 ++++++++++ src/ipmi_handler.c | 470 +++++++++++++++++++++++++++++++++ src/ipmi_symbols.c | 196 ++++++++++++++ src/main.c | 15 ++ src/sm_env_ipmi.c | 384 +++++++++++++++++++++++++++ src/sm_kstuff_caps.c | 173 ++++++++++++ 16 files changed, 1812 insertions(+), 3 deletions(-) create mode 100644 include/ipmi.h create mode 100644 include/ipmi_client.h create mode 100644 include/ipmi_handler.h create mode 100644 include/ipmi_log.h create mode 100644 include/ipmi_symbols.h create mode 100644 include/sm_env_ipmi.h create mode 100644 include/sm_env_ipmi_dispatch.h create mode 100644 include/sm_kstuff_caps.h create mode 100644 src/ipmi_client.c create mode 100644 src/ipmi_handler.c create mode 100644 src/ipmi_symbols.c create mode 100644 src/sm_env_ipmi.c create mode 100644 src/sm_kstuff_caps.c 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..212c17a --- /dev/null +++ b/src/ipmi_client.c @@ -0,0 +1,144 @@ +// 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 + logf_(" client: no destroy slot -- handle %p leaked", c->handle); + + c->handle = NULL; + } + + /* Ours to free either way: storage is our malloc, not the library's. */ + 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..3ec7f3c --- /dev/null +++ b/src/ipmi_handler.c @@ -0,0 +1,470 @@ +// 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; +} ConnectRecord; +static ConnectRecord g_connect; + +// Session memory for createSession. Static, not malloc'd: this is used from +// inside the connection window, where the less that happens the better. +static unsigned char g_sessionMem[0x20000] __attribute__((aligned(16))); + +// 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; + if (srv && 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(g_sessionMem); + 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), g_sessionMem); + g_connect.session = (u64)(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); + 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")); +} + +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..1264369 --- /dev/null +++ b/src/sm_env_ipmi.c @@ -0,0 +1,384 @@ +// 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; + + 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(g_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; + g_srv = NULL; // before the calls: nothing may reuse it + + // 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. + logf_("%s: dispatcher is still inside tryDispatch -- not destroying; " + "process exit will release the name", + why); + return; + } + + // 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; + } + + // 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; +} From 0eb9fddc73a1d5d3c941969eb0c2364ee891d977 Mon Sep 17 00:00:00 2001 From: sparky3387 Date: Sun, 6 Sep 2026 21:55:43 +1000 Subject: [PATCH 2/2] Give each IPMI session its own memory, and stop racing the dispatcher Five fixes to the environment service, four of them from review. Sessions overlap as a matter of course, so one shared buffer was never enough. Measured over four title launches on 4.03: every title connects twice about two seconds apart, the second connect landing while the first session is still alive -- eight connects, two live at once every time. createSession placement-constructs into the storage the handler supplies, so all eight sessions reported the same Session*: two handles, one object. A four-slot pool fixes that with no allocator in the connection window, claimed on connect and released in onSessionKilled, which the same run showed fires for every session. No lock, because connect, dispatch and the kill callback all arrive on the dispatcher thread. The dispatcher now takes its Server* once instead of re-reading g_srv each iteration. Teardown cleared g_srv before signalling the stop, so a dispatcher already past its check called tryDispatch(NULL) -- and g_srv is not volatile, so that read raced as well. g_srv is now cleared only once the dispatcher is provably out, and left alone on the wedged path where the registration really is still live. Also: require cfg before writing memorySize through it, since the callback already treats it as possibly null a few lines earlier; refuse to register when ServerImpl::tryDispatch is unresolved, the same rule already applied to the sync-dispatch slot; and leak client storage rather than freeing it when no destroy slot was found, since the library still holds a kid pointing into it. --- src/ipmi_client.c | 19 ++++++++++++--- src/ipmi_handler.c | 61 +++++++++++++++++++++++++++++++++++++++++----- src/sm_env_ipmi.c | 27 ++++++++++++++++++-- 3 files changed, 95 insertions(+), 12 deletions(-) diff --git a/src/ipmi_client.c b/src/ipmi_client.c index 212c17a..19cc0d8 100644 --- a/src/ipmi_client.c +++ b/src/ipmi_client.c @@ -129,15 +129,26 @@ void ipmi_client_close(IpmiClient* c) { * 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) + if (c->destroySlot >= 0) { (void)((DestroyFn)vt[c->destroySlot])(c->handle); - else - logf_(" client: no destroy slot -- handle %p leaked", 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 either way: storage is our malloc, not the library's. */ + /* 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 index 3ec7f3c..49d88b4 100644 --- a/src/ipmi_handler.c +++ b/src/ipmi_handler.c @@ -87,12 +87,46 @@ typedef struct ConnectRecord { int createSessionSlot; int createSessionRc; u64 session; + bool noSessionSlot; } ConnectRecord; static ConnectRecord g_connect; -// Session memory for createSession. Static, not malloc'd: this is used from -// inside the connection window, where the less that happens the better. -static unsigned char g_sessionMem[0x20000] __attribute__((aligned(16))); +// 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. @@ -118,13 +152,19 @@ static void capture_connect(int slot, void* self, u64 srv, u64 cfg, u64 extra) { g_connect.createSessionSlot = -1; g_connect.createSessionRc = 0; g_connect.session = 0; - if (srv && g_syms && g_syms->srvCreateSession) { + // 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(g_sessionMem); + const uint64_t have = sizeof(slot_mem->mem); memcpy((unsigned char*)(cfg) + 0x148, &have, 8); g_connect.memorySizeSet = have; @@ -139,8 +179,12 @@ static void capture_connect(int slot, void* self, u64 srv, u64 cfg, u64 extra) { g_connect.createSessionRc = ((CreateSessionFn)vt[slotIdx])( (void*)(srv), &session, - (void*)(cfg), g_sessionMem); + (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; @@ -267,6 +311,7 @@ static int64_t slot_dispatch(int slot, void* self, u64 a1, u64 a2, u64 a3, u64 a 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; @@ -374,6 +419,10 @@ void handler_drain_connect_log(void) { ? " <-- 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) { diff --git a/src/sm_env_ipmi.c b/src/sm_env_ipmi.c index 1264369..53de851 100644 --- a/src/sm_env_ipmi.c +++ b/src/sm_env_ipmi.c @@ -90,6 +90,13 @@ static void *dispatcher_thread(void *arg) { // 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) { @@ -101,7 +108,7 @@ static void *dispatcher_thread(void *arg) { logf_("polling tryDispatch every %ums", kPollMs); while (!g_disp_stop) { - const int rc = tryDispatch(g_srv, g_work_buf, g_work_size); + const int rc = tryDispatch(srv, g_work_buf, g_work_size); if (rc != 0) { logf_("tryDispatch rc=%#010x -- stopping", (unsigned)rc); break; @@ -180,7 +187,6 @@ static void server_teardown(const char *why) { if (!srv) return; - g_srv = NULL; // before the calls: nothing may reuse it // 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 @@ -193,12 +199,18 @@ static void server_teardown(const char *why) { // 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, @@ -315,6 +327,17 @@ bool sm_env_ipmi_serve(void) { 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;