Skip to content
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ notify.txt
src/notify_icon_asset.c
refs/*
docs/*
*.so

# Generated: carries VERSION_TAG so the objects that embed it rebuild when it moves.
src/version_tag.stamp
22 changes: 18 additions & 4 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 All @@ -43,5 +44,18 @@ src/config_ini_example_asset.c: config.ini.example
src/%.o: src/%.c $(HEADERS)
$(CC) $(CFLAGS) -c -o $@ $<

# VERSION_TAG reaches the code as a COMMAND-LINE macro, so nothing in the
# dependency graph moves when `git describe` does: a new commit leaves main.o
# alone and the banner then names the wrong build. Measured 2026-09-14 -- the
# console reported 1.6-6-ge19189 while running 26fa77c's code, which is the one
# thing we identify a deployed build by. The stamp carries the tag and is
# rewritten only when it actually changes, so the three objects that embed it
# rebuild exactly when they must and incremental builds stay incremental.
.PHONY: force
src/version_tag.stamp: force
@printf '%s' '$(VERSION_TAG)' | cmp -s - $@ || printf '%s' '$(VERSION_TAG)' > $@

src/main.o src/sm_log.o src/sm_env_ipmi.o: src/version_tag.stamp

clean:
rm -f shadowmountplus.elf kill.elf src/*.o $(KERNEL_SYS_STUB_SO) src/notify_icon_asset.c src/config_ini_example_asset.c
rm -f shadowmountplus.elf kill.elf src/*.o src/version_tag.stamp $(KERNEL_SYS_STUB_SO) src/notify_icon_asset.c src/config_ini_example_asset.c
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);
98 changes: 98 additions & 0 deletions include/ipmi_symbols.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// 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* sessRespondAsyncData;
void* sessRespondAsyncRaw;
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