mirror of
https://github.com/itsPLK/ps5-payload-manager.git
synced 2026-10-19 02:19:53 -05:00
fix: validate process snapshots before listing or startup lookup
This commit is contained in:
1 parent
9bd3dfdd1d
commit
197c9d765b
11 files changed
+371
-86
No files matched your search
@@ -160,6 +160,19 @@ worker, socketpair loader recorder and real HTTP worker fixtures are part of
|
||||
status/reset/start paths. The PS-button detector remains the existing headless
|
||||
per-read implementation.
|
||||
|
||||
Startup lookup and process listing now share a bounded, validated sysctl snapshot.
|
||||
Record sizes must advance within the returned bytes, all consumed fields must
|
||||
fit, and names must terminate inside their SDK field. A malformed snapshot is
|
||||
rejected in full before exposing a PID/list. Queries retry ENOMEM up to three
|
||||
attempts and allocate at most 16 MiB. Failed listings become HTTP 500 through
|
||||
the existing response-error path. SDK assertions preserve the current PID/RSS/
|
||||
process-name offsets; offset 447 is `ki_comm`, rather than the startup code's
|
||||
previously misnamed `ki_tdname`. `python3 tests/run_process_tests.py` runs the
|
||||
actual listing/lookup with synthetic kernel bytes and sanitizers; `--baseline`
|
||||
reproduces the previous bounds reads and startup loop from `9ffd392`.
|
||||
|
||||
PS5 firmware behavior requires verification on the target console.
|
||||
|
||||
The autoload editor waits for both persisted autoload fields before showing
|
||||
controls or saving. The initial empty config, failed fetches, and incomplete
|
||||
responses cannot enable autoload or flush an empty list. Run
|
||||
|
||||
@@ -59,6 +59,7 @@ test-host:
|
||||
HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_storage_tests.py
|
||||
HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_autoload_tests.py
|
||||
HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_launcher_tests.py
|
||||
HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_process_tests.py
|
||||
HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_config_tests.py
|
||||
HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_http_tests.py --autoload-only
|
||||
HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_http_tests.py --config-only
|
||||
|
||||
@@ -2,11 +2,16 @@
|
||||
#define PROCESS_MGR_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
/* Build a JSON list of all running processes.
|
||||
* Returns complete bytes written, or 0 on serialization failure. */
|
||||
* Returns complete bytes written, or 0 on query/record/serialization failure. */
|
||||
size_t process_list_json(char *buf, size_t max_size);
|
||||
|
||||
/* Match the process name used by startup, excluding self/nonpositive PIDs.
|
||||
* Returns a matching PID, or -1 on no match/query/record failure. */
|
||||
pid_t process_find_pid(const char *name);
|
||||
|
||||
/* Kill a process by PID. Returns 0 on success, -1 on failure. */
|
||||
int process_kill(int pid);
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
#ifndef PROCESS_RECORDS_H
|
||||
#define PROCESS_RECORDS_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/proc.h>
|
||||
#include <sys/user.h>
|
||||
|
||||
/* Pin the SDK layout already used on hardware. Offset 447 is ki_comm, not
|
||||
* ki_tdname. A changed SDK must be checked before accepting a different ABI. */
|
||||
_Static_assert(offsetof(struct kinfo_proc, ki_pid) == 72, "check process PID ABI");
|
||||
_Static_assert(offsetof(struct kinfo_proc, ki_comm) == 447, "check process name ABI");
|
||||
_Static_assert(offsetof(struct kinfo_proc, ki_rssize) == 264, "check process RSS ABI");
|
||||
_Static_assert(sizeof(((struct kinfo_proc *)0)->ki_comm) == 20, "check process name size");
|
||||
_Static_assert(sizeof(((struct kinfo_proc *)0)->ki_pid) == 4, "check process PID size");
|
||||
_Static_assert(sizeof(((struct kinfo_proc *)0)->ki_rssize) == 8, "check process RSS size");
|
||||
_Static_assert(offsetof(struct kinfo_proc, ki_structsize) == 0 && sizeof(int) == 4,
|
||||
"check process size header ABI");
|
||||
|
||||
typedef struct {
|
||||
pid_t pid;
|
||||
int64_t rss_pages;
|
||||
char name[sizeof(((struct kinfo_proc *)0)->ki_comm)];
|
||||
} ProcessRecord;
|
||||
|
||||
/* Return 1 for one validated record, 0 at end, -1 for malformed bytes.
|
||||
* Copy fields instead of casting: the next variable-sized record may be
|
||||
* unaligned. Never advance until all consumed fields fit in this record. */
|
||||
static inline int process_record_next(const unsigned char **cursor,
|
||||
size_t *remaining, ProcessRecord *record) {
|
||||
if (!*remaining) return 0;
|
||||
if (*remaining < sizeof(int)) return -1;
|
||||
int size;
|
||||
memcpy(&size, *cursor, sizeof(size));
|
||||
size_t minimum = offsetof(struct kinfo_proc, ki_comm) + sizeof(record->name);
|
||||
if (size <= 0 || (size_t)size < minimum || (size_t)size > *remaining) return -1;
|
||||
memcpy(&record->pid, *cursor + offsetof(struct kinfo_proc, ki_pid), sizeof(record->pid));
|
||||
memcpy(&record->rss_pages, *cursor + offsetof(struct kinfo_proc, ki_rssize), sizeof(record->rss_pages));
|
||||
memcpy(record->name, *cursor + offsetof(struct kinfo_proc, ki_comm), sizeof(record->name));
|
||||
if (!memchr(record->name, '\0', sizeof(record->name)) || record->rss_pages < 0) return -1;
|
||||
*cursor += (size_t)size;
|
||||
*remaining -= (size_t)size;
|
||||
return 1;
|
||||
}
|
||||
|
||||
#endif
|
||||
+2
-39
@@ -25,6 +25,7 @@
|
||||
#include "slot.h"
|
||||
#include "ps5_launcher.h"
|
||||
#include "app_installer.h"
|
||||
#include "process_mgr.h"
|
||||
|
||||
#define DEFAULT_PORT MENU_PORT
|
||||
|
||||
@@ -35,44 +36,6 @@ static volatile sig_atomic_t resume_flag = 0;
|
||||
|
||||
static void handle_sigcont(int sig) { resume_flag = 1; }
|
||||
|
||||
static pid_t find_pid(const char *name) {
|
||||
int mib[4] = {1, 14, 8, 0};
|
||||
pid_t mypid = getpid();
|
||||
pid_t pid = -1;
|
||||
size_t buf_size;
|
||||
uint8_t *buf;
|
||||
|
||||
if (sysctl(mib, 4, 0, &buf_size, 0, 0)) {
|
||||
pldmgr_log("[PLDMGR] sysctl failed\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!(buf = malloc(buf_size))) {
|
||||
pldmgr_log("[PLDMGR] malloc failed\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (sysctl(mib, 4, buf, &buf_size, 0, 0)) {
|
||||
pldmgr_log("[PLDMGR] sysctl failed\n");
|
||||
free(buf);
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (uint8_t *ptr = buf; ptr < (buf + buf_size);) {
|
||||
int ki_structsize = *(int *)ptr;
|
||||
pid_t ki_pid = *(pid_t *)&ptr[72];
|
||||
char *ki_tdname = (char *)&ptr[447];
|
||||
|
||||
ptr += ki_structsize;
|
||||
if (!strcmp(name, ki_tdname) && ki_pid != mypid) {
|
||||
pid = ki_pid;
|
||||
}
|
||||
}
|
||||
|
||||
free(buf);
|
||||
return pid;
|
||||
}
|
||||
|
||||
/* PS5 System Calls (Internal) */
|
||||
extern int sceNetCtlInit();
|
||||
extern int sceUserServiceInitialize(void *);
|
||||
@@ -95,7 +58,7 @@ int main(int argc, char *argv[]) {
|
||||
|
||||
syscall(SYS_thr_set_name, -1, "pldmgr.elf");
|
||||
|
||||
while ((pid = find_pid("pldmgr.elf")) > 0) {
|
||||
while ((pid = process_find_pid("pldmgr.elf")) > 0) {
|
||||
if (kill(pid, SIGKILL)) {
|
||||
pldmgr_log("[PLDMGR] kill failed for pid %d\n", pid);
|
||||
break;
|
||||
|
||||
+76
-42
@@ -5,10 +5,10 @@
|
||||
#include <unistd.h>
|
||||
#include <signal.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/proc.h>
|
||||
#include <sys/user.h>
|
||||
#include <sys/sysctl.h>
|
||||
#include <stdint.h>
|
||||
#include <errno.h>
|
||||
#include "process_records.h"
|
||||
|
||||
#include "process_mgr.h"
|
||||
#include "json_helpers.h"
|
||||
@@ -40,51 +40,85 @@ static int is_user_daemon(const char *name, uint32_t app_id) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Query/fetch can race process creation. Retry ENOMEM a bounded number of
|
||||
* times, never parse a returned length beyond the allocation, and validate the
|
||||
* entire snapshot before exposing entries or returning a PID to startup. */
|
||||
#define PROCESS_SNAPSHOT_LIMIT (16u * 1024u * 1024u)
|
||||
static int process_snapshot(unsigned char **out, size_t *length) {
|
||||
int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PROC, 0};
|
||||
*out = NULL;
|
||||
*length = 0;
|
||||
for (int attempt = 0; attempt < 3; attempt++) {
|
||||
size_t capacity = 0;
|
||||
if (sysctl(mib, 4, NULL, &capacity, NULL, 0) != 0 ||
|
||||
capacity > PROCESS_SNAPSHOT_LIMIT) return -1;
|
||||
if (!capacity) return 0;
|
||||
unsigned char *data = malloc(capacity);
|
||||
if (!data) return -1;
|
||||
size_t used = capacity;
|
||||
int result = sysctl(mib, 4, data, &used, NULL, 0);
|
||||
int saved_errno = errno;
|
||||
if (result != 0 || used > capacity) {
|
||||
free(data);
|
||||
if (result != 0 && saved_errno == ENOMEM) continue;
|
||||
return -1;
|
||||
}
|
||||
const unsigned char *cursor = data;
|
||||
size_t remaining = used;
|
||||
ProcessRecord record;
|
||||
int next;
|
||||
while ((next = process_record_next(&cursor, &remaining, &record)) > 0) {}
|
||||
if (next < 0) { free(data); return -1; }
|
||||
*out = data;
|
||||
*length = used;
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
pid_t process_find_pid(const char *name) {
|
||||
if (!name) return -1;
|
||||
unsigned char *data;
|
||||
size_t remaining;
|
||||
if (process_snapshot(&data, &remaining) != 0) {
|
||||
pldmgr_log("[PLDMGR] Process snapshot failed\n");
|
||||
return -1;
|
||||
}
|
||||
const unsigned char *cursor = data;
|
||||
ProcessRecord record;
|
||||
pid_t found = -1, self = getpid();
|
||||
while (process_record_next(&cursor, &remaining, &record) > 0) {
|
||||
if (record.pid > 0 && record.pid != self && !strcmp(name, record.name)) found = record.pid;
|
||||
}
|
||||
free(data);
|
||||
return found;
|
||||
}
|
||||
|
||||
size_t process_list_json(char *buf, size_t max_size) {
|
||||
if (!buf || !max_size) return 0;
|
||||
int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PROC, 0};
|
||||
size_t buf_size = 0;
|
||||
void *sysctl_buf = NULL;
|
||||
|
||||
JsonListBuilder jb = { buf, max_size, 0, 1, 0 };
|
||||
buf[0] = '\0';
|
||||
|
||||
unsigned char *data;
|
||||
size_t remaining;
|
||||
if (process_snapshot(&data, &remaining) != 0) return 0;
|
||||
JsonListBuilder jb = { buf, max_size, 0, 1, 0 };
|
||||
json_append(&jb, "{\"processes\":[\n");
|
||||
|
||||
if (sysctl(mib, 4, NULL, &buf_size, NULL, 0) == 0) {
|
||||
sysctl_buf = malloc(buf_size);
|
||||
if (sysctl_buf) {
|
||||
if (sysctl(mib, 4, sysctl_buf, &buf_size, NULL, 0) == 0) {
|
||||
int count = 0;
|
||||
for (void *ptr = sysctl_buf; ptr < (sysctl_buf + buf_size);) {
|
||||
struct kinfo_proc *ki = (struct kinfo_proc*)ptr;
|
||||
if (ki->ki_structsize == 0) break;
|
||||
ptr += ki->ki_structsize;
|
||||
|
||||
app_info_t appinfo;
|
||||
if(sceKernelGetAppInfo(ki->ki_pid, &appinfo)) {
|
||||
memset(&appinfo, 0, sizeof(appinfo));
|
||||
}
|
||||
|
||||
int is_daemon = is_user_daemon(ki->ki_comm, appinfo.app_id);
|
||||
|
||||
char name_e[6 * 512];
|
||||
json_escape_checked(&jb, ki->ki_comm, name_e, sizeof(name_e));
|
||||
|
||||
double mem_mib = MiB(ki->ki_rssize * PAGE_SIZE);
|
||||
|
||||
if (json_append(&jb, "%s {\"pid\":%d,\"name\":\"%s\",\"memory\":%.1f,\"is_daemon\":%s}",
|
||||
(count > 0) ? ",\n" : "",
|
||||
(int)ki->ki_pid, name_e, mem_mib, is_daemon ? "true" : "false") != 0) {
|
||||
break;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
}
|
||||
free(sysctl_buf);
|
||||
}
|
||||
const unsigned char *cursor = data;
|
||||
ProcessRecord record;
|
||||
int count = 0;
|
||||
while (process_record_next(&cursor, &remaining, &record) > 0) {
|
||||
app_info_t appinfo = {0};
|
||||
if (sceKernelGetAppInfo(record.pid, &appinfo)) memset(&appinfo, 0, sizeof(appinfo));
|
||||
int is_daemon = is_user_daemon(record.name, appinfo.app_id);
|
||||
char name_e[6 * sizeof(record.name)];
|
||||
json_escape_checked(&jb, record.name, name_e, sizeof(name_e));
|
||||
/* Convert before multiplying, avoiding signed page-count overflow. */
|
||||
double mem_mib = MiB((double)record.rss_pages * PAGE_SIZE);
|
||||
if (json_append(&jb, "%s {\"pid\":%d,\"name\":\"%s\",\"memory\":%.1f,\"is_daemon\":%s}",
|
||||
count ? ",\n" : "", (int)record.pid, name_e, mem_mib,
|
||||
is_daemon ? "true" : "false") != 0) break;
|
||||
count++;
|
||||
}
|
||||
|
||||
free(data);
|
||||
json_append(&jb, "\n]}\n");
|
||||
return jb.pos;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include "process_mgr.h"
|
||||
#include "process_records.h"
|
||||
|
||||
static unsigned char bytes[4096];
|
||||
static size_t length;
|
||||
static const char *scenario;
|
||||
static int queries, fetches, app_calls, kills;
|
||||
|
||||
void pldmgr_log(const char *format, ...) { (void)format; }
|
||||
void *test_malloc(size_t size) { return !strcmp(scenario, "alloc-fail") ? NULL : malloc(size); }
|
||||
int test_kill(pid_t pid, int signal) { (void)pid; (void)signal; kills++; return 0; }
|
||||
int sceKernelGetAppInfo(pid_t pid, void *info) {
|
||||
app_calls++;
|
||||
*(uint32_t *)info = pid == 42 ? 1 : 0;
|
||||
return pid == 43 ? -1 : 0;
|
||||
}
|
||||
int sysctl(const int *mib, unsigned int count, void *out, size_t *size, const void *input, size_t input_size) {
|
||||
(void)input; (void)input_size;
|
||||
assert(count == 4 && mib[0] == 1 && mib[1] == 14 && mib[2] == 8 && mib[3] == 0);
|
||||
if (!out) {
|
||||
queries++;
|
||||
if (!strcmp(scenario, "query-fail")) { errno = EIO; return -1; }
|
||||
*size = !strcmp(scenario, "query-large") ? 16u * 1024u * 1024u + 1 : length;
|
||||
if (!strcmp(scenario, "grow") && queries == 1) *size = 4;
|
||||
return 0;
|
||||
}
|
||||
fetches++;
|
||||
if (!strcmp(scenario, "fetch-fail")) { errno = EIO; return -1; }
|
||||
if (!strcmp(scenario, "grow-always") || (!strcmp(scenario, "grow") && fetches == 1)) {
|
||||
*size = length + 100; errno = ENOMEM; return -1;
|
||||
}
|
||||
assert(*size >= length);
|
||||
memcpy(out, bytes, length);
|
||||
*size = length + (!strcmp(scenario, "returned-large") ? 1 : 0);
|
||||
return 0;
|
||||
}
|
||||
static void add_record(int size, pid_t pid, const char *name, int64_t rss) {
|
||||
assert(size >= 467 && length + (size_t)size <= sizeof(bytes));
|
||||
unsigned char *start = bytes + length;
|
||||
memcpy(start, &size, 4);
|
||||
memcpy(start + 72, &pid, 4);
|
||||
memcpy(start + 264, &rss, 8);
|
||||
memcpy(start + 447, name, strlen(name) + 1);
|
||||
length += (size_t)size;
|
||||
}
|
||||
int main(int argc, char **argv) {
|
||||
assert(argc == 2); scenario = argv[1];
|
||||
if (!strcmp(scenario, "empty")) length = 0;
|
||||
else if (!strcmp(scenario, "short-header")) length = 3;
|
||||
else if (!strcmp(scenario, "zero")) {
|
||||
add_record(467, 41, "pldmgr.elf", 0); memset(bytes, 0, 4);
|
||||
}
|
||||
else if (!strcmp(scenario, "negative") ||
|
||||
!strcmp(scenario, "short") || !strcmp(scenario, "short-list") || !strcmp(scenario, "oversized")) {
|
||||
int size = !strcmp(scenario, "zero") ? 0 : !strcmp(scenario, "negative") ? -1 :
|
||||
!strcmp(scenario, "oversized") ? INT_MAX : 4;
|
||||
memcpy(bytes, &size, 4); length = 4;
|
||||
} else {
|
||||
add_record(467, getpid(), "pldmgr.elf", 100);
|
||||
add_record(1088, 41, "pldmgr.elf", 1024);
|
||||
add_record(469, 42, "game.elf", 2048);
|
||||
add_record(467, 43, "mini-syscore.elf", 0);
|
||||
add_record(467, 44, "a\"\\\n.elf", 1);
|
||||
if (!strcmp(scenario, "unterminated")) memset(bytes + 447, 'X', 20);
|
||||
if (!strcmp(scenario, "negative-rss")) { int64_t rss = -1; memcpy(bytes + 264, &rss, 8); }
|
||||
if (!strcmp(scenario, "huge-rss")) { int64_t rss = INT64_MAX; memcpy(bytes + 264, &rss, 8); }
|
||||
if (!strcmp(scenario, "max-name")) memcpy(bytes + 447, "abcdefghijklmno.elf", 20);
|
||||
if (!strcmp(scenario, "trailing")) bytes[length++] = 1;
|
||||
if (!strcmp(scenario, "bad-after-match")) { memset(bytes + length, 0, 4); length += 4; }
|
||||
if (!strcmp(scenario, "nonpositive")) { pid_t pid = -5; memcpy(bytes + 467 + 72, &pid, 4); }
|
||||
if (!strcmp(scenario, "thread-only")) {
|
||||
memcpy(bytes + 467 + 394, "pldmgr.elf", 11);
|
||||
memcpy(bytes + 467 + 447, "other.elf\0", 10);
|
||||
}
|
||||
}
|
||||
pid_t found = !strcmp(scenario, "short-list") ? -1 : process_find_pid("pldmgr.elf");
|
||||
int lookup_queries = queries, lookup_fetches = fetches;
|
||||
queries = fetches = 0;
|
||||
char output[8192]; memset(output, 'X', sizeof(output));
|
||||
size_t used = process_list_json(output, !strcmp(scenario, "small-output") ? 32 : sizeof(output));
|
||||
assert(used ? output[used] == '\0' : output[0] == '\0');
|
||||
assert(process_kill(0) == -1 && process_kill(-1) == -1 && process_kill(getpid()) == -1 && kills == 0);
|
||||
assert(process_kill(999999) == 0 && kills == 1);
|
||||
printf("{\"found\":%d,\"length\":%zu,\"queries\":%d,\"fetches\":%d,\"lookup_queries\":%d,\"lookup_fetches\":%d,\"app_calls\":%d,\"data\":%s}\n",
|
||||
(int)found, used, queries, fetches, lookup_queries, lookup_fetches, app_calls, used ? output : "null");
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#include <stdlib.h>
|
||||
#include <sys/types.h>
|
||||
void *test_malloc(size_t size);
|
||||
int test_kill(pid_t pid, int signal);
|
||||
#define malloc test_malloc
|
||||
#define kill test_kill
|
||||
@@ -0,0 +1,22 @@
|
||||
#ifndef TEST_PROCESS_SDK_STUB_H
|
||||
#define TEST_PROCESS_SDK_STUB_H
|
||||
#if defined(__linux__)
|
||||
#include_next <sys/user.h>
|
||||
#endif
|
||||
#include <stdint.h>
|
||||
#include <sys/types.h>
|
||||
/* Measured PS5 target SDK layout, not the host operating system's ABI.
|
||||
* Production static assertions check these offsets against the real SDK. */
|
||||
struct kinfo_proc {
|
||||
int ki_structsize;
|
||||
char pad0[68];
|
||||
pid_t ki_pid;
|
||||
char pad1[188];
|
||||
int64_t ki_rssize;
|
||||
char pad2[122];
|
||||
char ki_tdname[17];
|
||||
char pad3[36];
|
||||
char ki_comm[20];
|
||||
char tail[621];
|
||||
};
|
||||
#endif
|
||||
@@ -29,10 +29,10 @@ def main():
|
||||
(build / "pldmgr.h").write_text((ROOT / "include/pldmgr.h").read_text().replace("/data/pldmgr", str(storage)))
|
||||
(build / "assets_cacert_pem.h").write_text("static const unsigned char assets_cacert_pem[] = {0};\n"
|
||||
"static const unsigned int assets_cacert_pem_len = 0;\n")
|
||||
# Recording kernel fixture; verifies serialization, not the PS5 ABI.
|
||||
# Recording kernel fixture using the SDK-sized names/field offsets.
|
||||
(build / "sys").mkdir()
|
||||
(build / "sys/proc.h").write_text("")
|
||||
(build / "sys/user.h").write_text("#pragma once\n#if defined(__linux__)\n#include_next <sys/user.h>\n#endif\n#include <sys/types.h>\nstruct kinfo_proc { int ki_structsize; pid_t ki_pid; char ki_comm[64]; unsigned long ki_rssize; };\n")
|
||||
(build / "sys/user.h").write_text((ROOT / "tests/process_sdk_stub.h").read_text())
|
||||
(build / "sys/sysctl.h").write_text("#include <stddef.h>\n#define CTL_KERN 1\n#define KERN_PROC 2\n#define KERN_PROC_PROC 3\n#define PAGE_SIZE 4096\nint sysctl(const int *, unsigned, void *, size_t *, const void *, size_t);\n")
|
||||
(build / "kernel.c").write_text('''#include <string.h>
|
||||
#include <stdlib.h>
|
||||
@@ -43,7 +43,7 @@ int sysctl(const int *mib, unsigned n, void *out, size_t *size, const void *inpu
|
||||
if (out) for (int i = 0; i < 1000; i++) {
|
||||
struct kinfo_proc *record = (struct kinfo_proc *)out + i;
|
||||
record->ki_structsize = sizeof(*record); record->ki_pid = i + 1; record->ki_rssize = 1024;
|
||||
memset(record->ki_comm, 1, 63); record->ki_comm[63] = 0;
|
||||
memset(record->ki_comm, 1, 19); record->ki_comm[19] = 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -125,7 +125,7 @@ int download_to_file_limited(const char *url, const char *path, size_t limit) {
|
||||
assert all(item["name"] == "\x01" * 250 and item["description"] == "\x01" * 1000 for item in document["items"])
|
||||
elif mode == "processes":
|
||||
assert len(document["processes"]) == 1000
|
||||
assert all(item["name"] == "\x01" * 63 for item in document["processes"])
|
||||
assert all(item["name"] == "\x01" * 19 for item in document["processes"])
|
||||
elif mode == "repository":
|
||||
items = document["sources"][0]["payloads"]
|
||||
assert len(items) == 30
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Actual process listing/startup lookup with synthetic SDK-layout sysctl bytes."""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--baseline', action='store_true')
|
||||
args = parser.parse_args()
|
||||
os.environ.setdefault('ASAN_OPTIONS', 'detect_leaks=0:halt_on_error=1' if sys.platform == 'darwin' else 'detect_leaks=1:halt_on_error=1')
|
||||
os.environ.setdefault('UBSAN_OPTIONS', 'halt_on_error=1:print_stacktrace=1')
|
||||
with tempfile.TemporaryDirectory(prefix='pldmgr-process-') as temp:
|
||||
build = Path(temp)
|
||||
(build / 'sys').mkdir()
|
||||
# Measured with the PS5 target compiler. Production asserts these same
|
||||
# offsets against the actual SDK; this shim only supplies host types.
|
||||
(build / 'sys/proc.h').write_text('#include <stdint.h>\n#define PAGE_SIZE 16384\n')
|
||||
(build / 'sys/user.h').write_text((ROOT / 'tests/process_sdk_stub.h').read_text())
|
||||
(build / 'sys/sysctl.h').write_text('''#include <stddef.h>
|
||||
#define CTL_KERN 1
|
||||
#define KERN_PROC 14
|
||||
#define KERN_PROC_PROC 8
|
||||
int sysctl(const int *, unsigned int, void *, size_t *, const void *, size_t);
|
||||
''')
|
||||
flags = shlex.split(os.environ.get('HOST_CC', 'clang')) + [
|
||||
'-std=c11', '-D_DEFAULT_SOURCE', '-g', '-O1', '-Wall', '-Wextra',
|
||||
'-I' + str(build), '-Iinclude', '-fsanitize=address,undefined', '-fno-omit-frame-pointer']
|
||||
sources = ['src/process_mgr.c']
|
||||
if args.baseline:
|
||||
def previous(path):
|
||||
return subprocess.check_output(['git', 'show', '9ffd392:' + path], cwd=ROOT, text=True)
|
||||
old = build / 'process_old.c'
|
||||
old.write_text(previous('src/process_mgr.c'))
|
||||
main = previous('src/main.c')
|
||||
function = main[main.index('static pid_t find_pid('):main.index('/* PS5 System Calls')]
|
||||
startup = build / 'startup_old.c'
|
||||
startup.write_text('#include <stdint.h>\n#include <stdlib.h>\n#include <string.h>\n#include <unistd.h>\n#include <sys/sysctl.h>\n#include "pldmgr.h"\n' +
|
||||
function.replace('static pid_t find_pid(', 'pid_t process_find_pid('))
|
||||
sources = [str(old), str(startup)]
|
||||
objects = []
|
||||
for index, source in enumerate(sources):
|
||||
obj = build / f'process-{index}.o'
|
||||
subprocess.run(flags + ['-include', 'tests/process_hooks.h', '-c', source, '-o', str(obj)], cwd=ROOT, check=True)
|
||||
objects.append(str(obj))
|
||||
binary = build / 'process_tests'
|
||||
subprocess.run(flags + objects + ['src/json_helpers.c', 'src/utils.c', 'tests/process_driver.c', '-o', str(binary)], cwd=ROOT, check=True)
|
||||
if args.baseline:
|
||||
result = subprocess.run([str(binary), 'short'], capture_output=True, text=True, timeout=5)
|
||||
assert result.returncode != 0 and ('AddressSanitizer' in result.stderr or 'runtime error' in result.stderr), result.stderr
|
||||
print('Baseline: startup lookup reads beyond a four-byte record (sanitizer reproduced)')
|
||||
result = subprocess.run([str(binary), 'short-list'], capture_output=True, text=True, timeout=5)
|
||||
assert result.returncode != 0 and ('AddressSanitizer' in result.stderr or 'runtime error' in result.stderr), result.stderr
|
||||
print('Baseline: process listing reads beyond a four-byte record (sanitizer reproduced)')
|
||||
try:
|
||||
subprocess.run([str(binary), 'zero'], capture_output=True, text=True, timeout=2)
|
||||
except subprocess.TimeoutExpired:
|
||||
print('Baseline: startup zero-size record loops indefinitely (timeout reproduced)')
|
||||
else:
|
||||
raise AssertionError('baseline zero-size startup scan did not loop')
|
||||
else:
|
||||
errors = ['short-header', 'zero', 'negative', 'short', 'oversized', 'unterminated',
|
||||
'negative-rss', 'trailing', 'bad-after-match', 'query-fail', 'fetch-fail',
|
||||
'query-large', 'returned-large', 'alloc-fail', 'grow-always']
|
||||
valid = ['normal', 'empty', 'grow', 'huge-rss', 'nonpositive', 'thread-only', 'max-name', 'small-output']
|
||||
for scenario in errors + valid:
|
||||
result = subprocess.run([str(binary), scenario], capture_output=True, text=True, timeout=5)
|
||||
assert result.returncode == 0, scenario + ': ' + result.stderr
|
||||
actual = json.loads(result.stdout)
|
||||
if scenario in errors:
|
||||
assert actual['found'] == -1 and actual['length'] == 0 and actual['app_calls'] == 0, (scenario, actual)
|
||||
else:
|
||||
assert actual['found'] == (-1 if scenario in ['empty', 'nonpositive', 'thread-only'] else 41), (scenario, actual)
|
||||
if scenario == 'small-output':
|
||||
assert actual['length'] == 0, actual
|
||||
else:
|
||||
entries = actual['data']['processes']
|
||||
assert len(entries) == (0 if scenario == 'empty' else 5), actual
|
||||
if entries:
|
||||
if scenario == 'max-name':
|
||||
assert entries[0]['name'] == 'abcdefghijklmno.elf', actual
|
||||
if scenario == 'huge-rss':
|
||||
assert entries[0]['memory'] > 0, actual
|
||||
assert entries[1]['pid'] == ( -5 if scenario == 'nonpositive' else 41), actual
|
||||
assert entries[1]['memory'] == 16.0, actual
|
||||
assert entries[1]['is_daemon'] is True, actual
|
||||
assert entries[2]['is_daemon'] is False and entries[3]['is_daemon'] is False, actual
|
||||
assert entries[4]['name'] == 'a"\\\n.elf' and entries[4]['is_daemon'] is True, actual
|
||||
attempts = 3 if scenario == 'grow-always' else 2 if scenario == 'grow' else 1
|
||||
assert actual['queries'] == actual['lookup_queries'] == attempts, (scenario, actual)
|
||||
assert actual['fetches'] <= attempts and actual['lookup_fetches'] <= attempts, (scenario, actual)
|
||||
print('Process records:', scenario, 'passed')
|
||||
print(f'Process records: {len(errors) + len(valid)} sanitizer scenarios passed')
|
||||
Reference in new issue
Block a user