diff --git a/launcher/Makefile b/launcher/Makefile index 38aca68..a1ae90a 100644 --- a/launcher/Makefile +++ b/launcher/Makefile @@ -8,7 +8,7 @@ CC := clang-18 CXX := clang++-18 LD := ld.lld-18 CFLAGS := --target=x86_64-pc-freebsd12-elf -fPIC -funwind-tables -O2 -Wall -Wextra -Werror -MMD -MP -D_DEFAULT_SOURCE -isysroot $(SDK) -isystem $(SDK)/include -I../vendor/jsmn -I../vendor/qrcodegen -I../vendor/stb -OBJECTS := $(OUT)/main.o $(OUT)/screen.o $(OUT)/draw.o $(OUT)/service.o $(OUT)/autorun.o $(OUT)/qrcodegen.o $(OUT)/stb_truetype.o $(OUT)/fonts.o +OBJECTS := $(OUT)/main.o $(OUT)/screen.o $(OUT)/draw.o $(OUT)/service.o $(OUT)/autorun.o $(OUT)/sandbox.o $(OUT)/qrcodegen.o $(OUT)/stb_truetype.o $(OUT)/fonts.o LIBS := -lc -lkernel -lSceVideoOut -lScePad -lSceUserService -lSceSystemService -lSceNetCtl FONTS := ../vendor/roboto/Roboto-Light.ttf ../vendor/roboto/Roboto-Regular.ttf .PHONY: all package diff --git a/launcher/autorun.c b/launcher/autorun.c index be43648..f94951b 100644 --- a/launcher/autorun.c +++ b/launcher/autorun.c @@ -1,5 +1,6 @@ /* GoldHEN AutoRun for the bundled payload. */ #include "autorun.h" +#include "sandbox.h" #include #include #include @@ -19,47 +20,7 @@ static size_t c4fBundledSize; void c4fAutorunSetRoot(const char *root) { snprintf(c4fRoot, sizeof(c4fRoot), "%s", root); } -#ifdef __FreeBSD__ -/* GoldHEN's SDK call (syscall 500). Command 2 takes this process out of its - * sandbox, command 3 puts it back; the layout is the GoldHEN Plugins SDK's - * struct jailbreak_backup (MIT). */ -typedef struct { - uint32_t cr_uid, cr_ruid, cr_rgid, cr_groups; - uint64_t cr_paid, cr_caps[2]; - void *cr_prison, *fd_cdir, *fd_jdir, *fd_rdir; -} C4fJailbreak; - -static long c4fGoldHen(uint64_t command, void *data) -{ - long ret = 500; - int failed; - __asm__ volatile("syscall" : "+a"(ret), "=@ccc"(failed) : "D"(command), "S"(data) - : "rcx", "rdx", "r8", "r9", "r10", "r11", "memory"); - return failed ? -ret : ret; -} - -static int c4fEnter(C4fJailbreak *backup) -{ - /* Without GoldHEN the call does not exist: get an error, not SIGSYS. */ - signal(SIGSYS, SIG_IGN); - memset(backup, 0, sizeof(*backup)); - long ret = c4fGoldHen(2, backup); - if (ret != 0) errno = ret < 0 ? (int)-ret : EPERM; - return ret == 0 ? 0 : -1; -} - -static void c4fLeave(C4fJailbreak *backup) { (void)c4fGoldHen(3, backup); } -#else -/* Host tests: no sandbox to leave. */ -typedef struct { int unused; } C4fJailbreak; -static int c4fEnter(C4fJailbreak *backup) -{ - (void)backup; - if (getenv("C4F_TEST_NO_GOLDHEN")) { errno = ENOSYS; return -1; } - return 0; -} -static void c4fLeave(C4fJailbreak *backup) { (void)backup; } -#endif +const unsigned char *c4fAutorunBundled(size_t *size) { *size = c4fBundledSize; return c4fBundled; } static int c4fPath(char *out, size_t size, const char *relative) { @@ -206,12 +167,11 @@ static char *c4fIniEdited(const char *text, int enable) int c4fAutorunCheck(char *problem, size_t size) { - C4fJailbreak jailbreak; char path[300]; size_t length = 0; problem[0] = 0; if (!c4fBundled) { snprintf(problem, size, "the bundled payload could not be read"); return C4F_AUTORUN_UNKNOWN; } - if (c4fEnter(&jailbreak)) { snprintf(problem, size, "GoldHEN did not let the app check (errno %d)", errno); return C4F_AUTORUN_UNKNOWN; } + if (c4fSandboxLeave()) { snprintf(problem, size, "GoldHEN did not let the app check (errno %d)", errno); return C4F_AUTORUN_UNKNOWN; } int state = C4F_AUTORUN_OFF; char *ini = c4fPath(path, sizeof(path), "GoldHEN/payloads.ini") ? NULL : (char *)c4fReadFile(path, &length, 1u << 20); if (ini && c4fIniEnabled(ini) && !c4fPath(path, sizeof(path), "payloads/control4free.elf")) { @@ -222,23 +182,20 @@ int c4fAutorunCheck(char *problem, size_t size) free(installed); } free(ini); - c4fLeave(&jailbreak); return state; } int c4fAutorunEnable(char *message, size_t size) { - C4fJailbreak jailbreak; char directory[300], path[300]; size_t length = 0; if (!c4fBundled) { snprintf(message, size, "The bundled payload could not be read. Reinstall the Control4Free package."); return -1; } - if (c4fEnter(&jailbreak)) { snprintf(message, size, "GoldHEN did not let the app set up auto-start (errno %d).", errno); return -1; } + if (c4fSandboxLeave()) { snprintf(message, size, "GoldHEN did not let the app set up auto-start (errno %d).", errno); return -1; } int failed = c4fPath(directory, sizeof(directory), "payloads") || c4fPath(path, sizeof(path), "payloads/control4free.elf"); if (!failed && mkdir(directory, 0777) && errno != EEXIST) failed = 1; if (!failed && c4fWriteFile(path, c4fBundled, c4fBundledSize)) failed = 1; if (failed) { snprintf(message, size, "Could not copy Control4Free to /data/payloads (errno %d).", errno); - c4fLeave(&jailbreak); return -1; } failed = c4fPath(directory, sizeof(directory), "GoldHEN") || c4fPath(path, sizeof(path), "GoldHEN/payloads.ini"); @@ -251,16 +208,14 @@ int c4fAutorunEnable(char *message, size_t size) else snprintf(message, size, "Auto-start is on. GoldHEN starts Control4Free each time it loads."); free(old); free(edited); - c4fLeave(&jailbreak); return failed ? -1 : 0; } int c4fAutorunDisable(char *message, size_t size) { - C4fJailbreak jailbreak; char path[300]; size_t length = 0; - if (c4fEnter(&jailbreak)) { snprintf(message, size, "GoldHEN did not let the app change auto-start (errno %d).", errno); return -1; } + if (c4fSandboxLeave()) { snprintf(message, size, "GoldHEN did not let the app change auto-start (errno %d).", errno); return -1; } int failed = c4fPath(path, sizeof(path), "GoldHEN/payloads.ini"); char *old = failed ? NULL : (char *)c4fReadFile(path, &length, 1u << 20); char *edited = old ? c4fIniEdited(old, 0) : NULL; @@ -270,6 +225,5 @@ int c4fAutorunDisable(char *message, size_t size) else snprintf(message, size, "Auto-start is off. Start Control4Free here with Cross, or from GoldHEN's Payloader LaunchPad."); free(old); free(edited); - c4fLeave(&jailbreak); return failed ? -1 : 0; } diff --git a/launcher/autorun.h b/launcher/autorun.h index 7074f25..22452dd 100644 --- a/launcher/autorun.h +++ b/launcher/autorun.h @@ -12,8 +12,10 @@ enum { C4F_AUTORUN_UNKNOWN = -1, C4F_AUTORUN_OFF = 0, C4F_AUTORUN_ON = 1, C4F_AU /* Host tests point the data folder elsewhere; the console uses /user/data. */ void c4fAutorunSetRoot(const char *root); -/* Reads the bundled payload into memory; 0 on success. */ +/* Reads the bundled payload into memory; 0 on success. Do it before leaving + * the sandbox: /app0 is only visible from inside. */ int c4fAutorunLoadBundled(const char *path); +const unsigned char *c4fAutorunBundled(size_t *size); int c4fAutorunCheck(char *problem, size_t size); int c4fAutorunEnable(char *message, size_t size); int c4fAutorunDisable(char *message, size_t size); diff --git a/launcher/main.cpp b/launcher/main.cpp index f9b953b..d026e6f 100644 --- a/launcher/main.cpp +++ b/launcher/main.cpp @@ -1,4 +1,5 @@ /* Native PS4 launcher. The controller service runs outside this application. */ +#include #include #include #include @@ -12,6 +13,7 @@ #include #include #include "autorun.h" +#include "sandbox.h" #include "screen.h" #include "service.h" @@ -30,7 +32,9 @@ static int c4fSetUp(char *message, size_t size) int result = c4fAutorunEnable(message, size); if (result) return result; char started[160]; - result = c4fLauncherStart(C4F_BUNDLED_PAYLOAD, started, sizeof(started)); + size_t payloadSize; + const unsigned char *payload = c4fAutorunBundled(&payloadSize); + result = c4fLauncherStart(payload, payloadSize, started, sizeof(started)); if (result == 0) snprintf(message, size, "Set up and running. GoldHEN will also start it after each restart."); else if (result == -2) snprintf(message, size, "%s", started); else snprintf(message, size, "Auto-start is on: Control4Free starts the next time GoldHEN loads. %s", started); @@ -62,7 +66,11 @@ static void *c4fWorker(void *) } else { c4fLauncherSetHost(NULL); } - if (command == C4F_DO_START) result = c4fLauncherStart(C4F_BUNDLED_PAYLOAD, message, sizeof(message)); + if (command == C4F_DO_START) { + size_t payloadSize; + const unsigned char *payload = c4fAutorunBundled(&payloadSize); + result = c4fLauncherStart(payload, payloadSize, message, sizeof(message)); + } if (command == C4F_DO_STOP) result = c4fLauncherStop(message, sizeof(message)); if (command == C4F_DO_SET_UP) result = c4fSetUp(message, sizeof(message)); if (command == C4F_DO_AUTORUN_ON) result = c4fAutorunEnable(message, sizeof(message)); @@ -79,6 +87,7 @@ static void *c4fWorker(void *) (first || running != c4fScreen.running || (running < 0 && strcmp(problem, shownProblem)))) { if (running == 1) snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Ready. Open the address on your phone or PC."); else if (running == 0) snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Not running."); + else if (!c4fLauncherReached()) snprintf(c4fScreen.message, sizeof(c4fScreen.message), "The app cannot reach Control4Free: %s", problem); else snprintf(c4fScreen.message, sizeof(c4fScreen.message), "No answer the app understands: %s", problem); snprintf(shownProblem, sizeof(shownProblem), "%s", problem); } @@ -162,8 +171,11 @@ int main(void) pad = scePadOpen(user, ORBIS_PAD_PORT_TYPE_STANDARD, 0, NULL); c4fScreen.running = -1; c4fScreen.busy = 1; c4fScreen.autorun = C4F_AUTORUN_UNKNOWN; snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Checking Control4Free..."); - /* Read before any sandbox change: /app0 is only visible from inside it. */ + /* Read before leaving the sandbox: /app0 is only visible from inside it. */ if (c4fAutorunLoadBundled(C4F_BUNDLED_PAYLOAD)) printf("[c4f-launcher] bundled payload unreadable\n"); + /* The sandbox refuses connections to the console itself (EACCES), so the + * app could reach neither Control4Free nor PayLoader from inside it. */ + if (c4fSandboxLeave()) printf("[c4f-launcher] could not leave the sandbox, errno %d\n", errno); pthread_t worker; int workerStarted = pthread_create(&worker, NULL, c4fWorker, NULL) == 0; if (!workerStarted || pad < 0) { diff --git a/launcher/sandbox.c b/launcher/sandbox.c new file mode 100644 index 0000000..8d7a714 --- /dev/null +++ b/launcher/sandbox.c @@ -0,0 +1,54 @@ +/* Leaving the app sandbox through GoldHEN. */ +#include "sandbox.h" +#include +#include +#include +#include +#include + +static int c4fOut; + +int c4fSandboxIsOut(void) { return c4fOut; } + +#ifdef __FreeBSD__ +/* GoldHEN's SDK call (syscall 500), command 2: jailbreak this process. The + * kernel stores the old credentials in this layout, the GoldHEN Plugins SDK's + * struct jailbreak_backup (MIT). The app never goes back, so it is kept only + * because GoldHEN writes into it. */ +typedef struct { + uint32_t cr_uid, cr_ruid, cr_rgid, cr_groups; + uint64_t cr_paid, cr_caps[2]; + void *cr_prison, *fd_cdir, *fd_jdir, *fd_rdir; +} C4fJailbreak; + +static long c4fGoldHen(uint64_t command, void *data) +{ + long ret = 500; + int failed; + __asm__ volatile("syscall" : "+a"(ret), "=@ccc"(failed) : "D"(command), "S"(data) + : "rcx", "rdx", "r8", "r9", "r10", "r11", "memory"); + return failed ? -ret : ret; +} + +int c4fSandboxLeave(void) +{ + static C4fJailbreak backup; + if (c4fOut) return 0; + /* Without GoldHEN the call does not exist: get an error, not SIGSYS. The + * kernel's SIGSYS is FreeBSD's 12; OpenOrbis's header has Linux's 31. */ + signal(12, SIG_IGN); + memset(&backup, 0, sizeof(backup)); + long ret = c4fGoldHen(2, &backup); + if (ret != 0) { errno = ret < 0 ? (int)-ret : EPERM; return -1; } + c4fOut = 1; + return 0; +} +#else +/* Host tests: there is no sandbox. */ +int c4fSandboxLeave(void) +{ + if (getenv("C4F_TEST_NO_GOLDHEN")) { errno = ENOSYS; return -1; } + c4fOut = 1; + return 0; +} +#endif diff --git a/launcher/sandbox.h b/launcher/sandbox.h new file mode 100644 index 0000000..6e99b9f --- /dev/null +++ b/launcher/sandbox.h @@ -0,0 +1,17 @@ +#ifndef C4F_LAUNCHER_SANDBOX_H +#define C4F_LAUNCHER_SANDBOX_H +/* The app sandbox refuses connections to the console itself (EACCES on both + * 127.0.0.1 and the console's own address, seen 2026-10-04) and hides /data. + * GoldHEN's SDK call takes the process out of it for good. */ +#ifdef __cplusplus +extern "C" { +#endif + +/* 0 when out of the sandbox (again: no-op); -1 with errno otherwise. */ +int c4fSandboxLeave(void); +int c4fSandboxIsOut(void); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/launcher/service.c b/launcher/service.c index fad48d0..2c3c62e 100644 --- a/launcher/service.c +++ b/launcher/service.c @@ -23,6 +23,7 @@ static char c4fHost[16]; static char c4fProblem[128]; +static int c4fReached; uint64_t c4fLauncherTimeMs(void) { @@ -167,11 +168,14 @@ static int c4fHttp(const char *method, const char *path, char *body, size_t capa continue; } connected++; - if (c4fExchange(fd, targets[i], method, path, body, capacity) == 1) return 1; + if (c4fExchange(fd, targets[i], method, path, body, capacity) == 1) { c4fReached = 1; return 1; } } + c4fReached = connected > 0; return !connected && refused ? 0 : -1; } +int c4fLauncherReached(void) { return c4fReached; } + static int c4fEquals(const char *json, const jsmntok_t *token, const char *value) { return token->end - token->start == (int)strlen(value) && !memcmp(json + token->start, value, strlen(value)); @@ -210,32 +214,25 @@ int c4fLauncherProbe(C4fServiceStatus *status) return 1; } -int c4fLauncherStart(const char *payload, char *message, size_t size) +int c4fLauncherStart(const unsigned char *payload, size_t payloadSize, char *message, size_t size) { C4fServiceStatus status; int probe = c4fLauncherProbe(&status); if (probe == 1) { snprintf(message, size, "Already running. Open the address on your phone or PC."); return 0; } + if (probe != 0 && !c4fReached) { snprintf(message, size, "The app cannot check whether Control4Free is running (%s).", c4fProblem); return -1; } if (probe != 0) { snprintf(message, size, "Another copy may be running (%s). Stop it on its controller page, or restart the PS4.", c4fProblem); return -1; } - FILE *file = fopen(payload, "rb"); - if (!file) { snprintf(message, size, "The payload is missing. Reinstall the Control4Free package."); return -1; } - unsigned char buffer[16384]; - size_t first = fread(buffer, 1, sizeof(buffer), file); - if (first < 64 || memcmp(buffer, "\177ELF", 4) || buffer[4] != 2 || buffer[5] != 1) { - fclose(file); snprintf(message, size, "The bundled payload is damaged. Reinstall the Control4Free package."); return -1; + if (!payload) { snprintf(message, size, "The payload is missing. Reinstall the Control4Free package."); return -1; } + if (payloadSize < 64 || memcmp(payload, "\177ELF", 4) || payload[4] != 2 || payload[5] != 1) { + snprintf(message, size, "The bundled payload is damaged. Reinstall the Control4Free package."); return -1; } const char *targets[2]; int count = c4fTargets(targets), fd = -1; for (int i = 0; i < count && fd < 0; i++) fd = c4fConnect(targets[i], C4F_PAYLOADER_PORT); - if (fd < 0) { fclose(file); snprintf(message, size, "PayLoader did not answer. Turn it on in GoldHEN, then press Cross again."); return -1; } - uint64_t deadline = c4fLauncherTimeMs() + 10000; - int failed = c4fSendAll(fd, buffer, first, deadline); - while (!failed) { - size_t count = fread(buffer, 1, sizeof(buffer), file); - if (!count) { failed = ferror(file); break; } - failed = c4fSendAll(fd, buffer, count, deadline); - } - fclose(file); shutdown(fd, SHUT_WR); close(fd); + if (fd < 0) { snprintf(message, size, "PayLoader did not answer (errno %d). Turn it on in GoldHEN, then press Cross again.", errno); return -1; } + int failed = c4fSendAll(fd, payload, payloadSize, c4fLauncherTimeMs() + 10000); + shutdown(fd, SHUT_WR); close(fd); if (failed) { snprintf(message, size, "The transfer to PayLoader broke off. Restart the PS4 before you try again."); return -2; } + uint64_t deadline; deadline = c4fLauncherTimeMs() + 20000; while (c4fLauncherTimeMs() < deadline) { if (c4fLauncherProbe(&status) == 1 && !status.stopping) { @@ -252,6 +249,7 @@ int c4fLauncherStop(char *message, size_t size) C4fServiceStatus status; int probe = c4fLauncherProbe(&status); if (probe == 0) { snprintf(message, size, "Stopped. Press Cross to start it again."); return 0; } + if (probe != 1 && !c4fReached) { snprintf(message, size, "The app cannot reach Control4Free (%s). Stop it on its controller page.", c4fProblem); return -1; } if (probe != 1) { snprintf(message, size, "This copy does not answer the app (%s). Stop it on its controller page.", c4fProblem); return -1; } char reply[256]; int ret = c4fHttp("POST", "/api/stop", reply, sizeof(reply)); diff --git a/launcher/service.h b/launcher/service.h index 93c04f8..472609a 100644 --- a/launcher/service.h +++ b/launcher/service.h @@ -18,7 +18,10 @@ const char *c4fLauncherProblem(void); * Ambiguous answers must not allow starting another copy in the shared * payload host. */ int c4fLauncherProbe(C4fServiceStatus *status); -int c4fLauncherStart(const char *payload, char *message, size_t size); +/* Whether the last probe connected anywhere, so "no answer" can be told apart + * from "cannot reach". */ +int c4fLauncherReached(void); +int c4fLauncherStart(const unsigned char *payload, size_t payloadSize, char *message, size_t size); int c4fLauncherStop(char *message, size_t size); #ifdef __cplusplus }