mirror of
https://github.com/itsPLK/ps5-payload-manager.git
synced 2026-10-19 02:19:53 -05:00
fix: bound collection growth and propagate allocation failures
This commit is contained in:
1 parent
45be5c6cd7
commit
5c04204f97
11 files changed
+372
-54
No files matched your search
+14
-1
@@ -84,7 +84,7 @@ UTF-8-safe prefixes. The backend suite uses real repository, source save/load,
|
||||
and slot metadata read/write code inside temporary storage. It parses their
|
||||
results independently with Python. Unused network/PS5 code is discarded at
|
||||
link time, and no download or payload launch occurs. `make test-host` runs all
|
||||
five host suites with sanitizers.
|
||||
six host suites with sanitizers.
|
||||
|
||||
The serializer suite exercises actual history/log/payload/process/repository
|
||||
output at small and large capacities, sixfold escape expansion, failed format
|
||||
@@ -94,6 +94,19 @@ aborts if called. Process records use a host fixture; this checks serialization,
|
||||
not the PS5 kernel ABI. The existing unused SSE `pos` parameter produces a host
|
||||
`-Wextra` warning; the native build has no new warnings.
|
||||
|
||||
The allocation suite executes actual repository parsing, managed-slot discovery,
|
||||
cached-source serialization, and all USB discovery branches. It faults every
|
||||
observed growth/read/path-copy allocation in its current fixtures and verifies
|
||||
that failure clears results and releases partial collections. USB paths are
|
||||
redirected to temporary fixtures using `PLDMGR_USB_ROOT_FORMAT`; production
|
||||
defaults remain `/mnt/usb%d`. No fixture payload is launched.
|
||||
|
||||
Collected arrays are limited to **4,096 items** and **64 MiB** of allocated array
|
||||
capacity, whichever is smaller for the item type. Repository parsing accepts up
|
||||
to **16 MiB** of input text. These bounds apply to the parser/scanner collections;
|
||||
they do not impose download-stream limits or an aggregate daemon memory budget.
|
||||
Collection failures become explicit errors rather than successful partial lists.
|
||||
|
||||
`.github/workflows/verify.yml` runs all host suites, existing frontend tests,
|
||||
and frontend build without publishing or deploying. Frontend lint still reports existing errors. The host suite tests response
|
||||
ownership/status handling, request cleanup, and autoload-list bounds. Storage
|
||||
|
||||
@@ -54,6 +54,7 @@ test-host:
|
||||
HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_json_decode_tests.py
|
||||
HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_json_backend_tests.py
|
||||
HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_json_size_tests.py
|
||||
HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_allocation_tests.py
|
||||
HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_http_tests.py
|
||||
|
||||
test-frontend:
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
#ifndef COLLECTION_HELPERS_H
|
||||
#define COLLECTION_HELPERS_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#define COLLECTION_MAX_ITEMS ((size_t)4096)
|
||||
#define COLLECTION_MAX_BYTES ((size_t)64 * 1024 * 1024)
|
||||
|
||||
/* Bound each collected array, including unused geometric capacity. */
|
||||
static inline size_t collection_limit(size_t item_size) {
|
||||
if (!item_size) return 0;
|
||||
size_t limit = COLLECTION_MAX_BYTES / item_size;
|
||||
return limit < COLLECTION_MAX_ITEMS ? limit : COLLECTION_MAX_ITEMS;
|
||||
}
|
||||
|
||||
/* Neither pointer nor capacity changes on allocation/limit failure. */
|
||||
static inline void *collection_reserve(void *items, size_t *capacity,
|
||||
size_t needed, size_t item_size) {
|
||||
size_t limit = collection_limit(item_size);
|
||||
if (needed > limit) return NULL;
|
||||
if (needed <= *capacity) return items;
|
||||
size_t next = *capacity ? *capacity : (limit < 32 ? limit : 32);
|
||||
while (next < needed) next = next > limit / 2 ? limit : next * 2;
|
||||
void *grown = realloc(items, next * item_size);
|
||||
if (!grown) return NULL;
|
||||
*capacity = next;
|
||||
return grown;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <stdarg.h>
|
||||
#include <ctype.h>
|
||||
#include <stdint.h>
|
||||
#include <errno.h>
|
||||
#include <sys/stat.h>
|
||||
#include "json_helpers.h"
|
||||
#include "utils.h"
|
||||
@@ -318,6 +319,7 @@ int read_file_text(const char *path, char **out_buf, size_t *out_size) {
|
||||
buf = (char *)malloc((size_t)fsize + 1);
|
||||
if (!buf) {
|
||||
fclose(f);
|
||||
errno = ENOMEM;
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
+49
-27
@@ -17,6 +17,12 @@
|
||||
#include "pldmgr.h"
|
||||
#include "autoload.h"
|
||||
#include "utils.h"
|
||||
#include "collection_helpers.h"
|
||||
#include <errno.h>
|
||||
|
||||
#ifndef PLDMGR_USB_ROOT_FORMAT
|
||||
#define PLDMGR_USB_ROOT_FORMAT "/mnt/usb%d"
|
||||
#endif
|
||||
|
||||
/* ── Extension / path checks ──────────────────────────────── */
|
||||
|
||||
@@ -61,9 +67,19 @@ static int copy_file(const char *src, const char *dst) {
|
||||
|
||||
/* ── USB Scanning (Unmanaged, run-in-place) ─────────────────── */
|
||||
|
||||
static void scan_usb_dir(const char *dir_path, char ***out_paths, int *count, int *cap) {
|
||||
static int append_usb_path(char ***paths, size_t *count, size_t *capacity, const char *path) {
|
||||
char **grown = collection_reserve(*paths, capacity, *count + 1, sizeof(char *));
|
||||
if (!grown) return -1;
|
||||
*paths = grown;
|
||||
char *copy = strdup(path);
|
||||
if (!copy) return -1;
|
||||
(*paths)[(*count)++] = copy;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int scan_usb_dir(const char *dir_path, char ***out_paths, size_t *count, size_t *cap) {
|
||||
DIR *d = opendir(dir_path);
|
||||
if (!d) return;
|
||||
if (!d) return errno == ENOENT ? 0 : -1;
|
||||
|
||||
struct dirent *ent;
|
||||
while ((ent = readdir(d)) != NULL) {
|
||||
@@ -85,26 +101,19 @@ static void scan_usb_dir(const char *dir_path, char ***out_paths, int *count, in
|
||||
if (is_supported_extension(sub_ent->d_name)) {
|
||||
char sub_full[640];
|
||||
snprintf(sub_full, sizeof(sub_full), "%s/%s", full, sub_ent->d_name);
|
||||
if (*count >= *cap) {
|
||||
*cap = (*cap == 0) ? 32 : (*cap * 2);
|
||||
*out_paths = realloc(*out_paths, *cap * sizeof(char *));
|
||||
if (append_usb_path(out_paths, count, cap, sub_full) != 0) {
|
||||
closedir(sub); closedir(d); return -1;
|
||||
}
|
||||
(*out_paths)[*count] = strdup(sub_full);
|
||||
(*count)++;
|
||||
}
|
||||
}
|
||||
closedir(sub);
|
||||
}
|
||||
} else if (is_supported_extension(ent->d_name)) {
|
||||
if (*count >= *cap) {
|
||||
*cap = (*cap == 0) ? 32 : (*cap * 2);
|
||||
*out_paths = realloc(*out_paths, *cap * sizeof(char *));
|
||||
}
|
||||
(*out_paths)[*count] = strdup(full);
|
||||
(*count)++;
|
||||
if (append_usb_path(out_paths, count, cap, full) != 0) { closedir(d); return -1; }
|
||||
}
|
||||
}
|
||||
closedir(d);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ── Listing API ───────────────────────────────────────────── */
|
||||
@@ -113,21 +122,22 @@ size_t payload_mgr_list_json(char *json_buf, size_t buf_size) {
|
||||
if (!json_buf || !buf_size) return 0;
|
||||
SlotMeta *slots = NULL;
|
||||
size_t slot_count = 0;
|
||||
slot_list_all(&slots, &slot_count);
|
||||
json_buf[0] = '\0';
|
||||
if (slot_list_all(&slots, &slot_count) != 0) return 0;
|
||||
|
||||
/* Enumerate USB payloads */
|
||||
char **usb_paths = NULL;
|
||||
int usb_count = 0, usb_cap = 0;
|
||||
size_t usb_count = 0, usb_cap = 0;
|
||||
int scan_usb = config_read_bool("SCAN_USB_PAYLOADS", 0);
|
||||
|
||||
for (int u = 0; u < 8; u++) {
|
||||
char pldmgr_usb[64];
|
||||
snprintf(pldmgr_usb, sizeof(pldmgr_usb), "/mnt/usb%d/pldmgr", u);
|
||||
scan_usb_dir(pldmgr_usb, &usb_paths, &usb_count, &usb_cap);
|
||||
snprintf(pldmgr_usb, sizeof(pldmgr_usb), PLDMGR_USB_ROOT_FORMAT "/pldmgr", u);
|
||||
if (scan_usb_dir(pldmgr_usb, &usb_paths, &usb_count, &usb_cap) != 0) goto scan_failed;
|
||||
|
||||
if (scan_usb) {
|
||||
char root_usb[64];
|
||||
snprintf(root_usb, sizeof(root_usb), "/mnt/usb%d", u);
|
||||
snprintf(root_usb, sizeof(root_usb), PLDMGR_USB_ROOT_FORMAT, u);
|
||||
DIR *d = opendir(root_usb);
|
||||
if (d) {
|
||||
struct dirent *ent;
|
||||
@@ -136,11 +146,9 @@ size_t payload_mgr_list_json(char *json_buf, size_t buf_size) {
|
||||
if (is_supported_extension(ent->d_name)) {
|
||||
char full[512];
|
||||
snprintf(full, sizeof(full), "%s/%s", root_usb, ent->d_name);
|
||||
if (usb_count >= usb_cap) {
|
||||
usb_cap = (usb_cap == 0) ? 32 : (usb_cap * 2);
|
||||
usb_paths = realloc(usb_paths, usb_cap * sizeof(char *));
|
||||
if (append_usb_path(&usb_paths, &usb_count, &usb_cap, full) != 0) {
|
||||
closedir(d); goto scan_failed;
|
||||
}
|
||||
usb_paths[usb_count++] = strdup(full);
|
||||
}
|
||||
}
|
||||
closedir(d);
|
||||
@@ -218,7 +226,7 @@ size_t payload_mgr_list_json(char *json_buf, size_t buf_size) {
|
||||
}
|
||||
|
||||
json_append(&jb, " ],\n \"usb\": [\n");
|
||||
for (int u = 0; u < usb_count; u++) {
|
||||
for (size_t u = 0; u < usb_count; u++) {
|
||||
char u_e[6 * 512];
|
||||
json_escape_checked(&jb, usb_paths[u], u_e, sizeof(u_e));
|
||||
json_append(&jb, " \"%s\"%s\n", u_e, (u + 1 < usb_count) ? "," : "");
|
||||
@@ -234,7 +242,7 @@ size_t payload_mgr_list_json(char *json_buf, size_t buf_size) {
|
||||
curr_p++;
|
||||
json_append(&jb, " \"%s\"%s\n", pp_e, (curr_p < total_p) ? "," : "");
|
||||
}
|
||||
for (int u = 0; u < usb_count; u++) {
|
||||
for (size_t u = 0; u < usb_count; u++) {
|
||||
char u_e[6 * 512];
|
||||
json_escape_checked(&jb, usb_paths[u], u_e, sizeof(u_e));
|
||||
curr_p++;
|
||||
@@ -248,10 +256,16 @@ size_t payload_mgr_list_json(char *json_buf, size_t buf_size) {
|
||||
}
|
||||
|
||||
slot_free_all(slots);
|
||||
for (int u = 0; u < usb_count; u++) free(usb_paths[u]);
|
||||
for (size_t u = 0; u < usb_count; u++) free(usb_paths[u]);
|
||||
if (usb_paths) free(usb_paths);
|
||||
|
||||
return jb.pos;
|
||||
scan_failed:
|
||||
slot_free_all(slots);
|
||||
for (size_t u = 0; u < usb_count; u++) free(usb_paths[u]);
|
||||
free(usb_paths);
|
||||
json_buf[0] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
int payload_mgr_delete_payload(const char *slot_or_filename) {
|
||||
@@ -315,7 +329,11 @@ int payload_mgr_check_existing(const char *filename, char *out_json, size_t out_
|
||||
snprintf(check_dir, sizeof(check_dir), "%s/local/%s", PAYLOADS_STORAGE_DIR, lkey);
|
||||
|
||||
SlotMeta meta;
|
||||
int exists = (slot_read_meta(check_dir, &meta) == 0 && meta.payload_path[0] != '\0');
|
||||
if (slot_read_meta(check_dir, &meta) != 0) {
|
||||
json_append(&jb, "{\"error\":\"Failed to read slot metadata\"}");
|
||||
return -1;
|
||||
}
|
||||
int exists = meta.payload_path[0] != '\0';
|
||||
|
||||
char key_e[6 * 128];
|
||||
json_escape_checked(&jb, lkey, key_e, sizeof(key_e));
|
||||
@@ -440,7 +458,11 @@ int payload_mgr_usb_check(const char *usb_path, char *out_json, size_t out_size)
|
||||
snprintf(check_dir, sizeof(check_dir), "%s/local/%s", PAYLOADS_STORAGE_DIR, lkey);
|
||||
|
||||
SlotMeta meta;
|
||||
int exists = (slot_read_meta(check_dir, &meta) == 0 && meta.payload_path[0] != '\0');
|
||||
if (slot_read_meta(check_dir, &meta) != 0) {
|
||||
json_append(&jb, "{\"error\":\"Failed to read slot metadata\"}");
|
||||
return -1;
|
||||
}
|
||||
int exists = meta.payload_path[0] != '\0';
|
||||
|
||||
char name_e[6 * 256], path_e[6 * 512], key_e[6 * 128];
|
||||
json_escape_checked(&jb, filename, name_e, sizeof(name_e));
|
||||
|
||||
+8
-12
@@ -8,6 +8,7 @@
|
||||
#include <curl/curl.h>
|
||||
|
||||
#include "repository.h"
|
||||
#include "collection_helpers.h"
|
||||
#include "sources.h"
|
||||
#include "slot.h"
|
||||
#include "json_helpers.h"
|
||||
@@ -160,11 +161,13 @@ static const char *find_next_object_start(const char *p) {
|
||||
int parse_repository_payloads(const char *json, RepoPayload **out_items, size_t *out_count) {
|
||||
const char *p;
|
||||
RepoPayload *items = NULL;
|
||||
size_t count = 0;
|
||||
size_t count = 0, capacity = 0;
|
||||
|
||||
if (!out_items || !out_count) return -1;
|
||||
*out_items = NULL;
|
||||
*out_count = 0;
|
||||
|
||||
if (!json || strnlen(json, 16 * 1024 * 1024 + 1) > 16 * 1024 * 1024) return -1;
|
||||
p = skip_top_level_wrapper(json);
|
||||
if (!p) return 0;
|
||||
|
||||
@@ -185,19 +188,15 @@ int parse_repository_payloads(const char *json, RepoPayload **out_items, size_t
|
||||
if (depth > 0) end++;
|
||||
}
|
||||
|
||||
if (!*end && depth > 0) break;
|
||||
if (!*end && depth > 0) { free(items); return -1; }
|
||||
|
||||
RepoPayload *item = (RepoPayload *)malloc(sizeof(RepoPayload));
|
||||
if (!item) {
|
||||
free(items);
|
||||
return -1;
|
||||
}
|
||||
RepoPayload entry;
|
||||
RepoPayload *item = &entry;
|
||||
|
||||
memset(item, 0, sizeof(*item));
|
||||
if (json_extract_string(p, end + 1, "name", item->name, sizeof(item->name)) != 0 ||
|
||||
json_extract_string(p, end + 1, "filename", item->filename, sizeof(item->filename)) != 0 ||
|
||||
json_extract_string(p, end + 1, "url", item->url, sizeof(item->url)) != 0) {
|
||||
free(item);
|
||||
p = end + 1;
|
||||
continue;
|
||||
}
|
||||
@@ -217,7 +216,6 @@ int parse_repository_payloads(const char *json, RepoPayload **out_items, size_t
|
||||
/* A supplied oversized/non-string hash must not turn into an absent,
|
||||
* optional checksum after extraction fails. Hex validation is separate. */
|
||||
if (checksum_result != JSON_STRING_OK && checksum_result != JSON_STRING_MISSING) {
|
||||
free(item);
|
||||
p = end + 1;
|
||||
continue;
|
||||
}
|
||||
@@ -244,15 +242,13 @@ int parse_repository_payloads(const char *json, RepoPayload **out_items, size_t
|
||||
strncpy(item->category, "Uncategorized", sizeof(item->category) - 1);
|
||||
}
|
||||
|
||||
RepoPayload *tmp = (RepoPayload *)realloc(items, (count + 1) * sizeof(RepoPayload));
|
||||
RepoPayload *tmp = (RepoPayload *)collection_reserve(items, &capacity, count + 1, sizeof(RepoPayload));
|
||||
if (!tmp) {
|
||||
free(item);
|
||||
free(items);
|
||||
return -1;
|
||||
}
|
||||
items = tmp;
|
||||
items[count] = *item;
|
||||
free(item);
|
||||
count++;
|
||||
p = end + 1;
|
||||
}
|
||||
|
||||
+20
-8
@@ -3,12 +3,14 @@
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <dirent.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "slot.h"
|
||||
#include "collection_helpers.h"
|
||||
#include "sha256.h"
|
||||
#include "json_helpers.h"
|
||||
#include "pldmgr.h"
|
||||
@@ -203,7 +205,9 @@ int slot_read_meta(const char *slot_dir, SlotMeta *out_meta) {
|
||||
|
||||
char *json = NULL;
|
||||
size_t size = 0;
|
||||
if (read_file_text(meta_path, &json, &size) == 0 && json && size > 0) {
|
||||
int read_result = read_file_text(meta_path, &json, &size);
|
||||
if (read_result != 0 && errno != ENOENT) return -1;
|
||||
if (read_result == 0 && json && size > 0) {
|
||||
const char *end = json + size;
|
||||
char schema_str[16] = "";
|
||||
json_extract_string(json, end, "schema", schema_str, sizeof(schema_str));
|
||||
@@ -377,9 +381,9 @@ int slot_write_meta(const char *slot_dir, const SlotMeta *meta) {
|
||||
return rename(tmp_path, final_path);
|
||||
}
|
||||
|
||||
static void scan_slot_subdir(const char *parent_dir, SlotMeta **out_slots, size_t *out_count) {
|
||||
static int scan_slot_subdir(const char *parent_dir, SlotMeta **out_slots, size_t *out_count, size_t *capacity) {
|
||||
DIR *d = opendir(parent_dir);
|
||||
if (!d) return;
|
||||
if (!d) return errno == ENOENT ? 0 : -1;
|
||||
|
||||
struct dirent *ent;
|
||||
while ((ent = readdir(d)) != NULL) {
|
||||
@@ -392,15 +396,17 @@ static void scan_slot_subdir(const char *parent_dir, SlotMeta **out_slots, size_
|
||||
if (stat(sub_path, &st) != 0 || !S_ISDIR(st.st_mode)) continue;
|
||||
|
||||
SlotMeta meta;
|
||||
if (slot_read_meta(sub_path, &meta) == 0 && meta.payload_path[0] != '\0') {
|
||||
SlotMeta *tmp = realloc(*out_slots, (*out_count + 1) * sizeof(SlotMeta));
|
||||
if (!tmp) break;
|
||||
if (slot_read_meta(sub_path, &meta) != 0) { closedir(d); return -1; }
|
||||
if (meta.payload_path[0] != '\0') {
|
||||
SlotMeta *tmp = collection_reserve(*out_slots, capacity, *out_count + 1, sizeof(SlotMeta));
|
||||
if (!tmp) { closedir(d); return -1; }
|
||||
*out_slots = tmp;
|
||||
(*out_slots)[*out_count] = meta;
|
||||
(*out_count)++;
|
||||
}
|
||||
}
|
||||
closedir(d);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int slot_list_all(SlotMeta **out_slots, size_t *out_count) {
|
||||
@@ -412,8 +418,14 @@ int slot_list_all(SlotMeta **out_slots, size_t *out_count) {
|
||||
snprintf(repo_dir, sizeof(repo_dir), "%s/repo", PAYLOADS_STORAGE_DIR);
|
||||
snprintf(local_dir, sizeof(local_dir), "%s/local", PAYLOADS_STORAGE_DIR);
|
||||
|
||||
scan_slot_subdir(repo_dir, out_slots, out_count);
|
||||
scan_slot_subdir(local_dir, out_slots, out_count);
|
||||
size_t capacity = 0;
|
||||
if (scan_slot_subdir(repo_dir, out_slots, out_count, &capacity) != 0 ||
|
||||
scan_slot_subdir(local_dir, out_slots, out_count, &capacity) != 0) {
|
||||
free(*out_slots);
|
||||
*out_slots = NULL;
|
||||
*out_count = 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
+26
-6
@@ -473,9 +473,17 @@ size_t sources_multi_repository_list_json(char *buf, size_t size, int force_refr
|
||||
RepoPayload *items = NULL;
|
||||
size_t count = 0;
|
||||
|
||||
if (read_file_text(cache_path, &cached, &cached_size) == 0 && cached) {
|
||||
parse_repository_payloads(cached, &items, &count);
|
||||
free(cached);
|
||||
if (read_file_text(cache_path, &cached, &cached_size) != 0 || !cached) {
|
||||
free(cached); free(sources);
|
||||
json_builder_fail(&jb);
|
||||
return 0;
|
||||
}
|
||||
int parsed = parse_repository_payloads(cached, &items, &count);
|
||||
free(cached);
|
||||
if (parsed != 0) {
|
||||
free(items); free(sources);
|
||||
json_builder_fail(&jb);
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t icon_capacity = 6 * sizeof(items[0].icon);
|
||||
@@ -618,7 +626,11 @@ int sources_repo_install(const char *source_url, const char *id,
|
||||
if (!is_mode_replace && !is_mode_keep_both) {
|
||||
SlotMeta *existing_slots = NULL;
|
||||
size_t existing_count = 0;
|
||||
if (slot_list_all(&existing_slots, &existing_count) == 0 && existing_slots) {
|
||||
if (slot_list_all(&existing_slots, &existing_count) != 0) {
|
||||
json_append(&jb, "{\"ok\":false,\"message\":\"Failed to scan installed slots\"}");
|
||||
return -1;
|
||||
}
|
||||
if (existing_slots) {
|
||||
for (size_t i = 0; i < existing_count; i++) {
|
||||
if (strcmp(existing_slots[i].kind, SLOT_KIND_REPO) == 0 &&
|
||||
strcmp(existing_slots[i].id, safe_id) == 0 &&
|
||||
@@ -651,8 +663,12 @@ int sources_repo_install(const char *source_url, const char *id,
|
||||
char *cached = NULL;
|
||||
size_t csize = 0;
|
||||
if (read_file_text(cache_path, &cached, &csize) == 0 && cached) {
|
||||
parse_repository_payloads(cached, &items, &count);
|
||||
int parsed = parse_repository_payloads(cached, &items, &count);
|
||||
free(cached);
|
||||
if (parsed != 0) {
|
||||
json_append(&jb, "{\"ok\":false,\"message\":\"Failed to parse repository\"}");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
int found_idx = -1;
|
||||
@@ -672,8 +688,12 @@ int sources_repo_install(const char *source_url, const char *id,
|
||||
refreshed = 1;
|
||||
|
||||
if (read_file_text(cache_path, &cached, &csize) == 0 && cached) {
|
||||
parse_repository_payloads(cached, &items, &count);
|
||||
int parsed = parse_repository_payloads(cached, &items, &count);
|
||||
free(cached);
|
||||
if (parsed != 0) {
|
||||
json_append(&jb, "{\"ok\":false,\"message\":\"Failed to parse repository\"}");
|
||||
return -1;
|
||||
}
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
if (strcmp(items[i].id, safe_id) == 0) {
|
||||
found_idx = (int)i;
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "repository.h"
|
||||
#include "slot.h"
|
||||
#include "payload_mgr.h"
|
||||
#include "collection_helpers.h"
|
||||
#include "sources.h"
|
||||
|
||||
static size_t malloc_calls, realloc_calls, strdup_calls;
|
||||
static size_t fail_malloc, fail_realloc, fail_strdup;
|
||||
static int checks, failures;
|
||||
|
||||
void *allocation_test_malloc(size_t size) {
|
||||
if (++malloc_calls == fail_malloc) { errno = ENOMEM; return NULL; }
|
||||
return malloc(size);
|
||||
}
|
||||
void *allocation_test_realloc(void *pointer, size_t size) {
|
||||
if (++realloc_calls == fail_realloc) { errno = ENOMEM; return NULL; }
|
||||
return realloc(pointer, size);
|
||||
}
|
||||
char *allocation_test_strdup(const char *string) {
|
||||
if (++strdup_calls == fail_strdup) { errno = ENOMEM; return NULL; }
|
||||
return strdup(string);
|
||||
}
|
||||
static void reset(void) { malloc_calls = realloc_calls = strdup_calls = 0; fail_malloc = fail_realloc = fail_strdup = 0; }
|
||||
static void expect(int condition, const char *message) {
|
||||
checks++;
|
||||
if (!condition) { failures++; fprintf(stderr, "FAIL %s\n", message); }
|
||||
}
|
||||
/* No test may download or launch anything. Fresh cache tests use other suites. */
|
||||
int download_to_file(const char *url, const char *path) { (void)url; (void)path; abort(); }
|
||||
void pldmgr_log(const char *fmt, ...) { (void)fmt; }
|
||||
|
||||
static char *manifest(size_t count) {
|
||||
char *json = malloc(count * 128 + 4);
|
||||
if (!json) exit(2);
|
||||
size_t pos = 0;
|
||||
json[pos++] = '[';
|
||||
for (size_t i = 0; i < count; i++)
|
||||
pos += (size_t)sprintf(json + pos, "%s{\"name\":\"item-%zu\",\"filename\":\"safe.elf\",\"url\":\"https://example.invalid\"}", i ? "," : "", i);
|
||||
strcpy(json + pos, "]");
|
||||
return json;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 2) return 2;
|
||||
if (strcmp(argv[1], "repository") == 0) {
|
||||
char *json = manifest(500);
|
||||
RepoPayload *items; size_t count;
|
||||
reset();
|
||||
expect(parse_repository_payloads(json, &items, &count) == 0 && count == 500, "complete repository");
|
||||
size_t growths = realloc_calls, allocations = malloc_calls;
|
||||
expect(growths <= 6 && allocations == 0, "geometric parser growth without per-item malloc");
|
||||
free(items);
|
||||
#ifdef LEGACY_ALLOCATION
|
||||
if (growths > 8) growths = 8;
|
||||
#endif
|
||||
for (size_t failure = 1; failure <= growths; failure++) {
|
||||
reset(); fail_realloc = failure;
|
||||
items = NULL; count = 0;
|
||||
int result = parse_repository_payloads(json, &items, &count);
|
||||
expect(result != 0 && !items && !count, "each parser growth failure returns no partial collection");
|
||||
free(items);
|
||||
}
|
||||
free(json);
|
||||
reset(); json = manifest(collection_limit(sizeof(RepoPayload)) + 1);
|
||||
int result = parse_repository_payloads(json, &items, &count);
|
||||
expect(result != 0 && !items && !count, "repository item/byte limit rejects partial output"); free(items); free(json);
|
||||
reset(); json = malloc(16 * 1024 * 1024 + 2); memset(json, ' ', 16 * 1024 * 1024 + 1); json[16 * 1024 * 1024 + 1] = 0;
|
||||
result = parse_repository_payloads(json, &items, &count);
|
||||
expect(result != 0 && !items && !count, "repository input size bounded"); free(items); free(json);
|
||||
result = parse_repository_payloads("[{\"name\":\"valid\",\"filename\":\"safe.elf\",\"url\":\"https://example.invalid\"},{\"name\":\"unfinished", &items, &count);
|
||||
expect(result != 0 && !items && !count, "unterminated trailing object does not publish earlier items"); free(items);
|
||||
size_t capacity = 16;
|
||||
char *owned = malloc(16); memset(owned, 'x', 16);
|
||||
expect(collection_reserve(owned, &capacity, COLLECTION_MAX_ITEMS + 1, 1) == NULL && capacity == 16 && owned[0] == 'x',
|
||||
"item limit leaves ownership/capacity intact");
|
||||
expect(collection_reserve(owned, &capacity, 1, SIZE_MAX) == NULL && capacity == 16 && owned[0] == 'x',
|
||||
"byte/multiplication limit leaves ownership intact");
|
||||
free(owned);
|
||||
} else if (strcmp(argv[1], "slots") == 0) {
|
||||
SlotMeta *items; size_t count;
|
||||
reset(); expect(slot_list_all(&items, &count) == 0 && count == 70, "complete slot scan");
|
||||
size_t growths = realloc_calls, reads = malloc_calls;
|
||||
expect(growths == 3, "slot array grows geometrically"); free(items);
|
||||
for (size_t failure = 1; failure <= growths; failure++) {
|
||||
reset(); fail_realloc = failure;
|
||||
int result = slot_list_all(&items, &count);
|
||||
expect(result != 0 && !items && !count, "slot growth failure is not partial success"); free(items);
|
||||
}
|
||||
for (size_t failure = 1; failure <= reads; failure++) {
|
||||
reset(); fail_malloc = failure;
|
||||
int result = slot_list_all(&items, &count);
|
||||
expect(result != 0 && !items && !count, "metadata allocation failure aborts slot scan"); free(items);
|
||||
}
|
||||
} else if (strcmp(argv[1], "source_listing") == 0) {
|
||||
char *output = malloc(4194304); if (!output) return 2;
|
||||
reset(); expect(sources_multi_repository_list_json(output, 4194304, 0) != 0, "cached source listing complete");
|
||||
size_t growths = realloc_calls, allocations = malloc_calls;
|
||||
#ifdef LEGACY_ALLOCATION
|
||||
if (growths > 8) growths = 8;
|
||||
if (allocations > 8) allocations = 8;
|
||||
#endif
|
||||
for (size_t failure = 1; failure <= growths; failure++) {
|
||||
reset(); fail_realloc = failure;
|
||||
expect(sources_multi_repository_list_json(output, 4194304, 0) == 0 && output[0] == 0,
|
||||
"parser allocation failure propagates through source listing");
|
||||
}
|
||||
for (size_t failure = 1; failure <= allocations; failure++) {
|
||||
reset(); fail_malloc = failure;
|
||||
expect(sources_multi_repository_list_json(output, 4194304, 0) == 0 && output[0] == 0,
|
||||
"cache/icon allocation failure is not an empty successful source");
|
||||
}
|
||||
free(output);
|
||||
} else if (strcmp(argv[1], "usb") == 0) {
|
||||
char *output = malloc(4194304); if (!output) return 2;
|
||||
reset(); expect(payload_mgr_list_json(output, 4194304) != 0 && strdup_calls == 70, "all three USB scan paths collected");
|
||||
size_t growths = realloc_calls, copies = strdup_calls;
|
||||
for (size_t failure = 1; failure <= growths; failure++) {
|
||||
reset(); fail_realloc = failure;
|
||||
expect(payload_mgr_list_json(output, 4194304) == 0 && output[0] == 0, "USB growth failure cleans complete scan");
|
||||
}
|
||||
for (size_t failure = 1; failure <= copies; failure++) {
|
||||
reset(); fail_strdup = failure;
|
||||
expect(payload_mgr_list_json(output, 4194304) == 0 && output[0] == 0, "USB strdup failure cleans complete scan");
|
||||
}
|
||||
free(output);
|
||||
} else return 2;
|
||||
printf("Allocation tests (%s): %d checks, %d failures\n", argv[1], checks, failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stddef.h>
|
||||
void *allocation_test_malloc(size_t size);
|
||||
void *allocation_test_realloc(void *pointer, size_t size);
|
||||
char *allocation_test_strdup(const char *string);
|
||||
#define malloc allocation_test_malloc
|
||||
#define realloc allocation_test_realloc
|
||||
#define strdup allocation_test_strdup
|
||||
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fault every array growth and copied path in actual parsers/scanners."""
|
||||
import argparse
|
||||
import json
|
||||
import hashlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
import re
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--legacy", action="store_true")
|
||||
args = parser.parse_args()
|
||||
os.environ.setdefault("ASAN_OPTIONS", "detect_leaks=1:halt_on_error=1")
|
||||
os.environ.setdefault("UBSAN_OPTIONS", "halt_on_error=1:print_stacktrace=1")
|
||||
with tempfile.TemporaryDirectory(prefix="pldmgr-alloc-") as temp:
|
||||
build = Path(temp)
|
||||
storage = build / "data"
|
||||
storage.mkdir()
|
||||
(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};\nstatic const unsigned int assets_cacert_pem_len = 0;\n")
|
||||
common = shlex.split(os.environ.get("HOST_CC", "clang")) + ["-std=c11", "-D_DEFAULT_SOURCE", "-g", "-O1", "-Wall", "-Wextra",
|
||||
"-ffunction-sections", "-fdata-sections", "-fsanitize=address,undefined", "-fno-omit-frame-pointer",
|
||||
"-I" + str(build), "-Iinclude", "-pthread", '-DPLDMGR_USB_ROOT_FORMAT="' + str(storage / "usb%d") + '"']
|
||||
if args.legacy:
|
||||
common += ["-DLEGACY_ALLOCATION"]
|
||||
objects = []
|
||||
for name in ["json_helpers", "repository", "slot", "payload_mgr", "sources"]:
|
||||
source = f"scratch/{name}_before_step3a.c" if args.legacy and name != "json_helpers" else f"src/{name}.c"
|
||||
if args.legacy and name == "payload_mgr":
|
||||
# Test-only path overlay for the old hardcoded USB roots.
|
||||
patched = build / "payload_before.c"
|
||||
patched.write_text((ROOT / source).read_text().replace("/mnt/usb", str(storage / "usb")))
|
||||
source = str(patched)
|
||||
obj = build / (name + ".o")
|
||||
flags = ["-Ddownload_to_file=allocation_unused_download"] if name == "repository" else []
|
||||
subprocess.run(common + flags + ["-include", "tests/allocation_hooks.h", "-c", source, "-o", str(obj)], cwd=ROOT, check=True)
|
||||
objects.append(str(obj))
|
||||
binary = build / "allocation_tests"
|
||||
subprocess.run(common + objects + ["src/utils.c", "src/sha256.c", "src/config.c", "tests/allocation_driver.c",
|
||||
"-Wl,--gc-sections", "-o", str(binary)], cwd=ROOT, check=True)
|
||||
header = (ROOT / "include/pldmgr.h").read_text()
|
||||
url = re.search(r'#define REPOSITORY_SOURCE_URL\s*\\?\s*"([^"]+)"', header)[1]
|
||||
cache = storage / ("repo_cache_" + hashlib.sha256(url.rstrip('/').encode()).hexdigest()[:8] + ".json")
|
||||
cache.write_text(json.dumps([{"name": f"item-{i}", "filename": "safe.elf", "url": "https://example.invalid"} for i in range(500)]))
|
||||
Path(str(cache) + ".ts").write_text(str(int(time.time())))
|
||||
for i in range(70):
|
||||
slot = storage / "payloads/local" / f"test-{i}"
|
||||
slot.mkdir(parents=True)
|
||||
(slot / "safe.elf").write_bytes(b"fixture only")
|
||||
(slot / "meta.json").write_text(json.dumps({"kind": "local", "filename": "safe.elf"}))
|
||||
failures = 0
|
||||
for mode in ["repository", "slots", "source_listing", "usb"]:
|
||||
if mode == "usb":
|
||||
# Remove fixture metadata files from discovery without deleting:
|
||||
# rename the scanned local directory into this temporary build.
|
||||
(storage / "payloads/local").rename(build / "saved-slots")
|
||||
for directory, count in [("usb0/pldmgr", 24), ("usb0/pldmgr/nested", 23), ("usb0", 23)]:
|
||||
path = storage / directory
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
for i in range(count): (path / f"test-{i}.elf").write_bytes(b"fixture only")
|
||||
(storage / "pldmgr_config.txt").write_text("SCAN_USB_PAYLOADS=1\n")
|
||||
result = subprocess.run([str(binary), mode], capture_output=True, text=True)
|
||||
print(result.stdout.strip())
|
||||
if result.returncode:
|
||||
failures += 1
|
||||
print(result.stderr[-1800:])
|
||||
if failures: raise SystemExit(f"Allocation tests: {failures} failing scenarios")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user