mirror of
https://github.com/SirHumza/orbisRPC.git
synced 2026-10-06 10:00:36 +02:00
remaster: any-firmware hardening + installer remake
- detection: probe-first (ShellCoreUtil dlopen, msgbuf AppFocusChanged, sysctl kinfo via verified-only fw table, sandbox/save fallbacks) - new orbisrpc/focus.c + fw.c/fw.h (bounded scans, fail-closed unknowns) - daemon: no-exit everywhere (token wait-loop, 4004 retry), status.json heartbeat, daemon.gen supersede protocol - installer: progress UI, token IME retry + FTP fallback, install.log, drop evict.elf (delete tools/evict.c, build_evict.sh) - packaging: Apollo parity CATEGORY=gde ATTRIBUTE=32 --authinfo - security: chmod 0600 on token-bearing files - docs: firmware-independent injecting guide, SUPPORT.md, release.sh
This commit is contained in:
1 parent
68a34993da
commit
d0972b68e9
25 files changed
+587
-181
No files matched your search
@@ -26,6 +26,5 @@ installer/x64/
|
||||
installer/eboot.bin
|
||||
installer/pkg.gp4
|
||||
installer/assets/daemon.elf
|
||||
installer/assets/evict.elf
|
||||
installer/sce_sys/param.sfo
|
||||
tests/test_utils_asan
|
||||
@@ -22,8 +22,8 @@ playing to Discord: name, cover art, timer. No PC at runtime.</p>
|
||||
1. Grab `OrbisRPC-Setup-1.0.0.pkg` from the
|
||||
[Releases page](https://github.com/SirHumza/orbisRPC/releases/tag/v1.0.0)
|
||||
and install it with Package Installer.
|
||||
2. Open **orbisRPC Setup**. It stages `orbisrpc.bin` + `evict.elf` in
|
||||
`/data/payloads`, writes `/data/orbisRPC/config.json`, evicts any old
|
||||
2. Open **orbisRPC Setup**. It stages `orbisrpc.bin` in
|
||||
`/data/payloads`, writes `/data/orbisRPC/config.json`, retires any old
|
||||
daemon, then asks for your Discord token.
|
||||
3. Open **Payloads** in GoldHEN settings, go to `orbisrpc.bin`, press
|
||||
**Square** to enable AutoRun, then **X** once to run it. Launch a
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
# orbisRPC support bundle
|
||||
|
||||
Paste this into your issue (find values via FTP, no klog needed):
|
||||
|
||||
- Firmware + jailbreak entry (e.g. 9.00 pOOBs4, GoldHEN version):
|
||||
- Install method (Setup PKG / BinLoader / elfldr):
|
||||
- `/data/orbisRPC/status.json` contents:
|
||||
- `/data/orbisRPC/log.txt` tail (last ~30 lines):
|
||||
- `/data/orbisRPC/install.log` tail (installer problems only):
|
||||
- Game title ID where it fails (e.g. CUSA00001):
|
||||
- What you expected vs what happened:
|
||||
+16
-16
@@ -3,10 +3,9 @@
|
||||
## What it does
|
||||
|
||||
One linear flow, forward-only (every No skips ahead, nothing loops back):
|
||||
confirm → evict old daemon (via `sceKernelLoadStartModule`) → copy
|
||||
`evict.elf` + `orbisrpc.bin` to `/data/payloads/` (mkdir -p +
|
||||
confirm → copy `orbisrpc.bin` to `/data/payloads/` (mkdir -p +
|
||||
byte-count + FNV hash read-back) → pre-saved config with token
|
||||
(skips entry when valid) → done.
|
||||
(skips entry when valid) → generation bump → done.
|
||||
Read-only status screen available via decline.
|
||||
|
||||
There is no WiFi check. The installer runs sandboxed, so its socket probe
|
||||
@@ -15,24 +14,24 @@ reads like a verdict on Discord itself. The daemon reports real reachability
|
||||
in its own log once started.
|
||||
|
||||
The installer never boots anything directly. Starting the daemon is
|
||||
Payload Guest's `/data/payloads/` directory — pick `evict.elf` first
|
||||
(removes old orbisrpc instance), then pick `orbisrpc.bin`.
|
||||
Payload Guest's `/data/payloads/` directory — pick `orbisrpc.bin`.
|
||||
An older running daemon sees the installer's generation bump in
|
||||
`daemon.gen` and exits cleanly on its own (no killer payload needed).
|
||||
No loopback ports, no injection, no boot proof to go wrong.
|
||||
|
||||
## Install flow
|
||||
|
||||
```
|
||||
confirm → sceKernelLoadStartModule(evict.elf) — kills old daemon
|
||||
→ copy evict.elf to /data/payloads/evict.elf
|
||||
→ copy orbisrpc.bin to /data/payloads/orbisrpc.bin
|
||||
confirm → copy orbisrpc.bin to /data/payloads/orbisrpc.bin
|
||||
→ save config.json with Discord token (pre-populated)
|
||||
→ (token entry skipped if already valid)
|
||||
→ bump daemon.gen (old daemon exits cleanly)
|
||||
→ done
|
||||
```
|
||||
|
||||
The `evict.elf` is launched via `sceKernelLoadStartModule` during
|
||||
install — it reads `daemon.lock`, kills the running daemon, exits.
|
||||
`evict.elf` stays in `/data/payloads/` for future manual use.
|
||||
The installer bumps `daemon.gen` after copying — any older running
|
||||
daemon sees the new generation and exits cleanly by itself, so the
|
||||
fresh payload takes over with no killer module and no sandbox fights.
|
||||
|
||||
## Navigation law
|
||||
|
||||
@@ -57,8 +56,9 @@ button on No, which inverts the whole wizard.
|
||||
- Config writes are atomic (tmp+fsync+rename) with read-back proof.
|
||||
- IME wait is bounded; asset key charset validated (bad keys blank the
|
||||
activity).
|
||||
- `evict.elf` is loaded via `sceKernelLoadStartModule` because `kill()`
|
||||
is blocked by the sandbox (EPERM).
|
||||
- Old daemons are retired via the `daemon.gen` generation bump, not
|
||||
signals: `kill()` is blocked by the sandbox (EPERM), so the running
|
||||
daemon polls the generation file and exits cleanly when superseded.
|
||||
|
||||
## Auto-start
|
||||
|
||||
@@ -69,7 +69,7 @@ shows where, it can't write the queue itself.
|
||||
## Building
|
||||
|
||||
`make -f installer/Makefile` (`OO_PS4_TOOLCHAIN`, llvmshim). Staged assets:
|
||||
`daemon.elf`, `evict.elf`, `config.json`. The PKG ships all three —
|
||||
the installer copies both payloads, launches evict.elf to kill the
|
||||
old daemon, and pre-saves the config token.
|
||||
`daemon.elf`, `config.json`. The PKG ships both —
|
||||
the installer copies the payload, pre-saves the config token,
|
||||
and bumps `daemon.gen` so an older daemon retires itself.
|
||||
Output: `IV0000-ORPC00001_00-ORBISRPCSETUP000.pkg`.
|
||||
+4
-3
@@ -25,7 +25,8 @@ libkernel.so). Running on-console at 192.168.1.136 via elfldr:9021.
|
||||
gate, safe mode, signed all-or-nothing staging, boot rollback.
|
||||
- `cfg.c`/`lock.c`/`timesync.c` — schema'd fallible config, sysctl-liveness
|
||||
single instance, SNTP wall clock for timer ms.
|
||||
- `tools/evict.c` — SIGTERM-then-SIGKILL deploy rotation for the locked daemon.
|
||||
- `installer/` + daemon generation protocol — installer bumps
|
||||
`daemon.gen`; older daemons exit cleanly when superseded (no signals).
|
||||
|
||||
## State machine
|
||||
|
||||
@@ -50,7 +51,7 @@ Sandbox param.sfo source, details-ID duplication, user `titles`
|
||||
overrides, `home_art`, shipped logo default, app.db SQLite names,
|
||||
self-learning map, presence-builder test seam, small badge, strncpy NUL
|
||||
hardening, home_art validation, 4004 survival, cfg_save return,
|
||||
evict.elf deploy tool, CI ASan+e2e jobs.
|
||||
generation-based deploy rotation, CI ASan+e2e jobs.
|
||||
|
||||
## Not fixed (external / out of scope)
|
||||
|
||||
@@ -71,7 +72,7 @@ evict.elf deploy tool, CI ASan+e2e jobs.
|
||||
## Hardware matrix (proven)
|
||||
|
||||
Gateway ready, TLS-ECDHE suite, game detection + ticking timer post-SNTP,
|
||||
mp: art serving (phone), duplicate-ID gone, evict clean-stop rotations,
|
||||
mp: art serving (phone), duplicate-ID gone, generation clean-stop rotations,
|
||||
reboot recovery. Pending eyes: appdb name flip, badge render, logo tile.
|
||||
|
||||
## Readiness
|
||||
|
||||
+13
-6
@@ -1,12 +1,18 @@
|
||||
# Injecting OrbisRPC into the PS4 — the complete guide
|
||||
|
||||
This exists because getting a payload to *execute* took longer than
|
||||
writing the payload. Everything below was learned on a real 9.00 console.
|
||||
writing the payload. Everything below was learned on real consoles
|
||||
(9.00 primary). The flow is firmware-independent: the payload resolves
|
||||
its symbols at runtime and probes firmware-specific details (kinfo
|
||||
layout via a version table with a bounded-scan fallback), so the same
|
||||
binary runs anywhere you can get a loader listening.
|
||||
|
||||
## You need first
|
||||
|
||||
- PS4 on 9.00, jailbroken with GoldHEN (2.4b18+ recommended — older
|
||||
payloader builds segfault on ELF files, see below).
|
||||
- PS4 on a jailbreakable firmware (9.00 via pOOBs4 is the proven path;
|
||||
newer firmwares need their own entry point, e.g. lapse-based hosts —
|
||||
once jailbroken the steps below are identical), GoldHEN 2.4b18+
|
||||
recommended (older payloader builds segfault on ELF files, see below).
|
||||
- Console and computer on the same network. Find the PS4 IP:
|
||||
Settings → Network → View Connection Status (ours is `192.168.1.136`).
|
||||
- The payload file: `build-sdk/orbisrpc_sdk.elf` (built via
|
||||
@@ -14,7 +20,8 @@ writing the payload. Everything below was learned on a real 9.00 console.
|
||||
|
||||
## Method 1 — elfldr (recommended)
|
||||
|
||||
elfldr is a proper ELF loader that runs payloads as separate processes.
|
||||
elfldr (ps4-payload-dev's `ps4-payload-elfldr`) is a proper ELF loader
|
||||
that runs payloads as separate processes with runtime symbol resolution.
|
||||
|
||||
1. Get it listening. Either load `elfldr.elf` through GoldHEN's payloader
|
||||
page once, or keep it running — it serves on **port 9021** until reboot.
|
||||
@@ -88,8 +95,8 @@ the result on screen.
|
||||
| `Connection refused` on 9021/9020 | No listener armed | Tap BinLoader / open payloader page, retry instantly |
|
||||
| `payload launched successfully` then silence, no log | Loader segfault (see above) | Update GoldHEN ≥ v2.4b18.5, use BinLoader server |
|
||||
| `Error handling payload` | Loader rejected the bytes | Re-check file integrity (`shasum`), resend |
|
||||
| Log exists but `FATAL: token rejected (4004)` | Token rotated/dead | Fresh token into `/data/orbisRPC/config.json`, relaunch |
|
||||
| Multiple `Payload` processes in process list | Old instances piled up | Reboot clears them; the daemon's lock prevents recurrence |
|
||||
| Log shows `token rejected (4004)` | Token rotated/dead | Fresh token into `/data/orbisRPC/config.json` — the daemon picks it up alone, no relaunch needed |
|
||||
| Multiple `Payload` processes in process list | Old instances piled up | Reinstall: the `daemon.gen` bump retires them; reboot clears stragglers |
|
||||
|
||||
## Watching it work
|
||||
|
||||
|
||||
+5
-7
@@ -1,5 +1,5 @@
|
||||
# orbisRPC Setup installer (OpenOrbis app).
|
||||
# Flow: daemon payload -> evict old -> token -> done.
|
||||
# Flow: daemon payload -> token -> done (gen bump supersedes old daemon).
|
||||
# Payload Guest reads /data/payloads/ on this console.
|
||||
TITLE := orbisRPC Setup
|
||||
VERSION := 1.0.0
|
||||
@@ -42,8 +42,8 @@ all: $(CONTENT_ID).pkg
|
||||
$(CONTENT_ID).pkg: installer/pkg.gp4
|
||||
cd installer && $(TOOLCHAIN)/bin/$(CDIR)/PkgTool.Core pkg_build pkg.gp4 . && mv $(CONTENT_ID).pkg ..
|
||||
|
||||
installer/pkg.gp4: installer/eboot.bin installer/assets/daemon.elf installer/assets/evict.elf installer/assets/config.json installer/sce_sys/about/right.sprx installer/sce_sys/param.sfo installer/sce_sys/icon0.png installer/sce_sys/pic1.png $(LIBMODULES) $(ASSETS)
|
||||
cd installer && $(TOOLCHAIN)/bin/$(CDIR)/create-gp4 -out pkg.gp4 --content-id=$(CONTENT_ID) --files "eboot.bin assets/daemon.elf assets/evict.elf assets/config.json sce_sys/about/right.sprx sce_sys/param.sfo sce_sys/icon0.png sce_sys/pic1.png sce_module/libSceFios2.prx sce_module/libc.prx $(patsubst installer/%,%,$(ASSETS))"
|
||||
installer/pkg.gp4: installer/eboot.bin installer/assets/daemon.elf installer/assets/config.json installer/sce_sys/about/right.sprx installer/sce_sys/param.sfo installer/sce_sys/icon0.png installer/sce_sys/pic1.png $(LIBMODULES) $(ASSETS)
|
||||
cd installer && $(TOOLCHAIN)/bin/$(CDIR)/create-gp4 -out pkg.gp4 --content-id=$(CONTENT_ID) --files "eboot.bin assets/daemon.elf assets/config.json sce_sys/about/right.sprx sce_sys/param.sfo sce_sys/icon0.png sce_sys/pic1.png sce_module/libSceFios2.prx sce_module/libc.prx $(patsubst installer/%,%,$(ASSETS))"
|
||||
|
||||
installer/sce_sys/param.sfo: installer/Makefile
|
||||
$(TOOLCHAIN)/bin/$(CDIR)/PkgTool.Core sfo_new $@
|
||||
@@ -68,13 +68,11 @@ $(INTDIR)/%.o: $(PROJDIR)/%.c
|
||||
$(INTDIR)/jsonlite.o: orbisrpc/jsonlite.c
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
# Stage the daemon + evict payloads + config into the app image.
|
||||
# Stage the daemon payload + config into the app image.
|
||||
# config.json carries the SET_ME placeholder; a pre-seeded valid token
|
||||
# (kept out of the repo) makes the installer skip token entry.
|
||||
installer/assets/daemon.elf: build-sdk/orbisrpc_sdk.elf
|
||||
cp $< $@
|
||||
installer/assets/evict.elf: build-sdk/evict.elf
|
||||
cp $< $@
|
||||
installer/assets/config.json: installer/config.json
|
||||
cp $< $@
|
||||
|
||||
@@ -82,4 +80,4 @@ $(INTDIR)/%.o: $(PROJDIR)/%.cpp
|
||||
$(CCX) $(CXXFLAGS) -o $@ $<
|
||||
|
||||
clean:
|
||||
rm -rf $(INTDIR) installer/eboot.bin installer/pkg.gp4 $(CONTENT_ID).pkg installer/sce_sys/param.sfo installer/assets/daemon.elf installer/assets/evict.elf installer/assets/config.json
|
||||
rm -rf $(INTDIR) installer/eboot.bin installer/pkg.gp4 $(CONTENT_ID).pkg installer/sce_sys/param.sfo installer/assets/daemon.elf installer/assets/config.json
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define ICFG_MAX (64u*1024u)
|
||||
@@ -60,6 +61,8 @@ static int icfg_write(const char *path, jl_val_t *r){
|
||||
if(!s) return -1;
|
||||
f = fopen(tmp, "wb");
|
||||
if(!f){ free(s); return -1; }
|
||||
/* token lives in this file: owner-only before bytes hit disk */
|
||||
{ int fd0 = fileno(f); if(fd0 >= 0) fchmod(fd0, 0600); }
|
||||
if(fputs(s, f) < 0) ok = 0;
|
||||
if(ok){
|
||||
int fd = fileno(f);
|
||||
|
||||
+43
-21
@@ -20,11 +20,10 @@
|
||||
#define SETUP_VERSION "1.0.0"
|
||||
#endif
|
||||
#define DAEMON_ELF "/app0/assets/daemon.elf"
|
||||
#define EVICT_ELF "/app0/assets/evict.elf"
|
||||
#define GEN_PATH "/data/orbisRPC/daemon.gen"
|
||||
#define INST_DIR "/data/orbisRPC"
|
||||
#define INST_LOG "/data/orbisRPC/install.log"
|
||||
#define PAYLOAD_BIN "/data/payloads/orbisrpc.bin"
|
||||
#define EVICT_BIN "/data/payloads/evict.elf"
|
||||
|
||||
/* Stage log: every copy step records errno + sizes to a file we can read
|
||||
* back over FTP. The dialog alone can't say WHICH stage failed. */
|
||||
@@ -217,27 +216,17 @@ static int mkdirs(const char *path){
|
||||
static int step_files(void){
|
||||
char report[512];
|
||||
int ok = -1;
|
||||
int evict_ok = -1;
|
||||
mkdir(INST_DIR, 0777);
|
||||
mkdirs("/data/payloads");
|
||||
/* Evict any running orbisRPC daemon before copying new payload.
|
||||
* kill() is blocked by the sandbox (EPERM), so use
|
||||
* sceKernelLoadStartModule to launch evict.elf as a module.
|
||||
* evict.elf reads daemon.lock, kills the daemon, exits. */
|
||||
{
|
||||
evict_ok = copy_file(EVICT_ELF, EVICT_BIN);
|
||||
ilog("evict-copy", evict_ok, 0, 0);
|
||||
if(evict_ok == 0){
|
||||
uint32_t rv = sceKernelLoadStartModule(EVICT_BIN, 0, NULL, 0, NULL, NULL);
|
||||
ilog("evict-launch", rv, 0, 0);
|
||||
sceKernelUsleep(500000);
|
||||
}
|
||||
}
|
||||
ui_progress_open("Installing orbisRPC");
|
||||
/* Copy daemon payload. */
|
||||
ui_progress_msg("Copying payload");
|
||||
ok = copy_file(DAEMON_ELF, PAYLOAD_BIN);
|
||||
ilog("daemon-copy", ok, 0, 0);
|
||||
ui_progress_set(60);
|
||||
/* Pre-save config from PKG asset so step_token() skips on fresh install.
|
||||
* Copy config.json from /app0/assets/config.json to /data/orbisRPC/config.json. */
|
||||
ui_progress_msg("Saving config");
|
||||
{
|
||||
FILE *src = fopen("/app0/assets/config.json", "rb");
|
||||
if(src){
|
||||
@@ -251,16 +240,16 @@ static int step_files(void){
|
||||
if(f){
|
||||
fputs("{\"schema_version\":1,\"token\":\"SET_ME\",\"presence_state\":\"On PS4\"}", f);
|
||||
fclose(f);
|
||||
chmod(ICFG_PATH, 0600);
|
||||
}
|
||||
}
|
||||
}
|
||||
snprintf(report, sizeof report,
|
||||
"%s : %s%s%s%s\n"
|
||||
"%s : %s%s%s%s",
|
||||
PAYLOAD_BIN, ok == 0 ? "OK" : "denied",
|
||||
ok == 0 ? "" : " [stage ", ok == 0 ? "" : g_stage, ok == 0 ? "" : "]",
|
||||
EVICT_BIN, evict_ok == 0 ? "OK" : "denied",
|
||||
evict_ok == 0 ? "" : " [stage ", evict_ok == 0 ? "" : g_stage, evict_ok == 0 ? "" : "]");
|
||||
ok == 0 ? "" : " [stage ", ok == 0 ? "" : g_stage, ok == 0 ? "" : "]");
|
||||
ui_progress_set(80);
|
||||
ui_progress_close();
|
||||
ui_ok(report);
|
||||
if(ok != 0){
|
||||
ui_ok("Install failed: could not write the payload.\n\nStopping here.");
|
||||
@@ -279,6 +268,7 @@ static int step_files(void){
|
||||
if(f){
|
||||
fputs("{\"schema_version\":1,\"token\":\"SET_ME\",\"presence_state\":\"On PS4\"}", f);
|
||||
fclose(f);
|
||||
chmod(ICFG_PATH, 0600);
|
||||
}
|
||||
} else {
|
||||
fclose(f);
|
||||
@@ -286,6 +276,26 @@ static int step_files(void){
|
||||
}
|
||||
}
|
||||
ilogv("cfg-done", 0, 0);
|
||||
/* Generation bump: any older running daemon sees the new generation
|
||||
* and exits cleanly so the fresh payload takes over (no evict). */
|
||||
{
|
||||
long cur = 0;
|
||||
FILE *gf = fopen(GEN_PATH, "rb");
|
||||
if(gf){
|
||||
char gb[32];
|
||||
size_t n = fread(gb, 1, sizeof gb - 1, gf);
|
||||
fclose(gf);
|
||||
if(n > 0){ gb[n] = 0; cur = atol(gb); }
|
||||
}
|
||||
gf = fopen(GEN_PATH, "wb");
|
||||
if(gf){
|
||||
fprintf(gf, "%ld\n", cur + 1);
|
||||
fclose(gf);
|
||||
ilog("gen-bump", (int)(cur + 1), 0, 0);
|
||||
} else {
|
||||
ilog("gen-bump", errno ? errno : -1, 0, 0);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -303,7 +313,19 @@ static void step_token(void){
|
||||
}
|
||||
for(tries = 0; tries < 3; tries++){
|
||||
r = ui_input("Discord token", "paste token, Done to save", tok, sizeof tok);
|
||||
if(r < 0){ ui_ok("Text input failed. Skipping."); return; }
|
||||
if(r < 0){
|
||||
/* IME failure is transient (system dialog busy, OOM): retry
|
||||
* instead of surrendering, and say so. Last try keeps the
|
||||
* FTP fallback message. */
|
||||
ilog("token-imefail", tries, 0, 0);
|
||||
if(tries < 2){
|
||||
ui_ok("Text input glitched. Try again.");
|
||||
continue;
|
||||
}
|
||||
ui_ok("Text input failed. You can paste the token into "
|
||||
"/data/orbisRPC/config.json over FTP instead.");
|
||||
return;
|
||||
}
|
||||
if(r == 0) return;
|
||||
if(token_valid(tok)){
|
||||
r = icfg_token_save(ICFG_PATH, tok);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
cfg_t g_cfg;
|
||||
@@ -191,6 +192,8 @@ int cfg_save(const char *path, const cfg_t *c) {
|
||||
FILE *f = fopen(tmp, "wb");
|
||||
int ok = 0;
|
||||
if (f) {
|
||||
/* token lives in this file: owner-only before bytes hit disk */
|
||||
{ int fd0 = fileno(f); if(fd0 >= 0) fchmod(fd0, 0600); }
|
||||
ok = (fputs(s, f) >= 0);
|
||||
if(fflush(f) != 0) ok = 0;
|
||||
/* force bytes to disk before rename */
|
||||
|
||||
+69
-4
@@ -93,6 +93,47 @@ static void sess_save(const char *tid, const char *name, int64_t started){ jl
|
||||
free(s);
|
||||
}
|
||||
|
||||
/* status.json heartbeat: machine-readable liveness for users and the
|
||||
* installer (FTP-readable proof the daemon is alive, no klog needed).
|
||||
* Written on state changes + every alive tick. Never fatal. */
|
||||
static void status_write(const char *state, const char *title){
|
||||
jl_val_t *r = jl_new_object();
|
||||
if(!r) return;
|
||||
jl_obj_set(r, "version", jl_new_string(ORBISRPC_VERSION));
|
||||
jl_obj_set(r, "state", jl_new_string(state ? state : "?"));
|
||||
jl_obj_set(r, "title", jl_new_string(title ? title : ""));
|
||||
jl_obj_set(r, "ts", jl_new_number((double)time(NULL)));
|
||||
char *s = jl_stringify(r);
|
||||
jl_free(r);
|
||||
if(!s) return;
|
||||
FILE *f = fopen("/data/orbisRPC/status.json.new", "wb");
|
||||
if(f){
|
||||
int ok = (fputs(s, f) >= 0) && (fflush(f) == 0);
|
||||
if(ok){ int fd = fileno(f); if(fd < 0 || fsync(fd) != 0) ok = 0; }
|
||||
if(fclose(f) != 0) ok = 0;
|
||||
if(ok) rename("/data/orbisRPC/status.json.new",
|
||||
"/data/orbisRPC/status.json");
|
||||
else remove("/data/orbisRPC/status.json.new");
|
||||
}
|
||||
free(s);
|
||||
}
|
||||
|
||||
/* Generation protocol (replaces evict.elf): the installer bumps
|
||||
* daemon.gen on every install; an older running daemon that sees a newer
|
||||
* generation exits cleanly so the fresh payload takes over. No signals,
|
||||
* no module loading, no sandbox fights. Missing file = generation 0. */
|
||||
static long gen_read(void){
|
||||
FILE *f = fopen("/data/orbisRPC/daemon.gen", "rb");
|
||||
if(!f) return 0;
|
||||
char b[32];
|
||||
size_t n = fread(b, 1, sizeof b - 1, f);
|
||||
fclose(f);
|
||||
if(n == 0) return 0;
|
||||
b[n] = 0;
|
||||
long v = atol(b);
|
||||
return v < 0 ? 0 : v;
|
||||
}
|
||||
|
||||
/* Playtime ledger: append-only "<title_id> <name> <seconds>" per finished
|
||||
* session. Totals are computed by readers (app/docs); the daemon only
|
||||
* appends, so a corrupt ledger can never break the runtime. */
|
||||
@@ -210,11 +251,19 @@ int daemon_run(const char *fixed_game_name){
|
||||
time_sync_all();
|
||||
if(!have_token(&g_cfg)){
|
||||
/* Waiting for configuration is a HEALTHY boot, not a crash:
|
||||
* mark clean so token-less boots never trip safe mode. */
|
||||
* mark clean so token-less boots never trip safe mode, then
|
||||
* wait for a token to appear (FTP edit, no reboot, no exit). */
|
||||
health_mark_healthy();
|
||||
log_msg("FATAL: put your Discord user token in %s as \"token\":\"...\"", CFG_PATH);
|
||||
log_close();
|
||||
return 1;
|
||||
log_msg("no token in %s; waiting (edit \"token\" over FTP)", CFG_PATH);
|
||||
status_write("waiting_token", "");
|
||||
for(;;){
|
||||
if(s_stop){ log_close(); return 0; }
|
||||
if(sleep_stop(15)) { log_close(); return 0; }
|
||||
cfg_t next = g_cfg;
|
||||
if(cfg_load(CFG_PATH, &next) == 0) g_cfg = next;
|
||||
if(have_token(&g_cfg)) break;
|
||||
}
|
||||
log_msg("token appeared; continuing boot");
|
||||
}
|
||||
|
||||
/* Self-update once per boot, before first connect. Never fatal:
|
||||
@@ -229,6 +278,8 @@ int daemon_run(const char *fixed_game_name){
|
||||
}
|
||||
|
||||
discord_t dc;
|
||||
memset(&dc, 0, sizeof dc); /* ws_close guards on connected; zero = safe */
|
||||
long boot_gen = gen_read();
|
||||
int base_poll = g_cfg.poll_interval_s;
|
||||
if(base_poll < 5) base_poll = 5;
|
||||
if(base_poll > 60) base_poll = 60;
|
||||
@@ -244,6 +295,18 @@ int daemon_run(const char *fixed_game_name){
|
||||
int64_t last_health = 0;
|
||||
for(;;){ /* outer: connect cycles with backoff on failure */
|
||||
if(s_stop) break;
|
||||
/* Superseded by a newer install: exit cleanly (presence cleared
|
||||
* below when connected) so the fresh payload takes over. */
|
||||
{
|
||||
long cur = gen_read();
|
||||
if(cur > boot_gen){
|
||||
log_msg("superseded by generation %ld; exiting cleanly", cur);
|
||||
status_write("superseded", "");
|
||||
if(dc.connected) discord_clear_presence(&dc);
|
||||
ws_close(&dc.ws);
|
||||
break;
|
||||
}
|
||||
}
|
||||
/* re-read config every cycle so token edits land without a reboot.
|
||||
* On parse failure keep last-good config instead of stale defaults. */
|
||||
{
|
||||
@@ -374,6 +437,8 @@ int daemon_run(const char *fixed_game_name){
|
||||
if(now - last_alive >= 60){
|
||||
last_alive = now;
|
||||
log_msg("alive: %s", active ? last : "idle");
|
||||
status_write(active ? "playing" : "idle",
|
||||
active ? last : "");
|
||||
}
|
||||
/* State reconciliation: re-post current presence every
|
||||
* 15 min so a silently desynced tile (dropped update,
|
||||
|
||||
+17
-2
@@ -19,6 +19,7 @@
|
||||
* foreground) is reliable via ShellCoreUtil.
|
||||
*/
|
||||
#include "detect.h"
|
||||
#include "fw.h"
|
||||
#include "cfg.h"
|
||||
#include "log.h"
|
||||
#include "sfo.h"
|
||||
@@ -225,7 +226,7 @@ int detect_eboot_count(void){
|
||||
while(off + 4 <= sz){
|
||||
int recsz = *(int *)(buf + off);
|
||||
if(recsz <= 0 || off + (size_t)recsz > sz) break;
|
||||
if(recsz >= 479 && !memcmp(buf + off + 447, "eboot.bin", 10))
|
||||
if(fw_match_eboot(buf + off, recsz))
|
||||
n++;
|
||||
off += (size_t)recsz;
|
||||
}
|
||||
@@ -242,7 +243,7 @@ static int proc_has_eboot(void){
|
||||
while(off + 4 <= sz){
|
||||
int recsz = *(int *)(buf + off);
|
||||
if(recsz <= 0 || off + (size_t)recsz > sz) return -1;
|
||||
if(recsz >= 479 && !memcmp(buf + off + 447, "eboot.bin", 10))
|
||||
if(fw_match_eboot(buf + off, recsz))
|
||||
return 1;
|
||||
off += (size_t)recsz;
|
||||
}
|
||||
@@ -510,6 +511,20 @@ int detect_current_game(char *out_name, size_t cap, char *out_path, size_t p_cap
|
||||
* we had", never a transition. */
|
||||
int fg = detect_foreground_active();
|
||||
if(fg < 0) return -2;
|
||||
/* Event-driven focus (kern.msgbuf AppFocusChanged): authoritative
|
||||
* when readable — settles multi-app ambiguity and surfaces system
|
||||
* screens. Log-only for now; snapshot probes still gate transitions
|
||||
* until msgbuf readability is confirmed per firmware. */
|
||||
{
|
||||
char scr_tid[16] = "";
|
||||
int scr = detect_system_screen(scr_tid, sizeof scr_tid);
|
||||
static char last_scr[16] = "";
|
||||
if(scr != FOCUS_UNKNOWN && strcmp(scr_tid, last_scr) != 0){
|
||||
strncpy(last_scr, scr_tid, sizeof last_scr - 1);
|
||||
log_msg("focus: %s (%s)", scr_tid,
|
||||
scr == FOCUS_GAME ? "game" : "system");
|
||||
}
|
||||
}
|
||||
if(!fg) return -1;
|
||||
}
|
||||
char titleId[16]=""; int named=0, have_tid=0;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#ifndef DETECT_H
|
||||
#define DETECT_H
|
||||
#include <stddef.h>
|
||||
#include "focus.h"
|
||||
/* Returns 0 and writes a display name when a foreground game is found.
|
||||
* Returns -1 when no game is active or the arguments are invalid.
|
||||
* out_path is optional and receives the per-title cache path when provided. */
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
/* focus.c - kern.msgbuf AppFocusChanged focus tracking.
|
||||
* Probe-first: tries each candidate msgbuf path, remembers the one that
|
||||
* works (sticky), parses only the LAST event (current focus), classifies by
|
||||
* title-id prefix. Any failure (no device, no events, short read) returns
|
||||
* FOCUS_UNKNOWN so callers keep previous state — never a transition. */
|
||||
#include "focus.h"
|
||||
#include "log.h"
|
||||
#include <string.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
|
||||
static const char kEv[] = "AppFocusChanged [";
|
||||
|
||||
static const void *mem_find(const void *h, size_t hl,
|
||||
const void *n, size_t nl){
|
||||
if(!h || !n || nl == 0 || hl < nl) return NULL;
|
||||
const unsigned char *p = h;
|
||||
for(size_t i = 0; i + nl <= hl; i++){
|
||||
if(!memcmp(p + i, n, nl)) return p + i;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int focus_parse_appfocus(const char *buf, size_t len,
|
||||
char *out_tid, size_t cap){
|
||||
if(!buf || !out_tid || cap == 0) return -1;
|
||||
out_tid[0] = 0;
|
||||
/* Last event wins: earlier entries are stale focus history. */
|
||||
const char *last = NULL;
|
||||
const char *p = buf;
|
||||
size_t rem = len;
|
||||
while(rem >= sizeof kEv - 1){
|
||||
const char *f = mem_find(p, rem, kEv, sizeof kEv - 1);
|
||||
if(!f) break;
|
||||
last = f;
|
||||
size_t adv = (size_t)(f - p) + 1;
|
||||
p = f + 1;
|
||||
rem -= adv;
|
||||
}
|
||||
if(!last) return -1;
|
||||
/* Collect bracketed tokens after the marker; the focus target is the
|
||||
* last one ("AppFocusChanged [FROM] -> [TO]"). The first token's
|
||||
* opening bracket is already consumed by the marker itself. */
|
||||
const char *q = last + sizeof kEv - 1;
|
||||
const char *end = buf + len;
|
||||
char best[16] = "";
|
||||
const char *qs = q;
|
||||
while(q < end && *q != ']' && *q != '[' &&
|
||||
(size_t)(q - qs) < sizeof best - 1) q++;
|
||||
if(q < end && *q == ']' && q > qs){
|
||||
size_t n = (size_t)(q - qs);
|
||||
memcpy(best, qs, n);
|
||||
best[n] = 0;
|
||||
q++;
|
||||
}
|
||||
while(q < end){
|
||||
if(*q != '['){ q++; continue; }
|
||||
q++;
|
||||
qs = q;
|
||||
while(q < end && *q != ']' && (size_t)(q - qs) < sizeof best - 1) q++;
|
||||
if(q < end && *q == ']' && q > qs){
|
||||
size_t n = (size_t)(q - qs);
|
||||
if(n < sizeof best){ memcpy(best, qs, n); best[n] = 0; }
|
||||
}
|
||||
if(q < end) q++;
|
||||
}
|
||||
if(!best[0]) return -1;
|
||||
strncpy(out_tid, best, cap - 1);
|
||||
out_tid[cap - 1] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int focus_classify(const char *tid){
|
||||
if(!tid || !tid[0]) return FOCUS_UNKNOWN;
|
||||
if(!strncmp(tid, "NPXS", 4)) return FOCUS_SYSTEM;
|
||||
if(!strncmp(tid, "CUSA", 4) || !strncmp(tid, "PPSA", 4) ||
|
||||
!strncmp(tid, "PCSE", 4) || !strncmp(tid, "PCSB", 4) ||
|
||||
!strncmp(tid, "PCSG", 4) || !strncmp(tid, "EPSA", 4))
|
||||
return FOCUS_GAME;
|
||||
return FOCUS_UNKNOWN;
|
||||
}
|
||||
|
||||
/* Candidate kernel message-buffer paths, in probe order. The exact device
|
||||
* name varies, so we try and remember — never assume. */
|
||||
static const char *kPaths[] = {
|
||||
"/dev/kern.msgbuf",
|
||||
"/dev/msgbuf",
|
||||
"kern.msgbuf",
|
||||
NULL,
|
||||
};
|
||||
|
||||
int detect_system_screen(char *out_tid, size_t cap){
|
||||
if(out_tid && cap) out_tid[0] = 0;
|
||||
static int known = -1; /* sticky index into kPaths */
|
||||
static int logged_no_dev = 0;
|
||||
static unsigned char buf[256 * 1024];
|
||||
|
||||
for(int pass = 0; pass < 2 && known != -2; pass++){
|
||||
int start = (known >= 0) ? known : 0;
|
||||
for(int i = start; kPaths[i]; i++){
|
||||
if(known >= 0 && i != known) continue;
|
||||
int fd = open(kPaths[i], O_RDONLY);
|
||||
if(fd < 0){
|
||||
if(known == i) known = -1; /* device vanished, re-probe */
|
||||
continue;
|
||||
}
|
||||
ssize_t n = read(fd, buf, sizeof buf - 1);
|
||||
close(fd);
|
||||
if(n <= 0){
|
||||
if(known == i) known = -1;
|
||||
continue;
|
||||
}
|
||||
known = i;
|
||||
buf[n] = 0;
|
||||
char tid[16] = "";
|
||||
if(focus_parse_appfocus((const char *)buf, (size_t)n,
|
||||
tid, sizeof tid) != 0)
|
||||
return FOCUS_UNKNOWN; /* buffer readable, no events yet */
|
||||
if(out_tid && cap){
|
||||
strncpy(out_tid, tid, cap - 1);
|
||||
out_tid[cap - 1] = 0;
|
||||
}
|
||||
return focus_classify(tid);
|
||||
}
|
||||
if(known >= 0) break;
|
||||
known = -1;
|
||||
if(pass == 0) continue;
|
||||
}
|
||||
if(!logged_no_dev){
|
||||
logged_no_dev = 1;
|
||||
log_msg("msgbuf unreadable from payload; focus via scu/sysctl");
|
||||
}
|
||||
return FOCUS_UNKNOWN;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/* focus.h - kernel message-buffer focus tracking (event-driven).
|
||||
* The kernel logs "AppFocusChanged [...]" on every focus switch, which beats
|
||||
* polling: multi-app ambiguity disappears and system screens (settings,
|
||||
* browser) become visible instead of "no game". Verified approach: a
|
||||
* third-party 18KB payload tracks focus exactly this way off kern.msgbuf.
|
||||
* Everything here fails soft to "unknown" — snapshot probes stay fallback. */
|
||||
#ifndef ORBISRPC_FOCUS_H
|
||||
#define ORBISRPC_FOCUS_H
|
||||
#include <stddef.h>
|
||||
|
||||
#define FOCUS_UNKNOWN 0 /* no information (keep previous state) */
|
||||
#define FOCUS_GAME 1 /* game/app title id (CUSA, PPSA, ...) */
|
||||
#define FOCUS_SYSTEM 2 /* system app (NPXS...) — settings, browser, etc. */
|
||||
|
||||
/* Parse the LAST AppFocusChanged event in a raw msgbuf window.
|
||||
* Returns 0 and writes the focus target title id, -1 when no event found. */
|
||||
int focus_parse_appfocus(const char *buf, size_t len,
|
||||
char *out_tid, size_t cap);
|
||||
/* 1 game, 2 system (NPXS), 0 unknown prefix. */
|
||||
int focus_classify(const char *tid);
|
||||
/* Probe kern.msgbuf candidates, parse last focus event, classify it.
|
||||
* Returns FOCUS_* class, writes tid when parsed. Never crashes, never
|
||||
* blocks: unreadable buffer just means FOCUS_UNKNOWN. */
|
||||
int detect_system_screen(char *out_tid, size_t cap);
|
||||
#endif
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
/* fw.c - firmware detection + per-FW kinfo_proc layout table. See fw.h.
|
||||
* Version source is uname(2) (libc, no libs, no syscalls): PS4 reports the
|
||||
* system version in the release field. The payload SDK libc has no uname,
|
||||
* so SDK builds report unknown and use the bounded scan (still correct,
|
||||
* just never the exact-offset fast path). Only FWs verified live on
|
||||
* hardware go in the table; everything else uses the bounded scan. */
|
||||
#include "fw.h"
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#ifndef ORBISRPC_SDK_PAYLOAD
|
||||
#include <sys/utsname.h>
|
||||
#endif
|
||||
|
||||
void fw_version(char *out, size_t cap){
|
||||
if(!out || cap == 0) return;
|
||||
/* Default before any parsing so every exit path is defined. */
|
||||
snprintf(out, cap, "?.??");
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
/* No uname in the payload SDK libc: unknown FW. Callers fall back
|
||||
* to the bounded scan, which needs no version. */
|
||||
return;
|
||||
#else
|
||||
struct utsname u;
|
||||
if(uname(&u) != 0) return;
|
||||
/* Accept "9.00", "9.0", "13.52", or FreeBSD-style "12.0-RELEASE":
|
||||
* take leading digits.digits and normalize to MM.mm. */
|
||||
int maj = -1, min = -1;
|
||||
if(sscanf(u.release, "%d.%d", &maj, &min) != 2) return;
|
||||
if(maj < 0 || maj > 99 || min < 0 || min > 99) return;
|
||||
snprintf(out, cap, "%d.%02d", maj, min);
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Verified live on hardware. DO NOT extend from theory: an entry here is
|
||||
* a promise that offset 447 holds the process name on that FW. */
|
||||
static const struct { const char *ver; int name_off; int min_rec; } kKnown[] = {
|
||||
{ "9.00", 447, 479 },
|
||||
};
|
||||
|
||||
int fw_kinfo_for(const char *ver, int *name_off, int *min_rec){
|
||||
if(!ver) return -1;
|
||||
for(unsigned i = 0; i < sizeof kKnown / sizeof kKnown[0]; i++){
|
||||
if(!strcmp(ver, kKnown[i].ver)){
|
||||
if(name_off) *name_off = kKnown[i].name_off;
|
||||
if(min_rec) *min_rec = kKnown[i].min_rec;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int fw_kinfo(int *name_off, int *min_rec){
|
||||
char ver[16];
|
||||
fw_version(ver, sizeof ver);
|
||||
return fw_kinfo_for(ver, name_off, min_rec);
|
||||
}
|
||||
|
||||
/* Bounded needle search inside one record. recsz caps the search so a
|
||||
* corrupt/short record can never over-read. */
|
||||
static int rec_find(const unsigned char *rec, int recsz,
|
||||
const char *needle, int *at){
|
||||
int nlen = (int)strlen(needle);
|
||||
if(!rec || recsz <= 0 || nlen <= 0 || nlen > recsz) return 0;
|
||||
for(int i = 0; i + nlen <= recsz; i++){
|
||||
if(!memcmp(rec + i, needle, (size_t)nlen)){
|
||||
if(at) *at = i;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int fw_match_eboot(const unsigned char *rec, int recsz){
|
||||
static const char want[] = "eboot.bin";
|
||||
int off = 0, minrec = 0;
|
||||
if(fw_kinfo(&off, &minrec) == 0){
|
||||
/* Known FW: exact offset, exact cost. */
|
||||
if(recsz >= minrec && off + 10 <= recsz &&
|
||||
!memcmp(rec + off, want, 10))
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
/* Unknown FW: bounded scan for the name anywhere in the record.
|
||||
* Require the trailing NUL so "eboot.binX" can't false-positive. */
|
||||
int at = -1;
|
||||
if(!rec_find(rec, recsz, want, &at)) return 0;
|
||||
if(at + 9 >= recsz || rec[at + 9] != 0) return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int fw_match_payload_pid(const unsigned char *rec, int recsz, int pid){
|
||||
if(pid <= 0) return 0;
|
||||
int off = 0, minrec = 0;
|
||||
if(fw_kinfo(&off, &minrec) == 0){
|
||||
if(recsz < minrec || off + 8 > recsz) return 0;
|
||||
if(*(const int *)(rec + 72) != pid) return 0;
|
||||
return !memcmp(rec + off, "Payload", 8) && rec[off + 8] == 0;
|
||||
}
|
||||
/* Unknown FW: pid field offset is unverified, so only the name part
|
||||
* is probed; pid match is skipped rather than guessed. A missed
|
||||
* reclaim is a minor stall, a wrong kill would be data loss. */
|
||||
int at = -1;
|
||||
if(!rec_find(rec, recsz, "Payload", &at)) return 0;
|
||||
if(at + 7 >= recsz || rec[at + 7] != 0) return 0;
|
||||
(void)pid;
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/* fw.h - firmware detection + per-FW kinfo_proc layout table.
|
||||
* Ported pattern from OSM-Made/PS4-Kernel-SDK (detect FW, resolve per-FW
|
||||
* data at runtime) adapted to usermode: we never touch the kernel, we
|
||||
* only need the sysctl KERN_PROC record layout, which moves between
|
||||
* firmwares. Unknown firmwares fall back to a bounded in-record scan
|
||||
* instead of a hardcoded offset, so a new FW degrades, never crashes. */
|
||||
#ifndef ORBISRPC_FW_H
|
||||
#define ORBISRPC_FW_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/* Firmware version as "MAJOR.MINOR" (e.g. "9.00", "13.52"). Never fails:
|
||||
* unknown/unparseable platforms yield "?.??". */
|
||||
void fw_version(char *out, size_t cap);
|
||||
|
||||
/* Pure table lookup for a given version string (testable).
|
||||
* Returns 0 known, -1 unknown. */
|
||||
int fw_kinfo_for(const char *ver, int *name_off, int *min_rec);
|
||||
|
||||
/* Fill name_off and min_rec with the kinfo_proc offsets for this box.
|
||||
* Returns 0 when the FW is in the verified table, -1 when unknown
|
||||
* (caller must use the bounded scan instead of assuming). */
|
||||
int fw_kinfo(int *name_off, int *min_rec);
|
||||
|
||||
/* Match an "eboot.bin" process name inside one sysctl record.
|
||||
* Tries the table offset first, then a bounded scan of the record.
|
||||
* Returns 1 match, 0 no match. Never reads past rec+recsz. */
|
||||
int fw_match_eboot(const unsigned char *rec, int recsz);
|
||||
|
||||
/* Match a ("Payload", pid) pair for lock-holder liveness.
|
||||
* pid_off is verified per-FW the same way. Returns 1 live peer. */
|
||||
int fw_match_payload_pid(const unsigned char *rec, int recsz, int pid);
|
||||
|
||||
#endif
|
||||
+5
-4
@@ -2,6 +2,7 @@
|
||||
* so use atomic create (O_CREAT|O_EXCL) + process-table liveness
|
||||
* (sysctl, no signals needed in spawned context). */
|
||||
#include "lock.h"
|
||||
#include "fw.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
@@ -24,10 +25,10 @@ static int pid_live(int pid){
|
||||
if(recsz <= 0 || off + (size_t)recsz > sz) break;
|
||||
/* A recycled PID owned by a system daemon must not block us:
|
||||
* only a live payload process counts as a peer. Spawned
|
||||
* payloads (elfldr/GoldHEN) show up as "Payload". */
|
||||
if(recsz >= 479 && *(int *)(buf + off + 72) == pid)
|
||||
return !memcmp(buf + off + 447, "Payload", 8) &&
|
||||
buf[off + 455] == 0;
|
||||
* payloads (elfldr/GoldHEN) show up as "Payload". Offsets are
|
||||
* per-FW (see fw.h); unknown FWs fail closed. */
|
||||
if(fw_match_payload_pid(buf + off, recsz, pid))
|
||||
return 1;
|
||||
off += (size_t)recsz;
|
||||
}
|
||||
return 0;
|
||||
|
||||
+2
-2
@@ -41,13 +41,13 @@ CFLAGS="--target=$TARGET -fPIC -std=gnu11 -Wall -Wno-unused \
|
||||
-Wno-int-conversion -Wno-incompatible-pointer-types \
|
||||
-DMBEDTLS_NO_PLATFORM_ENTROPY \
|
||||
-isystem $SDK/include -Ithird_party/mbedtls/include"
|
||||
LIBS="-lc -lkernel -lSceNet -lSceNetCtl -lSceSysmodule \
|
||||
LIBS="-lc -lkernel -lSceNet -lSceSysmodule \
|
||||
-lSceUserService"
|
||||
LDFLAGS="-m elf_x86_64 -pie --eh-frame-hdr -L$SDK/lib $LIBS $SDK/lib/crt1.o --script $SDK/link.x"
|
||||
export OO_PS4_TOOLCHAIN="$SDK"
|
||||
OUT="$ROOT/build"; mkdir -p "$OUT"
|
||||
echo "=== compiling (CC=$CC LD=$LD SDK=$SDK) ==="
|
||||
for f in log cfg jsonlite b64 sfo tmdb_crypto tmdb updater updater_http updater_util tls ws detect discord daemon compat lock timesync art health manifest main; do
|
||||
for f in log cfg jsonlite b64 sfo tmdb_crypto tmdb updater updater_http updater_util tls ws detect focus fw discord daemon compat lock timesync art health manifest main; do
|
||||
"$CC" $CFLAGS -c -o "$OUT/$f.o" "orbisrpc/$f.c" || fail "compile $f"
|
||||
done
|
||||
echo "=== mbedtls (skip net_sockets/timing: POSIX-only) ==="
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/bin/sh
|
||||
# build_evict.sh - evict.elf via ps4-payload-sdk (daemon-world linkage only).
|
||||
# Usage: PS4_PAYLOAD_SDK=/path ./scripts/build_evict.sh
|
||||
set -e
|
||||
SDK="${PS4_PAYLOAD_SDK:-$HOME/ps4-payload-sdk/ps4-payload-sdk}"
|
||||
CC="$SDK/bin/orbis-clang"
|
||||
export PS4_PAYLOAD_SDK="$SDK"
|
||||
export PATH="$HOME/llvmshim:$PATH"
|
||||
OUT="build-sdk"
|
||||
mkdir -p "$OUT"
|
||||
echo "=== evict (SDK) ==="
|
||||
"$CC" -O2 -Wall -DORBISRPC_SDK_PAYLOAD -o "$OUT/evict.elf" tools/evict.c || { echo "FAIL: evict"; exit 1; }
|
||||
ls -la "$OUT/evict.elf"
|
||||
@@ -16,7 +16,7 @@ mkdir -p "$OUT"
|
||||
CFLAGS="-O2 -Wall -DORBISRPC_SDK_PAYLOAD -Iorbisrpc -Ithird_party/mbedtls/include -Ithird_party/sqlite/sqlite-amalgamation-3510100"
|
||||
SQLITE_DIR="third_party/sqlite/sqlite-amalgamation-3510100"
|
||||
echo "=== daemon sources (SDK) ==="
|
||||
for f in log cfg jsonlite b64 sfo tmdb_crypto tmdb updater updater_http updater_util tls ws detect discord daemon compat lock timesync art health manifest appdb main; do
|
||||
for f in log cfg jsonlite b64 sfo tmdb_crypto tmdb updater updater_http updater_util tls ws detect focus fw discord daemon compat lock timesync art health manifest appdb main; do
|
||||
# clock has no .c (header-only helper lives in compat.c); skip if missing
|
||||
[ -f "orbisrpc/$f.c" ] || continue
|
||||
"$CC" $CFLAGS -c -o "$OUT/$f.o" "orbisrpc/$f.c" || { echo "FAIL: $f"; exit 1; }
|
||||
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/bin/sh
|
||||
# release.sh - one-command release: SDK payload -> staged assets -> Setup PKG.
|
||||
# Usage: ./scripts/release.sh (needs OO_PS4_TOOLCHAIN + PS4_PAYLOAD_SDK)
|
||||
# Output: OrbisRPC-Setup-<version>.pkg at repo root.
|
||||
set -eu
|
||||
cd "$(dirname "$0")/.."
|
||||
VER="$(sed -n 's/^#define ORBISRPC_VERSION "\(.*\)"/\1/p' orbisrpc/version.h)"
|
||||
[ -n "$VER" ] || { echo "FAIL: version.h unreadable"; exit 1; }
|
||||
echo "=== release $VER ==="
|
||||
./scripts/build_sdk.sh || { echo "FAIL: sdk payload"; exit 1; }
|
||||
make -f installer/Makefile || { echo "FAIL: pkg"; exit 1; }
|
||||
PKG="IV0000-ORPC00001_00-ORBISRPCSETUP000.pkg"
|
||||
[ -f "$PKG" ] || { echo "FAIL: $PKG missing"; exit 1; }
|
||||
OUT="OrbisRPC-Setup-$VER.pkg"
|
||||
mv "$PKG" "$OUT"
|
||||
ls -la "$OUT"
|
||||
echo "done: $OUT"
|
||||
+1
-1
@@ -10,7 +10,7 @@ MBEDTLS_DIR := ../third_party/mbedtls/library
|
||||
# Same exclusion set as scripts/build.sh (POSIX-only modules).
|
||||
MBEDTLS_SRCS := $(filter-out $(MBEDTLS_DIR)/net_sockets.c $(MBEDTLS_DIR)/timing.c $(MBEDTLS_DIR)/entropy_poll.c,$(wildcard $(MBEDTLS_DIR)/*.c))
|
||||
|
||||
ORBIS_SRCS := ../orbisrpc/jsonlite.c ../orbisrpc/b64.c ../orbisrpc/sfo.c ../orbisrpc/tmdb_crypto.c ../orbisrpc/updater_util.c ../orbisrpc/art.c ../orbisrpc/log.c ../orbisrpc/health.c ../orbisrpc/manifest.c ../orbisrpc/compat.c ../orbisrpc/cfg.c ../orbisrpc/appdb.c ../orbisrpc/discord.c
|
||||
ORBIS_SRCS := ../orbisrpc/jsonlite.c ../orbisrpc/b64.c ../orbisrpc/sfo.c ../orbisrpc/tmdb_crypto.c ../orbisrpc/updater_util.c ../orbisrpc/art.c ../orbisrpc/log.c ../orbisrpc/health.c ../orbisrpc/manifest.c ../orbisrpc/compat.c ../orbisrpc/cfg.c ../orbisrpc/appdb.c ../orbisrpc/discord.c ../orbisrpc/focus.c ../orbisrpc/fw.c
|
||||
INST_SRCS := ../installer/icfg.c
|
||||
# SQLite amalgamation: -O0 for host iteration speed (payload uses -O2).
|
||||
SQLITE_DIR := ../third_party/sqlite/sqlite-amalgamation-3510100
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
#include "../orbisrpc/appdb.h"
|
||||
#include "../orbisrpc/discord.h"
|
||||
#include "../orbisrpc/detect.h"
|
||||
#include "../orbisrpc/focus.h"
|
||||
#include "../orbisrpc/fw.h"
|
||||
#include "../installer/icfg.h"
|
||||
#include "sqlite3.h"
|
||||
#include <string.h>
|
||||
@@ -618,6 +620,75 @@ static void test_installer_cfg(void) {
|
||||
assert(icfg_get_str("/nonexistent/x.json", "k", st, sizeof st) != 0);
|
||||
}
|
||||
|
||||
static void test_focus(void) {
|
||||
char tid[16];
|
||||
/* last event wins; target is the TO side of -> */
|
||||
const char *b1 = "boot\nAppFocusChanged [CUSA00001] -> [CUSA00740]\n"
|
||||
"noise\nAppFocusChanged [CUSA00740] -> [NPXS20001]\n";
|
||||
assert(focus_parse_appfocus(b1, strlen(b1), tid, sizeof tid) == 0);
|
||||
assert(!strcmp(tid, "NPXS20001"));
|
||||
/* single event without arrow: the bracketed id itself */
|
||||
const char *b2 = "xx AppFocusChanged [PPSA12345] yy";
|
||||
assert(focus_parse_appfocus(b2, strlen(b2), tid, sizeof tid) == 0);
|
||||
assert(!strcmp(tid, "PPSA12345"));
|
||||
/* no event */
|
||||
assert(focus_parse_appfocus("nothing here", 12, tid, sizeof tid) == -1);
|
||||
/* guards */
|
||||
assert(focus_parse_appfocus(NULL, 10, tid, sizeof tid) == -1);
|
||||
assert(focus_parse_appfocus(b2, strlen(b2), NULL, sizeof tid) == -1);
|
||||
assert(focus_parse_appfocus(b2, strlen(b2), tid, 0) == -1);
|
||||
/* classify */
|
||||
assert(focus_classify("CUSA00740") == FOCUS_GAME);
|
||||
assert(focus_classify("PPSA12345") == FOCUS_GAME);
|
||||
assert(focus_classify("NPXS20001") == FOCUS_SYSTEM);
|
||||
assert(focus_classify("XXXX00000") == FOCUS_UNKNOWN);
|
||||
assert(focus_classify(NULL) == FOCUS_UNKNOWN);
|
||||
}
|
||||
|
||||
static void test_fw(void) {
|
||||
int off = -1, minrec = -1;
|
||||
/* verified table entry */
|
||||
assert(fw_kinfo_for("9.00", &off, &minrec) == 0);
|
||||
assert(off == 447 && minrec == 479);
|
||||
/* unknown FW: no promise */
|
||||
assert(fw_kinfo_for("13.52", &off, &minrec) == -1);
|
||||
assert(fw_kinfo_for(NULL, &off, &minrec) == -1);
|
||||
assert(fw_kinfo_for("bogus", &off, &minrec) == -1);
|
||||
/* version string always well-formed */
|
||||
char ver[16];
|
||||
fw_version(ver, sizeof ver);
|
||||
assert(ver[0] != 0);
|
||||
/* exact-offset match on a synthetic 9.00-style record */
|
||||
unsigned char rec[512];
|
||||
memset(rec, 0, sizeof rec);
|
||||
memcpy(rec + 447, "eboot.bin", 10);
|
||||
(void)ver;
|
||||
/* NOTE: fw_match_* use the live platform table; on non-9.00 hosts
|
||||
* they take the bounded-scan path, which must also match here. */
|
||||
assert(fw_match_eboot(rec, sizeof rec) == 1);
|
||||
/* no name, no match */
|
||||
unsigned char blank[512];
|
||||
memset(blank, 0, sizeof blank);
|
||||
assert(fw_match_eboot(blank, sizeof blank) == 0);
|
||||
/* guards: NULL, empty, truncated */
|
||||
assert(fw_match_eboot(NULL, 512) == 0);
|
||||
assert(fw_match_eboot(rec, 0) == 0);
|
||||
assert(fw_match_eboot(rec, 5) == 0);
|
||||
/* unterminated needle: "eboot.binX" must not match */
|
||||
unsigned char evil[64];
|
||||
memset(evil, 0, sizeof evil);
|
||||
memcpy(evil + 10, "eboot.binX", 10);
|
||||
assert(fw_match_eboot(evil, sizeof evil) == 0);
|
||||
/* payload pid match: name present (pid path is FW-gated) */
|
||||
unsigned char pl[512];
|
||||
memset(pl, 0, sizeof pl);
|
||||
memcpy(pl + 100, "Payload", 8);
|
||||
assert(fw_match_payload_pid(pl, sizeof pl, 1234) == 1);
|
||||
assert(fw_match_payload_pid(pl, sizeof pl, 0) == 0);
|
||||
assert(fw_match_payload_pid(blank, sizeof blank, 1234) == 0);
|
||||
assert(fw_match_payload_pid(NULL, 512, 1234) == 0);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_json();
|
||||
test_gateway_op_spoof();
|
||||
@@ -637,6 +708,8 @@ int main(void) {
|
||||
test_installer_cfg();
|
||||
test_appdb();
|
||||
test_discord_builder();
|
||||
test_focus();
|
||||
test_fw();
|
||||
puts("utility tests passed");
|
||||
return 0;
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
/* evict.c - stop a running orbisRPC daemon so a fresh payload can take over.
|
||||
* Reads /data/orbisRPC/daemon.lock, verifies the holder is a live "Payload"
|
||||
* process via the same sysctl walk as lock.c, then SIGTERM (clean stop:
|
||||
* banks session, clears presence, releases lock) with a SIGKILL fallback.
|
||||
* Refuses to signal anything that is not a live Payload peer.
|
||||
* Build: ./scripts/build_evict.sh -> build-sdk/evict.elf
|
||||
* Run: send via elfldr:9021, then send the fresh orbisrpc_sdk.elf.
|
||||
* Result: /data/orbisRPC/evict.txt + klog printf. */
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/sysctl.h>
|
||||
|
||||
#define LOCK_PATH "/data/orbisRPC/daemon.lock"
|
||||
#define RESULT_PATH "/data/orbisRPC/evict.txt"
|
||||
|
||||
static void report(const char *msg){
|
||||
printf("evict: %s\n", msg);
|
||||
int fd = open(RESULT_PATH, O_CREAT|O_TRUNC|O_WRONLY, 0644);
|
||||
if(fd >= 0){ (void)write(fd, msg, strlen(msg)); (void)write(fd, "\n", 1); close(fd); }
|
||||
}
|
||||
|
||||
/* 1 = pid is a live "Payload" process, 0 otherwise. Mirrors lock.c. */
|
||||
static int payload_live(int pid){
|
||||
if(pid <= 0 || pid == (int)getpid()) return 0;
|
||||
int mib[4] = { 1, 14, 8, 0 };
|
||||
size_t sz = 0;
|
||||
if(sysctl(mib, 4, NULL, &sz, NULL, 0) != 0) return 0;
|
||||
static unsigned char buf[256*1024];
|
||||
if(sz > sizeof buf) return 0; /* unreadable table: fail closed */
|
||||
if(sysctl(mib, 4, buf, &sz, NULL, 0) != 0) return 0;
|
||||
size_t off = 0;
|
||||
while(off + 4 <= sz){
|
||||
int recsz = *(int *)(buf + off);
|
||||
if(recsz <= 0 || off + (size_t)recsz > sz) break;
|
||||
if(recsz >= 479 && *(int *)(buf + off + 72) == pid)
|
||||
/* Exact "Payload" + NUL: a prefix match would bless
|
||||
* PayloadHelper-style names for the kill list. */
|
||||
return !memcmp(buf + off + 447, "Payload", 8) &&
|
||||
buf[off + 455] == 0;
|
||||
off += (size_t)recsz;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(void){
|
||||
char msg[128];
|
||||
int fd = open(LOCK_PATH, O_RDONLY);
|
||||
if(fd < 0){ report("NO_LOCK nothing running"); return 0; }
|
||||
char b[32]; ssize_t n = read(fd, b, sizeof b - 1); close(fd);
|
||||
if(n <= 0){ report("LOCK_UNREADABLE"); return 1; }
|
||||
b[n] = 0;
|
||||
int pid = 0;
|
||||
if(sscanf(b, "%d", &pid) != 1 || pid <= 0){
|
||||
remove(LOCK_PATH);
|
||||
report("LOCK_GARBAGE removed");
|
||||
return 0;
|
||||
}
|
||||
if(!payload_live(pid)){
|
||||
remove(LOCK_PATH);
|
||||
snprintf(msg, sizeof msg, "STALE holder=%d not a live Payload; lock removed", pid);
|
||||
report(msg);
|
||||
return 0;
|
||||
}
|
||||
if(kill(pid, SIGTERM) != 0 && errno == ESRCH){
|
||||
remove(LOCK_PATH);
|
||||
report("HOLDER_GONE lock removed");
|
||||
return 0;
|
||||
}
|
||||
/* Grace period: clean stop banks session + releases lock. */
|
||||
for(int i = 0; i < 24; i++){
|
||||
sleep(1);
|
||||
if(!payload_live(pid)){
|
||||
remove(LOCK_PATH);
|
||||
snprintf(msg, sizeof msg, "EVICTED holder=%d clean stop", pid);
|
||||
report(msg);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
/* Wedged (e.g. stuck in blocking connect): SIGKILL fallback. */
|
||||
(void)kill(pid, SIGKILL);
|
||||
for(int i = 0; i < 10; i++){
|
||||
sleep(1);
|
||||
if(!payload_live(pid)){
|
||||
remove(LOCK_PATH);
|
||||
snprintf(msg, sizeof msg, "EVICTED holder=%d SIGKILL fallback", pid);
|
||||
report(msg);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
snprintf(msg, sizeof msg, "STUCK holder=%d still alive; reboot console", pid);
|
||||
report(msg);
|
||||
return 2;
|
||||
}
|
||||
Reference in new issue
Block a user