Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ notify.txt
src/notify_icon_asset.c
refs/*
docs/*
*.so
7 changes: 4 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
106 changes: 106 additions & 0 deletions include/ipmi.h
Original file line number Diff line number Diff line change
@@ -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 <stddef.h>
#include <stdint.h>

// 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_");
32 changes: 32 additions & 0 deletions include/ipmi_client.h
Original file line number Diff line number Diff line change
@@ -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 <stdbool.h>

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);
33 changes: 33 additions & 0 deletions include/ipmi_handler.h
Original file line number Diff line number Diff line change
@@ -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 <stdbool.h>

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);
12 changes: 12 additions & 0 deletions include/ipmi_log.h
Original file line number Diff line number Diff line change
@@ -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 <stddef.h>

void logf_(const char *fmt, ...) __attribute__((format(printf, 1, 2)));

void log_hexdump(const char *label, const void *p, size_t n);
96 changes: 96 additions & 0 deletions include/ipmi_symbols.h
Original file line number Diff line number Diff line change
@@ -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 <stdbool.h>

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);
Loading