fix: validate process snapshots before listing or startup lookup

This commit is contained in:
najdek committed 2026-10-13 00:58:08 +02:00
1 parent 9bd3dfdd1d
commit 197c9d765b
11 files changed
+371 -86

No files matched your search

+13
View File
@@ -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
+1
View File
@@ -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
+6 -1
View File
@@ -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);
+48
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
+95
View File
@@ -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");
}
+6
View File
@@ -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
+22
View File
@@ -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
+4 -4
View File
@@ -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
+98
View File
@@ -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')