fix: bound collection growth and propagate allocation failures

This commit is contained in:
najdek committed 2026-10-12 19:49:45 +02:00
1 parent 45be5c6cd7
commit 5c04204f97
11 files changed
+372 -54

No files matched your search

+14 -1
View File
@@ -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
+1
View File
@@ -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:
+31
View File
@@ -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
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+133
View File
@@ -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;
}
+9
View File
@@ -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
+79
View File
@@ -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()