mirror of
https://github.com/griches/PS5_DevTools.git
synced 2026-10-25 09:00:55 +01:00
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
1582 lines
56 KiB
C
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;
|
|
}
|