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:
SirHumza committed 2026-09-27 14:47:09 +02:00
1 parent 68a34993da
commit d0972b68e9
25 files changed
+587 -181

No files matched your search

-1
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+3
View File
@@ -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
View File
@@ -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);
+3
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+1
View File
@@ -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. */
+135
View File
@@ -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;
}
+25
View File
@@ -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
View File
@@ -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;
}
+34
View File
@@ -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
View File
@@ -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
View File
@@ -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) ==="
-13
View File
@@ -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"
+1 -1
View File
@@ -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; }
+17
View File
@@ -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
View File
@@ -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
+73
View File
@@ -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;
}
-98
View File
@@ -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;
}