Launcher: leave the app sandbox so it can reach Control4Free

On the console the app's connections to 127.0.0.1 and to the console's own
address both failed with EACCES: the app sandbox does not let an app reach
the console itself. So the app reached neither Control4Free nor PayLoader,
while a phone or PC on the network could.

The app now takes itself out of the sandbox at startup with GoldHEN's SDK
call, the same one the auto-start setup used, and stays out. The bundled
payload is read into memory first, since /app0 is only visible from inside,
and starts send it from there. When nothing connects at all, the screen
says the app cannot reach Control4Free instead of blaming another copy.

OpenOrbis's headers give SIGSYS Linux's value (31); the kernel's is 12.
This commit is contained in:
MoHadiShibli committed 2026-10-04 21:23:14 +03:00
1 parent 0fe839ad2e
commit 5e38f1db2d
8 files changed
+114 -74

No files matched your search

+1 -1
View File
@@ -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
+5 -51
View File
@@ -1,5 +1,6 @@
/* GoldHEN AutoRun for the bundled payload. */
#include "autorun.h"
#include "sandbox.h"
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
@@ -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;
}
+3 -1
View File
@@ -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);
+15 -3
View File
@@ -1,4 +1,5 @@
/* Native PS4 launcher. The controller service runs outside this application. */
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
@@ -12,6 +13,7 @@
#include <orbis/UserService.h>
#include <orbis/VideoOut.h>
#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) {
+54
View File
@@ -0,0 +1,54 @@
/* Leaving the app sandbox through GoldHEN. */
#include "sandbox.h"
#include <errno.h>
#include <signal.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
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
+17
View File
@@ -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
+15 -17
View File
@@ -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));
+4 -1
View File
@@ -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
}