Files
Gary Riches e225e02256 PS5 DevTools: crash reports and memory graphs for PS5 homebrew
A resident payload that attaches to homebrew titles as their debugger,
writes a crash report (registers, every thread's backtrace, stack,
modules, code at the fault, memory before the crash) and shows a TV
notification, then hands the signal back. It samples every title's
memory (direct/GPU, heap, other), CPU and threads, and serves a
dashboard on port 9113. The host tool symbolicates reports against the
matching build's ELF, including reports a player sends as JSON.

Tested on firmware 13.60 with Super Mario Sunshine and SwitchWaker.

Claude-Session: https://claude.ai/code/session_016xMYBaR9GfxtETyVf6er1P
2026-10-14 15:50:36 +01:00

1582 lines
56 KiB
C

/*
* PS5 DevTools - resident crash reporter and memory monitor.
* Copyright (C) 2026 Gary Riches
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Load once per console boot. It watches the console's process list, and for
* every homebrew title that starts it:
*
* - samples the title's memory and CPU once a second (an hour is kept), with
* its mappings totalled by kind every few seconds;
* - attaches to it as its debugger, passes every signal straight back, and
* when one is a crash (SEGV, BUS, ILL, FPE, ABRT, SYS, TRAP) writes a report
* before letting the system handle the crash as it always does: the signal,
* the faulting address, every thread's registers, a copy of the faulting
* stack, a backtrace, the loaded modules, the code at the fault and the last
* two minutes of memory samples;
* - shows a notification on the screen when a report is written.
*
* Reports are files under /data/devtools/crashes, so nothing needs a network
* beyond the local one. Port 9113 serves the dashboard (a browser at
* http://<console>:9113/) and the JSON it reads:
*
* GET /api/status payload, console and the titles it has seen
* GET /api/memory?pid=P&since=T samples of one title after time T (unix s)
* GET /api/crashes the reports, newest first
* GET /api/crash/<id> one report
* GET /api/errors the system's own error history (CE-... codes)
* GET /api/log this payload's log
* DELETE /api/crash/<id> remove a report
* POST /api/quit detach from everything and leave
*
* Which titles are attached to: every title with a folder in /data/homebrew,
* unless /data/devtools/watch.txt lists title ids (one a line, or "all", or
* "none"). Every title is sampled either way.
*/
#include <arpa/inet.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <machine/reg.h>
#include <netinet/in.h>
#include <pthread.h>
#include <signal.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ptrace.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/sysctl.h>
#include <sys/types.h>
#include <sys/user.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include <ps5/kernel.h>
#define DEVTOOLS_VERSION "0.1.0"
#define DEVTOOLS_PORT 9113
#define ROOT_DIR "/data/devtools"
#define CRASH_DIR ROOT_DIR "/crashes"
#define LOG_PATH ROOT_DIR "/devtools.log"
#define WATCH_PATH ROOT_DIR "/watch.txt"
#define ERROR_DIR "/system_data/priv/error/history"
/* The credentials ptrace needs from a payload (measured on 13.60; DevLoop's) */
#define DEBUGGER_AUTHID 0x4800000000010003ull
#define MAX_TITLES 8
#define MAX_SAMPLES 3600 /* an hour at one a second */
#define MAX_REPORTS_PER_PID 3
#define STACK_COPY (16 * 1024)
#define MAX_FRAMES 48
#define MAX_MODULES 256
/* The SDK's ptrace is the bare system call: an error comes back as the
* positive error number, and errno is not set. This puts it the usual way. */
static int pt(int request, pid_t pid, caddr_t addr, int data)
{
int r = ptrace(request, pid, addr, data);
if (request != PT_GETNUMLWPS && request != PT_GETLWPLIST && r > 0 && r < 256) {
errno = (int)r;
return -1;
}
return (int)r;
}
/* What sceKernelGetAppInfo fills: the layout ps5vkctl reads */
typedef struct
{
uint32_t app_id;
uint64_t unknown1;
char title_id[14];
char unknown2[0x3c];
} app_info_t;
int sceKernelGetAppInfo(pid_t pid, app_info_t *info);
typedef struct
{
int type, req_id, priority, msg_id, target_id, user_id, unk1, unk2, app_id, error_num, unk3;
char use_icon_image_uri;
char message[1024];
char icon_uri[1024];
char unk[1024];
} notify_request_t;
int sceKernelSendNotificationRequest(int, notify_request_t *, size_t, int);
/* ------------------------------------------------------------------ text */
typedef struct
{
char *p;
size_t n, cap;
} sb_t;
static void sb_grow(sb_t *s, size_t more)
{
if (s->n + more + 1 <= s->cap)
return;
size_t cap = s->cap ? s->cap : 4096;
while (cap < s->n + more + 1)
cap *= 2;
char *p = realloc(s->p, cap);
if (!p)
abort();
s->p = p;
s->cap = cap;
}
static void sb_printf(sb_t *s, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
static void sb_printf(sb_t *s, const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
int need = vsnprintf(NULL, 0, fmt, ap);
va_end(ap);
if (need < 0)
return;
sb_grow(s, (size_t)need);
va_start(ap, fmt);
vsnprintf(s->p + s->n, s->cap - s->n, fmt, ap);
va_end(ap);
s->n += (size_t)need;
}
static void sb_add(sb_t *s, const void *data, size_t len)
{
sb_grow(s, len);
memcpy(s->p + s->n, data, len);
s->n += len;
s->p[s->n] = 0;
}
/* A JSON string, quoted */
static void sb_str(sb_t *s, const char *text)
{
sb_add(s, "\"", 1);
for (const unsigned char *c = (const unsigned char *)text; c && *c; c++) {
if (*c == '"' || *c == '\\')
sb_printf(s, "\\%c", *c);
else if (*c == '\n')
sb_add(s, "\\n", 2);
else if (*c < 0x20)
sb_printf(s, "\\u%04x", *c);
else
sb_add(s, c, 1);
}
sb_add(s, "\"", 1);
}
static void sb_base64(sb_t *s, const uint8_t *data, size_t len)
{
static const char table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
sb_grow(s, (len + 2) / 3 * 4 + 2);
s->p[s->n++] = '"';
for (size_t i = 0; i < len; i += 3) {
uint32_t v = (uint32_t)data[i] << 16 | (i + 1 < len ? data[i + 1] << 8 : 0) | (i + 2 < len ? data[i + 2] : 0);
s->p[s->n++] = table[v >> 18 & 63];
s->p[s->n++] = table[v >> 12 & 63];
s->p[s->n++] = i + 1 < len ? table[v >> 6 & 63] : '=';
s->p[s->n++] = i + 2 < len ? table[v & 63] : '=';
}
s->p[s->n++] = '"';
s->p[s->n] = 0;
}
/* ------------------------------------------------------------------- log */
static pthread_mutex_t g_log_lock = PTHREAD_MUTEX_INITIALIZER;
static void say(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
static void say(const char *fmt, ...)
{
pthread_mutex_lock(&g_log_lock);
FILE *f = fopen(LOG_PATH, "a");
if (f) {
time_t now = time(NULL);
struct tm tm;
gmtime_r(&now, &tm);
fprintf(f, "%02d:%02d:%02d ", tm.tm_hour, tm.tm_min, tm.tm_sec);
va_list ap;
va_start(ap, fmt);
vfprintf(f, fmt, ap);
va_end(ap);
fputc('\n', f);
fclose(f);
}
pthread_mutex_unlock(&g_log_lock);
}
static void notify(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
static void notify(const char *fmt, ...)
{
notify_request_t req;
memset(&req, 0, sizeof req);
va_list ap;
va_start(ap, fmt);
vsnprintf(req.message, sizeof req.message, fmt, ap);
va_end(ap);
sceKernelSendNotificationRequest(0, &req, sizeof req, 0);
}
static char *read_file(const char *path, size_t *len, size_t limit)
{
FILE *f = fopen(path, "rb");
if (!f)
return NULL;
sb_t s = {0};
char buf[16384];
size_t got;
while ((got = fread(buf, 1, sizeof buf, f)) > 0 && s.n < limit)
sb_add(&s, buf, got);
fclose(f);
if (!s.p)
sb_add(&s, "", 0);
if (len)
*len = s.n;
return s.p;
}
static void write_file(const char *path, const char *data, size_t len)
{
char tmp[512];
snprintf(tmp, sizeof tmp, "%s.tmp", path);
FILE *f = fopen(tmp, "wb");
if (!f) {
say("cannot write %s: %d", tmp, errno);
return;
}
fwrite(data, 1, len, f);
fclose(f);
rename(tmp, path);
chmod(path, 0666);
}
/* The value of "key": "..." in a JSON text, without a parser */
static void json_field(const char *json, const char *key, char *out, size_t size)
{
char pattern[64];
snprintf(pattern, sizeof pattern, "\"%s\"", key);
out[0] = 0;
const char *at = json ? strstr(json, pattern) : NULL;
if (!at || !(at = strchr(at + strlen(pattern), '"')))
return;
at++;
size_t n = 0;
while (*at && *at != '"' && n + 1 < size)
out[n++] = *at++;
out[n] = 0;
}
/* ---------------------------------------------------------------- titles */
typedef struct
{
uint32_t t; /* unix seconds */
uint32_t rss_kb, vsz_kb;
uint16_t cpu_permille; /* of one core */
uint16_t threads;
uint32_t map_kb[9]; /* mapped, by KVME_TYPE (0-8), from the latest map walk */
uint32_t res_kb[9]; /* resident, by type */
} sample_t;
typedef struct
{
bool used, live;
pid_t pid;
uint32_t app_id;
char title_id[16];
char name[96];
char version[24];
time_t started, ended;
bool watch; /* the policy says attach */
bool running; /* it has become the title (no longer the launcher's fork) */
time_t ready_at; /* attach from then: launch stops and resumes it */
bool traced; /* attached right now */
int trace_error;
int reports;
time_t retry_after;
char last_crash[160];
char outcome_pending[64]; /* report id waiting for its outcome */
time_t outcome_deadline;
uint64_t last_runtime_us;
uint64_t last_sample_us;
sample_t *samples; /* ring */
int nsamples, head;
uint32_t map_kb[9], res_kb[9];
} title_t;
static title_t g_titles[MAX_TITLES];
static volatile pid_t g_fault_pid; /* POST /api/test/fault: make this title fault for real */
static volatile bool g_fault_untraced; /* ... after detaching, as an untraced crash */
static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER;
static time_t g_started;
static volatile bool g_quit;
static uint64_t now_us(void)
{
struct timespec t;
clock_gettime(CLOCK_MONOTONIC, &t);
return (uint64_t)t.tv_sec * 1000000 + (uint64_t)t.tv_nsec / 1000;
}
static void title_metadata(title_t *t)
{
static const char *const roots[] = {"/data/homebrew/%s/sce_sys/param.json", "/system_ex/app/%s/sce_sys/param.json",
"/user/app/%s/sce_sys/param.json"};
for (unsigned r = 0; r < sizeof roots / sizeof *roots; r++) {
char path[256];
snprintf(path, sizeof path, roots[r], t->title_id);
char *json = read_file(path, NULL, 1 << 20);
if (!json)
continue;
json_field(json, "titleName", t->name, sizeof t->name);
json_field(json, "contentVersion", t->version, sizeof t->version);
free(json);
if (t->name[0])
return;
}
snprintf(t->name, sizeof t->name, "%s", t->title_id);
}
static bool watch_policy(const char *title_id)
{
char *list = read_file(WATCH_PATH, NULL, 65536);
if (list) {
bool all = false, found = false, none = false;
for (char *line = strtok(list, "\r\n"); line; line = strtok(NULL, "\r\n")) {
while (*line == ' ')
line++;
if (!strcmp(line, "all"))
all = true;
else if (!strcmp(line, "none"))
none = true;
else if (!strncmp(line, title_id, 9))
found = true;
}
free(list);
if (none || all || found)
return !none && (all || found);
}
char path[128];
struct stat st;
snprintf(path, sizeof path, "/data/homebrew/%s", title_id);
return stat(path, &st) == 0 && S_ISDIR(st.st_mode);
}
/* ----------------------------------------------------------- the debugger */
static int peek(pid_t pid, uint64_t addr, void *out, size_t len)
{
struct ptrace_io_desc io = {PIOD_READ_D, (void *)addr, out, len};
if (pt(PT_IO, pid, (caddr_t)&io, 0) != 0)
return -1;
return (int)io.piod_len;
}
typedef struct
{
uint64_t start, end, offset;
int prot;
char path[192];
} region_t;
typedef struct
{
uint64_t base, end;
char path[192];
} module_t;
static int read_regions(pid_t pid, region_t *regions, int max)
{
struct ptrace_vm_entry e;
memset(&e, 0, sizeof e);
int n = 0;
while (n < max) {
char path[192] = "";
e.pve_path = path;
e.pve_pathlen = sizeof path;
if (pt(PT_VM_ENTRY, pid, (caddr_t)&e, 0) != 0)
break;
regions[n].start = e.pve_start;
regions[n].end = e.pve_end;
regions[n].offset = e.pve_offset;
regions[n].prot = (int)e.pve_prot;
snprintf(regions[n].path, sizeof regions[n].path, "%s", path);
n++;
}
return n;
}
/* Modules: each file's lowest mapping is its base; anonymous code is its own */
static int modules_from(const region_t *r, int nr, module_t *m, int max)
{
int n = 0;
for (int i = 0; i < nr; i++) {
if (!r[i].path[0] && !(r[i].prot & 4))
continue;
int j = 0;
if (r[i].path[0])
for (; j < n; j++)
if (!strcmp(m[j].path, r[i].path))
break;
if (r[i].path[0] && j < n) {
if (r[i].start < m[j].base)
m[j].base = r[i].start;
if (r[i].end > m[j].end)
m[j].end = r[i].end;
continue;
}
if (n == max)
break;
m[n].base = r[i].start;
m[n].end = r[i].end;
snprintf(m[n].path, sizeof m[n].path, "%s", r[i].path[0] ? r[i].path : "(anonymous code)");
n++;
}
return n;
}
/* The process's loaded modules, from the kernel's list of its objects (the
* memory map has no paths on the PS5). The layout is the SDK's dynlib_obj_t:
* next at 0, path at 8, handle at 40, mapbase at 48, mapsize at 56, textsize
* at 64; the list's head is at 0x3e8 in struct proc. */
static int modules_from_kernel(pid_t pid, module_t *m, int max)
{
intptr_t proc = kernel_get_proc(pid);
uint64_t obj = 0;
int n = 0;
if (!proc || kernel_copyout(proc + 0x3e8, &obj, sizeof obj) < 0)
return 0;
for (int guard = 0; obj && n < max && guard < 1024; guard++) {
uint64_t fields[9];
if (kernel_copyout((intptr_t)obj, fields, sizeof fields) < 0)
break;
char path[192] = "";
if (fields[1])
kernel_copyout((intptr_t)fields[1], path, sizeof path - 1);
/* user-space modules only: the list starts with an entry that is not one */
if (fields[6] && fields[7] && fields[6] < 0x800000000000ull && fields[7] < 0x100000000ull) {
m[n].base = fields[6];
m[n].end = fields[6] + fields[7] - 1;
snprintf(m[n].path, sizeof m[n].path, "%s", path[0] ? path : "(unnamed module)");
n++;
}
obj = fields[0];
}
return n;
}
static const region_t *code_region(const region_t *r, int nr, uint64_t a)
{
for (int i = 0; i < nr; i++)
if ((r[i].prot & 4) && a >= r[i].start && a <= r[i].end)
return &r[i];
return NULL;
}
static const module_t *module_of(const module_t *m, int nm, uint64_t a)
{
for (int i = 0; i < nm; i++)
if (a >= m[i].base && a <= m[i].end)
return &m[i];
return NULL;
}
/* The main executable while a report is taken: its code is read from its file */
static char g_exe[256];
static uint64_t g_exe_base, g_exe_end;
static int elf_file_read(const char *path, uint64_t vaddr, void *buf, size_t len);
/* Does the instruction before this address look like a call? */
static bool after_call(pid_t pid, uint64_t ret)
{
uint8_t b[8];
if (peek(pid, ret - 8, b, 8) != 8 &&
!(g_exe[0] && ret >= g_exe_base + 8 && ret <= g_exe_end && elf_file_read(g_exe, ret - 8 - g_exe_base, b, 8) == 8))
return false;
if (b[3] == 0xe8) /* call rel32 */
return true;
for (int k = 2; k <= 7; k++) /* call r/m: FF /2 */
if (b[8 - k] == 0xff && ((b[8 - k + 1] >> 3) & 7) == 2)
return true;
return false;
}
typedef struct
{
uint64_t addr;
const char *how;
} frame_t;
static int backtrace(pid_t pid, const struct reg *rg, const region_t *r, int nr, frame_t *f)
{
int n = 0;
f[n++] = (frame_t){rg->r_rip, "pc"};
/* a leaf function has pushed nothing: its caller is at the stack pointer */
uint64_t top;
if (peek(pid, rg->r_rsp, &top, 8) == 8 && code_region(r, nr, top) && after_call(pid, top))
f[n++] = (frame_t){top, "leaf"};
uint64_t fp = rg->r_rbp;
for (int i = 0; i < 64 && fp && n < MAX_FRAMES; i++) {
uint64_t pair[2];
if (peek(pid, fp, pair, 16) != 16 || !code_region(r, nr, pair[1]))
break;
if (!(n == 2 && f[1].addr == pair[1]))
f[n++] = (frame_t){pair[1], "fp"};
if (pair[0] <= fp)
break;
fp = pair[0];
}
return n;
}
static const char *signal_name(int sig)
{
switch (sig) {
case SIGSEGV: return "SIGSEGV";
case SIGBUS: return "SIGBUS";
case SIGILL: return "SIGILL";
case SIGFPE: return "SIGFPE";
case SIGABRT: return "SIGABRT";
case SIGSYS: return "SIGSYS";
case SIGTRAP: return "SIGTRAP";
default: return "signal";
}
}
static bool is_crash(int sig)
{
return sig == SIGSEGV || sig == SIGBUS || sig == SIGILL || sig == SIGFPE || sig == SIGABRT || sig == SIGSYS ||
sig == SIGTRAP;
}
static uint64_t fnv64(const uint8_t *p, size_t n)
{
uint64_t h = 1469598103934665603ull;
for (size_t i = 0; i < n; i++)
h = (h ^ p[i]) * 1099511628211ull;
return h;
}
/* The title's executable on disk. Its code is execute-only in memory (even a
* debugger cannot read it), so the build is identified, and the code at a
* fault shown, from the file. Reads len bytes at the address vaddr (relative to
* the image's base); returns how many it read. */
static int elf_file_read(const char *path, uint64_t vaddr, void *buf, size_t len)
{
FILE *f = fopen(path, "rb");
if (!f)
return -1;
/* A title's eboot.bin is a fake-signed SELF: a table of entries, then the
* ELF header and program headers, with each segment's bytes stored as
* they are. A plain ELF is read the same way with no table. */
uint8_t head[32];
long elf_at = 0;
uint16_t entries = 0;
int got = -1;
if (fread(head, 1, 32, f) != 32)
goto done;
if (!memcmp(head, "\x4f\x15\x3d\x1d", 4)) {
memcpy(&entries, head + 24, 2);
elf_at = 32 + 32L * entries;
}
uint8_t eh[64];
if (fseek(f, elf_at, SEEK_SET) || fread(eh, 1, 64, f) != 64 || memcmp(eh, "\x7f" "ELF", 4) || eh[4] != 2)
goto done;
uint64_t phoff;
uint16_t phentsize, phnum;
memcpy(&phoff, eh + 32, 8);
memcpy(&phentsize, eh + 54, 2);
memcpy(&phnum, eh + 56, 2);
for (unsigned i = 0; i < phnum && got < 0; i++) {
uint8_t ph[56];
if (fseek(f, elf_at + (long)(phoff + i * phentsize), SEEK_SET) || fread(ph, 1, 56, f) != 56)
break;
uint32_t type;
uint64_t offset, va, filesz;
memcpy(&type, ph, 4);
memcpy(&offset, ph + 8, 8);
memcpy(&va, ph + 16, 8);
memcpy(&filesz, ph + 32, 8);
if (type != 1 || vaddr < va || vaddr >= va + filesz)
continue;
if (entries) {
/* the SELF entry holding this segment: its index in bits 20-31 of
the properties, stored whole (the same size as the segment) */
uint64_t found = 0;
for (unsigned e = 0; e < entries && !found; e++) {
uint64_t entry[4];
if (fseek(f, 32 + 32L * e, SEEK_SET) || fread(entry, 1, 32, f) != 32)
break;
if (((entry[0] >> 20) & 0xfff) == i && entry[2] == filesz)
found = entry[1];
}
if (!found)
break;
offset = found;
}
size_t n = len < va + filesz - vaddr ? len : (size_t)(va + filesz - vaddr);
if (fseek(f, (long)(offset + vaddr - va), SEEK_SET) == 0)
got = (int)fread(buf, 1, n, f);
}
done:
fclose(f);
return got;
}
/* Where the title's executable is, seen from here (it sees itself as /app0) */
static void title_file(const title_t *t, const char *module_path, char *out, size_t size)
{
static const char *const roots[] = {"/data/homebrew/%s%s", "/system_ex/app/%s%s", "/user/app/%s%s"};
const char *inside = !strncmp(module_path, "/app0", 5) ? module_path + 5 : "/eboot.bin";
for (unsigned r = 0; r < sizeof roots / sizeof *roots; r++) {
struct stat st;
snprintf(out, size, roots[r], t->title_id, inside);
if (stat(out, &st) == 0)
return;
}
out[0] = 0;
}
static void sb_regs(sb_t *s, const struct reg *rg)
{
sb_printf(s,
"{\"rip\":\"0x%lx\",\"rsp\":\"0x%lx\",\"rbp\":\"0x%lx\",\"rax\":\"0x%lx\",\"rbx\":\"0x%lx\",\"rcx\":"
"\"0x%lx\",\"rdx\":\"0x%lx\",\"rsi\":\"0x%lx\",\"rdi\":\"0x%lx\",\"r8\":\"0x%lx\",\"r9\":\"0x%lx\","
"\"r10\":\"0x%lx\",\"r11\":\"0x%lx\",\"r12\":\"0x%lx\",\"r13\":\"0x%lx\",\"r14\":\"0x%lx\",\"r15\":"
"\"0x%lx\",\"rflags\":\"0x%lx\",\"fs\":\"0x%x\",\"gs\":\"0x%x\"}",
rg->r_rip, rg->r_rsp, rg->r_rbp, rg->r_rax, rg->r_rbx, rg->r_rcx, rg->r_rdx, rg->r_rsi, rg->r_rdi,
rg->r_r8, rg->r_r9, rg->r_r10, rg->r_r11, rg->r_r12, rg->r_r13, rg->r_r14, rg->r_r15, rg->r_rflags,
(unsigned)rg->r_fs, (unsigned)rg->r_gs);
}
static void sb_frames(sb_t *s, const frame_t *f, int n, const module_t *m, int nm)
{
sb_add(s, "[", 1);
for (int i = 0; i < n; i++) {
const module_t *mod = module_of(m, nm, f[i].addr);
sb_printf(s, "%s{\"addr\":\"0x%lx\",\"how\":\"%s\",\"module\":", i ? "," : "", f[i].addr, f[i].how);
sb_str(s, mod ? mod->path : "?");
sb_printf(s, ",\"offset\":\"0x%lx\"}", mod ? f[i].addr - mod->base : 0);
}
sb_add(s, "]", 1);
}
static void sb_samples(sb_t *s, const title_t *t, uint32_t since)
{
sb_add(s, "[", 1);
bool first = true;
for (int i = 0; i < t->nsamples; i++) {
const sample_t *x = &t->samples[(t->head - t->nsamples + i + MAX_SAMPLES) % MAX_SAMPLES];
if (x->t <= since)
continue;
sb_printf(s, "%s[%u,%u,%u,%u,%u,[", first ? "" : ",", x->t, x->rss_kb, x->vsz_kb, x->cpu_permille,
x->threads);
for (int k = 0; k < 9; k++)
sb_printf(s, "%s%u", k ? "," : "", x->map_kb[k]);
sb_add(s, "],[", 3);
for (int k = 0; k < 9; k++)
sb_printf(s, "%s%u", k ? "," : "", x->res_kb[k]);
sb_add(s, "]]", 2);
first = false;
}
sb_add(s, "]", 1);
}
/* Write what a crash report needs, while the title is stopped at the fault.
* Returns the report's id in id. */
static void capture(title_t *t, pid_t pid, int sig, char *id, size_t idsize)
{
static region_t regions[4096];
static module_t modules[MAX_MODULES];
static uint8_t stack[STACK_COPY];
static pthread_mutex_t capture_lock = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&capture_lock);
struct ptrace_lwpinfo li;
memset(&li, 0, sizeof li);
pt(PT_LWPINFO, pid, (caddr_t)&li, sizeof li);
struct reg rg;
memset(&rg, 0, sizeof rg);
pt(PT_GETREGS, li.pl_lwpid, (caddr_t)&rg, 0);
int nr = read_regions(pid, regions, 4096);
int nm = modules_from_kernel(pid, modules, MAX_MODULES);
if (nm == 0)
nm = modules_from(regions, nr, modules, MAX_MODULES);
char exe[256] = "";
const module_t *main_module = NULL;
for (int i = 0; i < nm; i++) {
const char *slash = strrchr(modules[i].path, '/');
if (!strcmp(slash ? slash + 1 : modules[i].path, "eboot.bin"))
main_module = &modules[i];
}
if (main_module)
title_file(t, main_module->path, exe, sizeof exe);
snprintf(g_exe, sizeof g_exe, "%s", exe);
g_exe_base = main_module ? main_module->base : 0;
g_exe_end = main_module ? main_module->end : 0;
frame_t frames[MAX_FRAMES];
int nf = backtrace(pid, &rg, regions, nr, frames);
time_t now = time(NULL);
struct tm tm;
gmtime_r(&now, &tm);
snprintf(id, idsize, "%04d%02d%02d-%02d%02d%02d-%s-%d", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour,
tm.tm_min, tm.tm_sec, t->title_id, pid);
char when[32];
strftime(when, sizeof when, "%Y-%m-%dT%H:%M:%SZ", &tm);
const module_t *top = module_of(modules, nm, rg.r_rip);
sb_t s = {0};
sb_printf(&s, "{\"format\":1,\"id\":\"%s\",\"time\":\"%s\",\"title_id\":", id, when);
sb_str(&s, t->title_id);
sb_add(&s, ",\"title_name\":", 14);
sb_str(&s, t->name);
sb_add(&s, ",\"title_version\":", 17);
sb_str(&s, t->version);
sb_printf(&s, ",\"pid\":%d,\"app_id\":%u,\"uptime_s\":%ld,\"firmware\":\"%08x\",\"devtools\":\"%s\"", pid,
t->app_id, (long)(now - t->started), kernel_get_fw_version(), DEVTOOLS_VERSION);
sb_printf(&s, ",\"signal\":%d,\"signal_name\":\"%s\",\"si_code\":%d,\"fault_addr\":\"0x%lx\"", sig,
signal_name(sig), li.pl_siginfo.si_code, (uint64_t)li.pl_siginfo.si_addr);
sb_printf(&s, ",\"thread\":{\"id\":%d,\"name\":", li.pl_lwpid);
sb_str(&s, li.pl_tdname);
sb_add(&s, "},\"where\":", 10);
/* where it happened: the faulting module, or for a jump to nowhere (a bad
function pointer), the code that made it */
char where[256];
const frame_t *at = &frames[0];
const char *how = "";
if (!top) {
for (int i = 1; i < nf; i++)
if (module_of(modules, nm, frames[i].addr)) {
at = &frames[i];
how = " (called from)";
break;
}
}
const module_t *at_module = module_of(modules, nm, at->addr);
if (at_module) {
const char *slash = strrchr(at_module->path, '/');
snprintf(where, sizeof where, "%s+0x%lx%s", slash ? slash + 1 : at_module->path, at->addr - at_module->base, how);
} else
snprintf(where, sizeof where, "pc 0x%lx", rg.r_rip);
sb_str(&s, where);
/* the first frame in the title's own code: what a developer can change */
char mine[64] = "";
for (int i = 0; i < nf && main_module; i++)
if (frames[i].addr >= main_module->base && frames[i].addr <= main_module->end) {
snprintf(mine, sizeof mine, "eboot.bin+0x%lx", frames[i].addr - main_module->base);
break;
}
sb_add(&s, ",\"title_frame\":", 15);
sb_str(&s, mine);
sb_add(&s, ",\"registers\":", 13);
sb_regs(&s, &rg);
sb_add(&s, ",\"backtrace\":", 13);
sb_frames(&s, frames, nf, modules, nm);
/* the code either side of the fault */
uint8_t code[48];
int got = peek(pid, rg.r_rip - 16, code, sizeof code);
if (got < 0 && exe[0] && main_module && (uint64_t)rg.r_rip >= main_module->base + 16 &&
(uint64_t)rg.r_rip < main_module->end)
got = elf_file_read(exe, (uint64_t)rg.r_rip - 16 - main_module->base, code, sizeof code);
sb_add(&s, ",\"code_at_pc\":\"", 15);
for (int i = 0; i < got; i++)
sb_printf(&s, "%02x%s", code[i], i == 15 ? "|" : "");
sb_add(&s, "\"", 1);
/* the faulting thread's stack, for unwinding on the host */
int stack_len = peek(pid, rg.r_rsp, stack, sizeof stack);
if (stack_len < 0) {
/* the end of the stack is near: take what there is, page by page */
stack_len = 0;
while (stack_len < STACK_COPY && peek(pid, rg.r_rsp + stack_len, stack + stack_len, 4096) == 4096)
stack_len += 4096;
}
sb_printf(&s, ",\"stack\":{\"base\":\"0x%lx\",\"data\":", rg.r_rsp);
sb_base64(&s, stack, (size_t)stack_len);
sb_add(&s, "}", 1);
/* every thread */
int nlwps = pt(PT_GETNUMLWPS, pid, NULL, 0);
sb_printf(&s, ",\"threads\":[");
if (nlwps > 0) {
lwpid_t *lwps = calloc((size_t)nlwps, sizeof *lwps);
nlwps = pt(PT_GETLWPLIST, pid, (caddr_t)lwps, nlwps);
for (int i = 0; i < nlwps; i++) {
struct ptrace_lwpinfo tl;
struct reg tr;
memset(&tl, 0, sizeof tl);
memset(&tr, 0, sizeof tr);
pt(PT_LWPINFO, lwps[i], (caddr_t)&tl, sizeof tl);
pt(PT_GETREGS, lwps[i], (caddr_t)&tr, 0);
frame_t tf[MAX_FRAMES];
int ntf = backtrace(pid, &tr, regions, nr, tf);
sb_printf(&s, "%s{\"id\":%d,\"name\":", i ? "," : "", lwps[i]);
sb_str(&s, tl.pl_tdname);
sb_printf(&s, ",\"crashed\":%s,\"rip\":\"0x%lx\",\"rsp\":\"0x%lx\",\"backtrace\":",
lwps[i] == li.pl_lwpid ? "true" : "false", tr.r_rip, tr.r_rsp);
sb_frames(&s, tf, ntf, modules, nm);
sb_add(&s, "}", 1);
}
free(lwps);
}
sb_add(&s, "]", 1);
/* modules, with a hash of the main executable's first 64 KiB of code, which
the host matches against its builds' ELFs */
sb_add(&s, ",\"modules\":[", 12);
for (int i = 0; i < nm; i++) {
sb_printf(&s, "%s{\"path\":", i ? "," : "");
sb_str(&s, modules[i].path);
sb_printf(&s, ",\"base\":\"0x%lx\",\"end\":\"0x%lx\"", modules[i].base, modules[i].end);
if (&modules[i] == main_module) {
uint8_t *text = malloc(65536);
sb_add(&s, ",\"main\":true,\"file\":", 20);
sb_str(&s, exe);
if (text && exe[0] && elf_file_read(exe, 0, text, 65536) == 65536)
sb_printf(&s, ",\"text_fnv64\":\"%016lx\"", fnv64(text, 65536));
free(text);
}
sb_add(&s, "}", 1);
}
sb_add(&s, "]", 1);
pthread_mutex_lock(&g_lock);
sb_printf(&s, ",\"memory_kb_by_type\":{\"mapped\":[");
for (int k = 0; k < 9; k++)
sb_printf(&s, "%s%u", k ? "," : "", t->map_kb[k]);
sb_add(&s, "],\"resident\":[", 14);
for (int k = 0; k < 9; k++)
sb_printf(&s, "%s%u", k ? "," : "", t->res_kb[k]);
sb_add(&s, "]},\"memory\":", 12);
sb_samples(&s, t, (uint32_t)now - 120);
pthread_mutex_unlock(&g_lock);
sb_add(&s, ",\"outcome\":\"pending\"}", 21);
char path[256];
snprintf(path, sizeof path, CRASH_DIR "/%s.json", id);
write_file(path, s.p, s.n);
free(s.p);
pthread_mutex_unlock(&capture_lock);
say("crash: %s %s %s at %s, fault address %p, thread %d %s: %s", t->title_id, t->name, signal_name(sig), where,
li.pl_siginfo.si_addr, li.pl_lwpid, li.pl_tdname, path);
if (mine[0] && strncmp(where, mine, strlen(mine)))
snprintf(t->last_crash, sizeof t->last_crash, "%s, in %s", where, mine);
else
snprintf(t->last_crash, sizeof t->last_crash, "%s", where);
}
/* Finish a report: the title died of the crash, or carried on (its own handler took it) */
static void finish_report(const char *id, const char *outcome, const char *name, const char *where)
{
char path[256];
snprintf(path, sizeof path, CRASH_DIR "/%s.json", id);
size_t len;
char *json = read_file(path, &len, 64 << 20);
if (!json)
return;
char *at = strstr(json, "\"outcome\":\"pending\"}");
if (at) {
sb_t s = {0};
sb_add(&s, json, (size_t)(at - json));
sb_printf(&s, "\"outcome\":\"%s\"}", outcome);
write_file(path, s.p, s.n);
free(s.p);
}
free(json);
FILE *index = fopen(CRASH_DIR "/index.txt", "a");
if (index) {
fprintf(index, "%s\t%s\n", id, outcome);
fclose(index);
}
say("report %s: %s", id, outcome);
if (!strcmp(outcome, "crashed"))
notify("%s crashed at %s\nReport saved (PS5 DevTools)", name, where);
}
typedef struct
{
int slot;
pid_t pid;
} debug_arg_t;
static void *debugger(void *argument)
{
debug_arg_t arg = *(debug_arg_t *)argument;
free(argument);
title_t *t = &g_titles[arg.slot];
pid_t pid = arg.pid;
{
int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};
struct kinfo_proc ki;
size_t len = sizeof ki;
if (sysctl(mib, 4, &ki, &len, NULL, 0) == 0)
say("attaching to %s pid %d: state %d flags %lx comm %s", t->title_id, pid, ki.ki_stat, (long)ki.ki_flag,
ki.ki_comm);
}
if (pt(PT_ATTACH, pid, 0, 0) != 0) {
const int error = errno;
pthread_mutex_lock(&g_lock);
t->trace_error = error ? error : -1;
t->traced = false;
t->retry_after = time(NULL) + 10;
pthread_mutex_unlock(&g_lock);
say("attach %s pid %d failed: errno %d", t->title_id, pid, error);
return NULL;
}
int status;
if (waitpid(pid, &status, 0) < 0 || !WIFSTOPPED(status)) {
say("attach %s pid %d: no stop (status %x errno %d)", t->title_id, pid, status, errno);
pt(PT_DETACH, pid, (caddr_t)1, 0);
pthread_mutex_lock(&g_lock);
t->traced = false;
t->retry_after = time(NULL) + 10;
pthread_mutex_unlock(&g_lock);
return NULL;
}
pt(PT_CONTINUE, pid, (caddr_t)1, 0);
pthread_mutex_lock(&g_lock);
t->traced = true;
t->trace_error = 0;
pthread_mutex_unlock(&g_lock);
say("attached to %s (%s) pid %d", t->title_id, t->name, pid);
for (;;) {
if (waitpid(pid, &status, 0) < 0) {
say("%s pid %d: waitpid errno %d", t->title_id, pid, errno);
break;
}
if (WIFEXITED(status) || WIFSIGNALED(status)) {
say("%s pid %d ended: %s %d", t->title_id, pid, WIFEXITED(status) ? "exit" : "signal",
WIFEXITED(status) ? WEXITSTATUS(status) : WTERMSIG(status));
break;
}
if (!WIFSTOPPED(status))
continue;
int sig = WSTOPSIG(status);
if (g_quit) {
pt(PT_DETACH, pid, (caddr_t)1, sig == SIGSTOP ? 0 : sig);
break;
}
if (is_crash(sig) && t->reports < MAX_REPORTS_PER_PID) {
char id[96];
capture(t, pid, sig, id, sizeof id);
pthread_mutex_lock(&g_lock);
t->reports++;
snprintf(t->outcome_pending, sizeof t->outcome_pending, "%s", id);
t->outcome_deadline = time(NULL) + 15;
t->traced = false;
t->retry_after = time(NULL) + 20;
pthread_mutex_unlock(&g_lock);
/* the system sees the crash exactly as it would untraced */
pt(PT_DETACH, pid, (caddr_t)1, sig);
return NULL;
}
if (sig == SIGSTOP && g_fault_pid == pid) {
/* the test: send the stopped thread to an unmapped address, so the
fault takes the same path through the kernel as a real one */
g_fault_pid = 0;
struct ptrace_lwpinfo li;
struct reg rg;
memset(&li, 0, sizeof li);
pt(PT_LWPINFO, pid, (caddr_t)&li, sizeof li);
pt(PT_GETREGS, li.pl_lwpid, (caddr_t)&rg, 0);
say("%s pid %d: test fault, thread %d rip %lx -> 0x10", t->title_id, pid, li.pl_lwpid, rg.r_rip);
/* as a call through a null pointer would leave it: the caller's
return address on the stack */
rg.r_rsp -= 8;
uint64_t ret = rg.r_rip;
struct ptrace_io_desc io = {PIOD_WRITE_D, (void *)rg.r_rsp, &ret, 8};
pt(PT_IO, pid, (caddr_t)&io, 0);
rg.r_rip = 0x10;
pt(PT_SETREGS, li.pl_lwpid, (caddr_t)&rg, 0);
if (g_fault_untraced) {
/* the comparison: the system sees the fault with no debugger */
g_fault_untraced = false;
pt(PT_DETACH, pid, (caddr_t)1, 0);
pthread_mutex_lock(&g_lock);
t->traced = false;
t->reports = MAX_REPORTS_PER_PID; /* do not attach again */
pthread_mutex_unlock(&g_lock);
say("%s pid %d: detached before the test fault", t->title_id, pid);
return NULL;
}
pt(PT_CONTINUE, pid, (caddr_t)1, 0);
continue;
}
if (sig == SIGSTOP) {
/* a stop that is not ours: step aside and let it happen */
say("%s pid %d: stop requested, detaching", t->title_id, pid);
pt(PT_DETACH, pid, (caddr_t)1, SIGSTOP);
pthread_mutex_lock(&g_lock);
t->traced = false;
t->retry_after = time(NULL) + 10;
pthread_mutex_unlock(&g_lock);
return NULL;
}
pt(PT_CONTINUE, pid, (caddr_t)1, sig);
}
pthread_mutex_lock(&g_lock);
t->traced = false;
pthread_mutex_unlock(&g_lock);
return NULL;
}
/* --------------------------------------------------------------- sampling */
static void map_totals(pid_t pid, uint32_t map_kb[9], uint32_t res_kb[9], bool dump)
{
int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_VMMAP, pid};
size_t size = 0;
memset(map_kb, 0, 9 * sizeof *map_kb);
memset(res_kb, 0, 9 * sizeof *res_kb);
if (sysctl(mib, 4, NULL, &size, NULL, 0) || !size)
return;
size = size * 5 / 4;
char *buf = malloc(size);
if (!buf)
return;
if (sysctl(mib, 4, buf, &size, NULL, 0) == 0) {
for (char *p = buf; p < buf + size;) {
struct kinfo_vmentry *e = (struct kinfo_vmentry *)p;
if (e->kve_structsize <= 0)
break;
p += e->kve_structsize;
int k = e->kve_type >= 0 && e->kve_type <= 8 ? e->kve_type : 0;
map_kb[k] += (uint32_t)((e->kve_end - e->kve_start) / 1024);
res_kb[k] += (uint32_t)((uint64_t)e->kve_resident * 16); /* 16 KiB pages */
if (dump && e->kve_end - e->kve_start >= 64 << 20)
say(" map %012lx-%012lx %6lu MiB type %d prot %x res %d path \"%s\"", e->kve_start, e->kve_end,
(e->kve_end - e->kve_start) >> 20, e->kve_type, e->kve_protection, e->kve_resident, e->kve_path);
}
}
free(buf);
}
static int find_slot(pid_t pid)
{
for (int i = 0; i < MAX_TITLES; i++)
if (g_titles[i].used && g_titles[i].pid == pid)
return i;
return -1;
}
static int new_slot(void)
{
int oldest = -1;
for (int i = 0; i < MAX_TITLES; i++) {
if (!g_titles[i].used)
return i;
if (!g_titles[i].live && !g_titles[i].traced && !g_titles[i].outcome_pending[0] &&
(oldest < 0 || g_titles[i].ended < g_titles[oldest].ended))
oldest = i;
}
return oldest;
}
static void *watcher(void *unused)
{
(void)unused;
unsigned tick = 0;
for (; !g_quit; tick++) {
int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PROC, 0};
size_t size = 0;
char *buf = NULL;
if (sysctl(mib, 4, NULL, &size, NULL, 0) == 0 && (size += 32768, buf = malloc(size)) &&
sysctl(mib, 4, buf, &size, NULL, 0) == 0) {
uint64_t now = now_us();
pthread_mutex_lock(&g_lock);
for (int i = 0; i < MAX_TITLES; i++)
if (g_titles[i].used)
g_titles[i].live = false;
for (char *p = buf; p < buf + size;) {
struct kinfo_proc *ki = (struct kinfo_proc *)p;
if (ki->ki_structsize <= 0)
break;
p += ki->ki_structsize;
app_info_t ai;
memset(&ai, 0, sizeof ai);
if (sceKernelGetAppInfo(ki->ki_pid, &ai) || !ai.app_id || !ai.title_id[0] ||
!strncmp(ai.title_id, "NPXS", 4))
continue;
int slot = find_slot(ki->ki_pid);
if (slot < 0) {
slot = new_slot();
if (slot < 0)
continue;
title_t *t = &g_titles[slot];
sample_t *samples = t->samples;
memset(t, 0, sizeof *t);
t->samples = samples ? samples : calloc(MAX_SAMPLES, sizeof(sample_t));
t->used = true;
t->pid = ki->ki_pid;
t->app_id = ai.app_id;
snprintf(t->title_id, sizeof t->title_id, "%.9s", ai.title_id);
title_metadata(t);
t->started = time(NULL);
t->watch = watch_policy(t->title_id);
t->last_runtime_us = ki->ki_runtime;
t->last_sample_us = now;
say("title %s (%s %s) pid %d %s: %s", t->title_id, t->name, t->version, t->pid, ki->ki_comm,
t->watch ? "watching" : "sampling only");
map_totals(t->pid, t->map_kb, t->res_kb, true);
}
title_t *t = &g_titles[slot];
t->live = true;
if (!t->running && strcmp(ki->ki_comm, "SceSysCore.elf") != 0) {
t->running = true;
t->ready_at = time(NULL) + 3;
say("%s pid %d is now %s", t->title_id, t->pid, ki->ki_comm);
}
if (tick % 2 == 0 && t->samples) {
if (tick % 10 == 0)
map_totals(t->pid, t->map_kb, t->res_kb, false);
sample_t *x = &t->samples[t->head];
uint64_t elapsed = now - t->last_sample_us;
x->t = (uint32_t)time(NULL);
x->rss_kb = (uint32_t)((uint64_t)ki->ki_rssize * 16);
x->vsz_kb = (uint32_t)(ki->ki_size / 1024);
x->cpu_permille =
elapsed ? (uint16_t)((ki->ki_runtime - t->last_runtime_us) * 1000 / elapsed) : 0;
x->threads = (uint16_t)ki->ki_numthreads;
memcpy(x->map_kb, t->map_kb, sizeof x->map_kb);
memcpy(x->res_kb, t->res_kb, sizeof x->res_kb);
t->head = (t->head + 1) % MAX_SAMPLES;
if (t->nsamples < MAX_SAMPLES)
t->nsamples++;
t->last_runtime_us = ki->ki_runtime;
t->last_sample_us = now;
}
}
time_t wall = time(NULL);
for (int i = 0; i < MAX_TITLES; i++) {
title_t *t = &g_titles[i];
if (!t->used)
continue;
if (!t->live && !t->ended) {
t->ended = wall;
say("title %s pid %d is gone", t->title_id, t->pid);
}
if (t->outcome_pending[0] && (!t->live || wall >= t->outcome_deadline)) {
char id[64], name[96], where[160];
snprintf(id, sizeof id, "%s", t->outcome_pending);
snprintf(name, sizeof name, "%s", t->name);
snprintf(where, sizeof where, "%s", t->last_crash);
bool died = !t->live;
t->outcome_pending[0] = 0;
pthread_mutex_unlock(&g_lock);
finish_report(id, died ? "crashed" : "handled", name, where);
pthread_mutex_lock(&g_lock);
}
if (t->live && t->running && wall >= t->ready_at && t->watch && !t->traced &&
!t->outcome_pending[0] && wall >= t->retry_after &&
t->reports < MAX_REPORTS_PER_PID) {
debug_arg_t *arg = malloc(sizeof *arg);
arg->slot = i;
arg->pid = t->pid;
t->traced = true; /* until the debugger says otherwise */
t->retry_after = wall + 30;
pthread_t thread;
if (pthread_create(&thread, NULL, debugger, arg) == 0)
pthread_detach(thread);
else {
t->traced = false;
free(arg);
}
}
}
pthread_mutex_unlock(&g_lock);
}
free(buf);
usleep(500000);
}
return NULL;
}
/* -------------------------------------------------------------- leaving */
/* A debugger that exits takes its tracees with it (the kernel kills a traced
* process whose debugger goes), so before this payload leaves, by request or by
* crashing, every title is stopped and detached. A stopped title's debugger
* thread sees g_quit and detaches; this waits until they all have. */
static int traced_count(void)
{
int n = 0;
for (int i = 0; i < MAX_TITLES; i++)
n += g_titles[i].used && g_titles[i].traced;
return n;
}
static void detach_all(int wait_ms)
{
g_quit = true;
for (int i = 0; i < MAX_TITLES; i++)
if (g_titles[i].used && g_titles[i].traced && g_titles[i].live)
kill(g_titles[i].pid, SIGSTOP);
for (int waited = 0; traced_count() && waited < wait_ms; waited += 50)
usleep(50000);
}
static void payload_crashed(int sig)
{
/* async-signal-safe only: kill, waitpid, ptrace, usleep, _exit */
g_quit = true;
for (int i = 0; i < MAX_TITLES; i++) {
title_t *t = &g_titles[i];
if (!t->used || !t->traced || !t->live)
continue;
kill(t->pid, SIGSTOP);
int status;
for (int tries = 0; tries < 40; tries++) {
if (pt(PT_DETACH, t->pid, (caddr_t)1, 0) == 0)
break;
waitpid(t->pid, &status, WNOHANG);
usleep(25000);
}
}
_exit(128 + sig);
}
/* ------------------------------------------------------------------- http */
extern const char dashboard_html[];
extern const char dashboard_html_end[];
__asm__(".section .rodata\n"
".global dashboard_html\n"
"dashboard_html:\n"
".incbin \"" DASHBOARD_PATH "\"\n"
".global dashboard_html_end\n"
"dashboard_html_end:\n"
".byte 0\n"
".previous\n");
static void send_all(int fd, const void *data, size_t len)
{
const char *p = data;
while (len) {
ssize_t n = send(fd, p, len, 0);
if (n <= 0)
return;
p += n;
len -= (size_t)n;
}
}
static void reply(int fd, int code, const char *type, const char *body, size_t len)
{
char head[512];
int n = snprintf(head, sizeof head,
"HTTP/1.1 %d %s\r\nContent-Type: %s\r\nContent-Length: %zu\r\nAccess-Control-Allow-Origin: *\r\n"
"Access-Control-Allow-Methods: GET, POST, DELETE\r\nCache-Control: no-store\r\nConnection: "
"close\r\n\r\n",
code, code == 200 ? "OK" : code == 404 ? "Not Found" : "Error", type, len);
send_all(fd, head, (size_t)n);
send_all(fd, body, len);
}
static void reply_text(int fd, int code, const char *type, const char *text)
{
reply(fd, code, type, text, strlen(text));
}
static void reply_sb(int fd, sb_t *s)
{
reply(fd, 200, "application/json", s->p ? s->p : "", s->n);
free(s->p);
}
static bool safe_id(const char *id)
{
if (!*id || strlen(id) > 90)
return false;
for (const char *c = id; *c; c++)
if (!(*c == '-' || *c == '_' || (*c >= '0' && *c <= '9') || (*c >= 'A' && *c <= 'Z') ||
(*c >= 'a' && *c <= 'z')))
return false;
return true;
}
static void api_status(int fd)
{
sb_t s = {0};
sb_printf(&s, "{\"devtools\":\"%s\",\"firmware\":\"%08x\",\"started\":%ld,\"now\":%ld,\"titles\":[",
DEVTOOLS_VERSION, kernel_get_fw_version(), (long)g_started, (long)time(NULL));
pthread_mutex_lock(&g_lock);
bool first = true;
for (int i = 0; i < MAX_TITLES; i++) {
title_t *t = &g_titles[i];
if (!t->used)
continue;
sb_printf(&s, "%s{\"pid\":%d,\"title_id\":", first ? "" : ",", t->pid);
sb_str(&s, t->title_id);
sb_add(&s, ",\"name\":", 8);
sb_str(&s, t->name);
sb_add(&s, ",\"version\":", 11);
sb_str(&s, t->version);
sb_printf(&s,
",\"live\":%s,\"started\":%ld,\"ended\":%ld,\"watch\":%s,\"traced\":%s,\"trace_error\":%d,"
"\"reports\":%d}",
t->live ? "true" : "false", (long)t->started, (long)t->ended, t->watch ? "true" : "false",
t->traced ? "true" : "false", t->trace_error, t->reports);
first = false;
}
pthread_mutex_unlock(&g_lock);
sb_add(&s, "]}", 2);
reply_sb(fd, &s);
}
static void api_memory(int fd, const char *query)
{
pid_t pid = -1;
uint32_t since = 0;
const char *q;
if ((q = strstr(query, "pid=")))
pid = atoi(q + 4);
if ((q = strstr(query, "since=")))
since = (uint32_t)strtoul(q + 6, NULL, 10);
sb_t s = {0};
pthread_mutex_lock(&g_lock);
int slot = find_slot(pid);
if (slot >= 0) {
sb_printf(&s, "{\"pid\":%d,\"fields\":[\"t\",\"rss_kb\",\"vsz_kb\",\"cpu_permille\",\"threads\",\"mapped_kb_by_type\","
"\"resident_kb_by_type\"],\"samples\":",
pid);
sb_samples(&s, &g_titles[slot], since);
sb_add(&s, "}", 1);
}
pthread_mutex_unlock(&g_lock);
if (slot < 0)
reply_text(fd, 404, "application/json", "{\"error\":\"no such title\"}");
else
reply_sb(fd, &s);
}
static int compare_names(const void *a, const void *b)
{
return strcmp(*(char *const *)b, *(char *const *)a);
}
/* The summary of each report: its first fields, up to the registers */
static void api_crashes(int fd)
{
DIR *dir = opendir(CRASH_DIR);
char *names[512];
int n = 0;
struct dirent *e;
while (dir && (e = readdir(dir)) && n < 512) {
size_t len = strlen(e->d_name);
if (len > 5 && !strcmp(e->d_name + len - 5, ".json"))
names[n++] = strdup(e->d_name);
}
if (dir)
closedir(dir);
qsort(names, (size_t)n, sizeof *names, compare_names);
sb_t s = {0};
sb_add(&s, "[", 1);
for (int i = 0; i < n; i++) {
char path[300];
snprintf(path, sizeof path, CRASH_DIR "/%s", names[i]);
FILE *f = fopen(path, "rb");
char head[4096];
size_t got = f ? fread(head, 1, sizeof head - 1, f) : 0;
if (f)
fclose(f);
head[got] = 0;
char *cut = strstr(head, ",\"registers\"");
if (cut) {
*cut = 0;
/* the outcome is at the end of the file: read it back */
char outcome[32] = "pending";
f = fopen(path, "rb");
if (f) {
char tail[64] = "";
fseek(f, -40, SEEK_END);
size_t t = fread(tail, 1, sizeof tail - 1, f);
tail[t] = 0;
fclose(f);
json_field(tail, "outcome", outcome, sizeof outcome);
}
sb_printf(&s, "%s%s,\"outcome\":\"%s\"}", s.n > 1 ? "," : "", head, outcome);
}
free(names[i]);
}
sb_add(&s, "]", 1);
reply_sb(fd, &s);
}
static void api_crash(int fd, const char *id, bool remove)
{
if (!safe_id(id)) {
reply_text(fd, 404, "application/json", "{}");
return;
}
char path[256];
snprintf(path, sizeof path, CRASH_DIR "/%s.json", id);
if (remove) {
int r = unlink(path);
reply_text(fd, r == 0 ? 200 : 404, "application/json", r == 0 ? "{\"deleted\":true}" : "{}");
return;
}
size_t len;
char *json = read_file(path, &len, 64 << 20);
if (!json) {
reply_text(fd, 404, "application/json", "{}");
return;
}
reply(fd, 200, "application/json", json, len);
free(json);
}
/* The system's error dialogs, one JSON file each */
static void api_errors(int fd)
{
DIR *dir = opendir(ERROR_DIR);
sb_t s = {0};
sb_add(&s, "[", 1);
struct dirent *e;
while (dir && (e = readdir(dir))) {
size_t len = strlen(e->d_name);
if (len < 6 || strcmp(e->d_name + len - 5, ".json"))
continue;
char path[300];
snprintf(path, sizeof path, ERROR_DIR "/%s", e->d_name);
size_t n;
char *json = read_file(path, &n, 65536);
if (!json)
continue;
char *body = json;
if ((unsigned char)body[0] == 0xef && (unsigned char)body[1] == 0xbb && (unsigned char)body[2] == 0xbf)
body += 3;
while (n && (json[n - 1] == '\n' || json[n - 1] == '\r' || json[n - 1] == ' '))
json[--n] = 0;
if (*body == '{') {
sb_printf(&s, "%s{\"file\":\"%s\",\"entry\":%s}", s.n > 1 ? "," : "", e->d_name, body);
}
free(json);
}
if (dir)
closedir(dir);
sb_add(&s, "]", 1);
reply_sb(fd, &s);
}
static void serve(int fd)
{
char req[8192];
size_t got = 0;
struct timeval tv = {5, 0};
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
while (got < sizeof req - 1) {
ssize_t n = recv(fd, req + got, sizeof req - 1 - got, 0);
if (n <= 0)
break;
got += (size_t)n;
req[got] = 0;
if (strstr(req, "\r\n\r\n"))
break;
}
req[got] = 0;
char method[8] = "", target[1024] = "";
if (sscanf(req, "%7s %1023s", method, target) != 2) {
close(fd);
return;
}
char *query = strchr(target, '?');
if (query)
*query++ = 0;
else
query = "";
if (!strcmp(method, "OPTIONS"))
reply_text(fd, 200, "text/plain", "");
else if (!strcmp(target, "/") || !strcmp(target, "/index.html"))
reply(fd, 200, "text/html; charset=utf-8", dashboard_html, (size_t)(dashboard_html_end - dashboard_html));
else if (!strcmp(target, "/api/status"))
api_status(fd);
else if (!strcmp(target, "/api/memory"))
api_memory(fd, query);
else if (!strcmp(target, "/api/crashes"))
api_crashes(fd);
else if (!strncmp(target, "/api/crash/", 11))
api_crash(fd, target + 11, !strcmp(method, "DELETE"));
else if (!strcmp(target, "/api/errors"))
api_errors(fd);
else if (!strcmp(target, "/api/log")) {
size_t len;
char *log = read_file(LOG_PATH, &len, 4 << 20);
reply(fd, 200, "text/plain; charset=utf-8", log ? log : "", log ? len : 0);
free(log);
} else if (!strcmp(target, "/api/test/fault") && !strcmp(method, "POST")) {
pid_t pid = strstr(query, "pid=") ? atoi(strstr(query, "pid=") + 4) : -1;
pthread_mutex_lock(&g_lock);
int slot = find_slot(pid);
bool ok = slot >= 0 && g_titles[slot].traced && g_titles[slot].live;
pthread_mutex_unlock(&g_lock);
if (ok) {
g_fault_untraced = strstr(query, "untraced=1") != NULL;
g_fault_pid = pid;
kill(pid, SIGSTOP);
say("test fault requested for pid %d", pid);
}
reply_text(fd, ok ? 200 : 404, "application/json", ok ? "{\"fault\":true}" : "{\"error\":\"not an attached title\"}");
} else if (!strcmp(target, "/api/quit") && !strcmp(method, "POST")) {
detach_all(5000);
reply_text(fd, 200, "application/json", traced_count() ? "{\"quit\":true,\"still_attached\":true}" : "{\"quit\":true}");
} else
reply_text(fd, 404, "application/json", "{\"error\":\"not found\"}");
close(fd);
}
int main(void)
{
static const uint8_t all_caps[16] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
mkdir(ROOT_DIR, 0777);
mkdir(CRASH_DIR, 0777);
chmod(ROOT_DIR, 0777);
chmod(CRASH_DIR, 0777);
g_started = time(NULL);
signal(SIGPIPE, SIG_IGN);
static const int fatal[] = {SIGSEGV, SIGBUS, SIGILL, SIGFPE, SIGABRT, SIGSYS};
for (unsigned i = 0; i < sizeof fatal / sizeof *fatal; i++)
signal(fatal[i], payload_crashed);
int server = socket(AF_INET, SOCK_STREAM, 0);
int on = 1;
setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &on, sizeof on);
struct sockaddr_in address = {0};
address.sin_family = AF_INET;
address.sin_port = htons(DEVTOOLS_PORT);
address.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(server, (struct sockaddr *)&address, sizeof address) || listen(server, 8)) {
say("port %d is taken: is PS5 DevTools already running?", DEVTOOLS_PORT);
return 1;
}
const int authid = kernel_set_ucred_authid(getpid(), DEBUGGER_AUTHID);
const int caps = kernel_set_ucred_caps(getpid(), all_caps);
say("PS5 DevTools %s, pid %d, firmware %08x, port %d (authid %d caps %d)", DEVTOOLS_VERSION, getpid(),
kernel_get_fw_version(), DEVTOOLS_PORT, authid, caps);
notify("PS5 DevTools %s is running\nCrash reports and memory on port %d", DEVTOOLS_VERSION, DEVTOOLS_PORT);
pthread_t thread;
pthread_create(&thread, NULL, watcher, NULL);
while (!g_quit) {
int client = accept(server, NULL, NULL);
if (client >= 0)
serve(client);
}
detach_all(5000);
say("leaving, %d title(s) still attached", traced_count());
close(server);
return 0;
}