diff --git a/docs/agent/architecture.md b/docs/agent/architecture.md index 08e46f7..d35b579 100644 --- a/docs/agent/architecture.md +++ b/docs/agent/architecture.md @@ -224,6 +224,7 @@ Hardware diagnostics are synchronous console commands. RS-232 tests own the phys - Password authentication performs PBKDF2 outside the user-database mutex and revalidates afterward. Some password mutation paths currently derive verifiers while holding the mutation lock; do not generalize the authentication locking pattern without checking the exact path. - Avoid holding service/database/broker locks across I2C, network sends, or other potentially long operations unless the existing contract explicitly requires it. Preserve the existing broker-before-serial lock order. - Serial RX/TX stream payloads, broker per-client payloads, the transactional user-database candidate, and selected cryptographic allocations prefer PSRAM with internal fallback. The live user database, FreeRTOS control structures, UART driver buffers, and task stacks remain internal where deterministic/cache-disable access matters. +- Ping's 21-event payload (4,200 bytes) and the public user-console snapshot (2,156 bytes) are lazy PSRAM-only allocations retained for firmware lifetime, with no internal fallback. Queue control and synchronization stay internal; only ping or user status/list/show fails on allocation failure, never registration, mutations or UART0 recovery. Commands retain dispatcher/gate serialization; ping callbacks are task-context producers. The entire user snapshot is wiped after each display attempt. Never introduce ISR/cache-off access or free payloads while callbacks can retain them. Regression coverage: `tests/admin_console_boundary/psram_ping.py` and `accounts.py`. - The build disables wolfSSL ESP32 AES/SHA acceleration, and the HTTPS path uses software AES for PSRAM-backed records. This preserves the validated workaround for uncoordinated mbedTLS/wolfSSL hardware-crypto locks and a prior mbedTLS external-RAM DMA watchdog stall. ## Legacy credential removal storage boundary diff --git a/docs/agent/current-state.md b/docs/agent/current-state.md index 9008290..9f7aec9 100644 --- a/docs/agent/current-state.md +++ b/docs/agent/current-state.md @@ -2,6 +2,14 @@ Working memory, not an implementation timeline. Source is authoritative; begin with [code map](code-map.md), then [architecture](architecture.md) and [decisions](design-decisions.md). +## Focused cleanup / PSRAM review — 2026-09-13 + +- User-authorized post-acceptance code review: removed superseded `admin_ssh_console_open()` SSH-only wrapper; production already uses available-slot admission. Updated adapter regression to actual production entry. Wrapper was already linker-discarded: no binary saving attributed to removal. +- Moved ping queue payload4,200 B and public user snapshot2,156 B to lazy PSRAM-only lifetime allocations, no fallback. Queue control/internal locks unchanged; allocation failure affects only ping or status/list/show, not UART0 registration/mutations/recovery. Snapshot fully wiped on success/error. No serial hot-path, stacks, CPU, queue bounds or external-BSS config changes. +- Baseline pio PASS23.79s100,556 RAM/1,828,573 flash. Final parent pio PASS22.11s **94,212 RAM/1,828,809 flash: −6,344 B linked internal RAM/+236 B flash**. Requested lazy PSRAM6,356 B plus allocator overhead; target pointer sizes4 B, controls84 B verified. This is not measured runtime-minimum improvement. CPU160 retained. +- Focused ping allocation/callback/end-capacity and accounts allocation/full-wipe/failure/retry tests PASS; console boundary/lifecycle/policy and SSH management/runtime suites PASS. Independent review found no actionable bugs and reran ping/accounts/boundary/diff PASS. Sanitizer linking unavailable (missing host runtimes), no sanitizer or hardware pass. No upload/erase/commit. Next target check: ping/user list/show repeatedly alongside NVS writes and full mix230400, collect serial/broker counters and memory before/after first allocations. +- Further audited opportunities, not implemented: OLED framebuffer1,024 B (internal I2C staging retained), local/remote completion scratch1,024 B each, optional web diagnostic ring2,816 B, remote console output payload8,192 B. Console rings need explicit secret-output wiping/lifecycle and admission-failure isolation; do not relocate mixed state wholesale. Leave authoritative user database, driver/DMA buffers, locks and task stacks internal. Global external-BSS enable is not surgical: it also changes SDK library placement. + ## Accepted state — 2026-09-13 - **8D.22 explicitly signed off by the user:** “Yep, I tested the firmware thats a 8d.22 signoff.” The retained Phase 8D scope is complete; earlier per-slice pending target/review/integration gates are superseded. Roadmap already records 8A/B/C as complete and target-hardware validated, so **Phase 8 is complete**. Acceptance does not manufacture individual unreported test passes. @@ -23,7 +31,7 @@ Working memory, not an implementation timeline. Source is authoritative; begin w - Typed operations carry original-login IDs to the existing dispatcher. Owner-reserved generations fence stale/ABA changes; later revocation/timeout does not cancel admitted work. HTTPS commits before stop/restart; SSH stops before commit/restart; committed identity never rolls back on lifecycle failure. Lost ACK/result means uncertainty, never automatic replay. - Preserve private IDF HTTPD version guards, at-most-one owner-work reservations through failed destruction, retained SSH context until all slots retire, bounded queues/buffers and secret-free metadata. Canonical recovery survives conditional-token exhaustion. -## Documentation handoff +## Previous documentation consolidation handoff Initial Git status was clean. This task changes root `README.md`, `docs/` and five test-directory READMEs; executable source/tests/config/generated assets remain untouched. Independent documentation review checked acceptance scope, owner contracts and local links. It restored explicit pointer-backed HTTPD response-header lifetime and same-version SDK-patch audit warnings, updated test README links, and removed obsolete forwarding notes without reopening sign-off. diff --git a/src/admin_ssh_console.h b/src/admin_ssh_console.h index c99c940..c581373 100644 --- a/src/admin_ssh_console.h +++ b/src/admin_ssh_console.h @@ -142,12 +142,6 @@ esp_err_t admin_ssh_console_dispatch_read_input( esp_err_t admin_ssh_console_dispatch_defer( admin_ssh_deferred_action_type_t action, uint32_t argument); -/* SSH compatibility entry point, implemented by the owner in ssh_transport.c. - * Token/principal are copied; no SSH or socket objects cross this boundary. - * Existing feed/close/read/snapshot APIs below also accept open_owned tokens. - */ -esp_err_t admin_ssh_console_open(const admin_ssh_console_token_t *token, - const user_principal_t *principal); void admin_ssh_console_close(const admin_ssh_console_token_t *token); /* Called by the session owner. Returns false when input must be backpressured. */ diff --git a/src/network_console.c b/src/network_console.c index a7ab83c..726c526 100644 --- a/src/network_console.c +++ b/src/network_console.c @@ -12,6 +12,7 @@ #include "esp_console.h" #include "esp_err.h" +#include "esp_heap_caps.h" #include "esp_timer.h" #include "freertos/FreeRTOS.h" #include "freertos/queue.h" @@ -186,7 +187,9 @@ typedef struct { #define PING_EVENT_QUEUE_LENGTH (PING_MAX_COUNT + 1U) static StaticQueue_t s_ping_queue_storage; -static uint8_t s_ping_queue_bytes[PING_EVENT_QUEUE_LENGTH * sizeof(ping_event_t)]; +/* Dispatcher-owned lazy payload; retain for firmware lifetime so callback queue + * storage cannot dangle. Queue control stays internal. No internal-RAM fallback. */ +static uint8_t *s_ping_queue_bytes; static QueueHandle_t s_ping_queue; static void ping_on_success(esp_ping_handle_t handle, void *arguments) @@ -289,6 +292,15 @@ static int execute_ping(int argc, char **argv) return 1; } + if (s_ping_queue_bytes == NULL) { + s_ping_queue_bytes = heap_caps_malloc( + PING_EVENT_QUEUE_LENGTH * sizeof(ping_event_t), + MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if (s_ping_queue_bytes == NULL) { + printf("ping: PSRAM event storage unavailable\n"); + return 1; + } + } if (s_ping_queue == NULL) { s_ping_queue = xQueueCreateStatic(PING_EVENT_QUEUE_LENGTH, sizeof(ping_event_t), s_ping_queue_bytes, &s_ping_queue_storage); diff --git a/src/ssh_transport.c b/src/ssh_transport.c index 62ec83f..b8b4793 100644 --- a/src/ssh_transport.c +++ b/src/ssh_transport.c @@ -292,15 +292,6 @@ static const admin_console_owner_t s_admin_console_owner = { .perform = admin_console_perform, }; -esp_err_t admin_ssh_console_open(const admin_ssh_console_token_t *token, - const user_principal_t *principal) -{ - if (token == NULL || token->transport != ADMIN_CONSOLE_TRANSPORT_SSH) { - return ESP_ERR_INVALID_ARG; - } - return admin_ssh_console_open_owned(token, principal, &s_admin_console_owner); -} - static bool consume_external_close(const ssh_slot_t *slot, size_t slot_index) { taskENTER_CRITICAL(&s_lock); diff --git a/src/user_console.c b/src/user_console.c index 06f3abe..59c1d32 100644 --- a/src/user_console.c +++ b/src/user_console.c @@ -11,6 +11,7 @@ #include "admin_ssh_console.h" #include "console_input.h" #include "esp_console.h" +#include "esp_heap_caps.h" #include "mbedtls/base64.h" #include "secure_random.h" #include "ssh_transport.h" @@ -19,8 +20,9 @@ #define USER_CONSOLE_KEY_LINE_CAPACITY 256U -/* `user` commands are serialized by the administration gate. */ -static user_database_snapshot_t s_user_snapshot; +/* Gate-owned public projection (no verifiers). Lazily retained for firmware + * lifetime; allocation failure must not disable mutations or UART0 recovery. */ +static user_database_snapshot_t *s_user_snapshot; static void print_usage(void) { @@ -92,21 +94,30 @@ static void print_user(const user_database_user_snapshot_t *user) static int show_users(const char *selected) { - esp_err_t error = user_database_get_snapshot(&s_user_snapshot); + if (s_user_snapshot == NULL) { + s_user_snapshot = heap_caps_malloc(sizeof(*s_user_snapshot), + MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if (s_user_snapshot == NULL) { + printf("User status unavailable: PSRAM snapshot storage unavailable\n"); + return 1; + } + } + esp_err_t error = user_database_get_snapshot(s_user_snapshot); if (error != ESP_OK) { printf("User database unavailable: %s\n", esp_err_to_name(error)); + secure_wipe(s_user_snapshot, sizeof(*s_user_snapshot)); return 1; } if (selected == NULL) { printf("User database: generation=%lu users=%u/%u admins=%u\n", - (unsigned long)s_user_snapshot.generation, - (unsigned int)s_user_snapshot.user_count, + (unsigned long)s_user_snapshot->generation, + (unsigned int)s_user_snapshot->user_count, USER_DATABASE_MAX_USERS, - (unsigned int)s_user_snapshot.admin_count); + (unsigned int)s_user_snapshot->admin_count); } bool found = false; for (size_t index = 0U; index < USER_DATABASE_MAX_USERS; ++index) { - const user_database_user_snapshot_t *user = &s_user_snapshot.users[index]; + const user_database_user_snapshot_t *user = &s_user_snapshot->users[index]; if (!user->active || (selected != NULL && (strlen(selected) != user->username_length || @@ -118,11 +129,13 @@ static int show_users(const char *selected) } if (selected != NULL && !found) { printf("User '%s' not found.\n", selected); + secure_wipe(s_user_snapshot, sizeof(*s_user_snapshot)); return 1; } - if (s_user_snapshot.admin_count == 0U) { + if (s_user_snapshot->admin_count == 0U) { printf("No administrators; use 'user add admin' on UART0.\n"); } + secure_wipe(s_user_snapshot, sizeof(*s_user_snapshot)); return 0; } diff --git a/tests/admin_console_boundary/accounts.py b/tests/admin_console_boundary/accounts.py index a8b8975..e9652a5 100644 --- a/tests/admin_console_boundary/accounts.py +++ b/tests/admin_console_boundary/accounts.py @@ -138,8 +138,9 @@ static bool admin_ssh_console_dispatch_is_current(void) { if (checks==revoke_check) owner_current=false; return owner_current && user_database_principal_is_current(&actor, ¤t)==ESP_OK && current; } -static int admin_command_gate_take(void) { return ESP_OK; } -static void admin_command_gate_give(void) {} +static bool gate_held; +static int admin_command_gate_take(void) { assert(!gate_held); gate_held=true; return ESP_OK; } +static void admin_command_gate_give(void) { assert(gate_held); gate_held=false; } static int console_input_read_hidden(const char *prompt, uint8_t *out, size_t cap, size_t min, size_t max, size_t *n) { (void)prompt; (void)min; (void)max; assert(cap>=13); ++prompts; @@ -158,7 +159,28 @@ static int ssh_transport_revoke_user(const uint8_t *u, size_t n) { ++ssh_revokes; assert(strlen(revoked_name)==n && !memcmp(u,revoked_name,n)); return notify_error; } /* Forbidden paths are traps rather than alternative implementations. */ -static int show_users(const char *n) { (void)n; return 0; } +#define MALLOC_CAP_SPIRAM 1U +#define MALLOC_CAP_8BIT 2U +static bool fail_snapshot_alloc; +static unsigned snapshot_allocations; +static struct { uint64_t before; user_database_snapshot_t value; uint64_t after; } snapshot_memory; +static void *heap_caps_malloc(size_t n, unsigned caps) { + assert(gate_held && n==sizeof(snapshot_memory.value)); + assert(caps==(MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)); + ++snapshot_allocations; + if (fail_snapshot_alloc) return NULL; + snapshot_memory.before=snapshot_memory.after=UINT64_C(0xaabbccdd11223344); + memset(&snapshot_memory.value,0xa5,n); + return &snapshot_memory.value; +} +static unsigned user_registrations; +typedef struct { const char *command,*help,*hint; int (*func)(int,char **); void *argtable; } esp_console_cmd_t; +static int esp_console_cmd_register(const esp_console_cmd_t *c) { + assert(!strcmp(c->command,"user") && c->func); ++user_registrations; return ESP_OK; +} +static int mbedtls_base64_encode(uint8_t *out,size_t cap,size_t *n,const uint8_t *in,size_t len) { + (void)in; assert(cap>=4 && len==32); memcpy(out,"AAAA",4); *n=4; return 0; +} static int add_key(const char *n) { (void)n; assert(!"key mutation"); return 1; } static int add_key_parts(const char *n,const uint8_t *t,size_t tl,const uint8_t *b,size_t bl) { @@ -190,17 +212,21 @@ db_names = ["constant_time_equal", "all_zero", "user_database_username_valid", "user_database_clear_ssh_keys_current", "fill_principal", "user_database_authorize_ssh_public_key", "initialize_dummy_verifier", "user_database_init", "user_database_recover_empty", "user_database_get_snapshot"] -console_names = ["print_usage", "revoke_user_network_sessions", "read_password", +console_names = ["print_fingerprint", "print_user", "show_users", "print_usage", "revoke_user_network_sessions", "read_password", "show_generated_password", "mutation_currentness", "add_user", "change_password", - "parse_key_index", "recover_database", "command_user_inner", "command_user"] + "parse_key_index", "recover_database", "command_user_inner", "command_user", + "user_console_register_commands"] unit = prelude + header + "\n" + state + fakes unit += "\n".join(function(db, n) for n in db_names) unit += function(admin, "admin_ssh_console_web_user_command_allowed") +unit += console[console.index("static user_database_snapshot_t"):console.index("static void print_usage")] unit += "\n".join(function(console, n) for n in console_names) account_tests = (ROOT / "tests/admin_console_boundary/accounts.c").read_text() key_tests = (ROOT / "tests/admin_console_boundary/account_keys.c").read_text() account_tests = account_tests.replace('int main(void)', key_tests + '\nint main(void)') -account_tests = account_tests.replace(' typed_account_tests();', ' typed_key_tests();\n typed_account_tests();') +snapshot_tests = (ROOT / "tests/admin_console_boundary/psram_snapshot.c").read_text() +account_tests = account_tests.replace('int main(void)', snapshot_tests + '\nint main(void)') +account_tests = account_tests.replace(' typed_account_tests();', ' psram_snapshot_tests();\n typed_key_tests();\n typed_account_tests();') assert ' typed_key_tests();' in account_tests unit += account_tests with tempfile.TemporaryDirectory(prefix="admin-accounts-") as directory: @@ -209,9 +235,13 @@ with tempfile.TemporaryDirectory(prefix="admin-accounts-") as directory: subprocess.run(["cc", "-std=c11", "-Wall", "-Wextra", "-Werror", "-Wno-unused-variable", str(path / "test.c"), str(IDF / "components/console/split_argv.c"), "-lcrypto", "-o", str(path / "test")], check=True, timeout=30) - result = subprocess.run([str(path / "test")], check=True, timeout=10, capture_output=True, text=True) + result = subprocess.run([str(path / "test")], timeout=10, capture_output=True, text=True) + if result.returncode: + print(result.stderr) + result.check_returncode() assert "test-password" not in result.stdout assert "Generated password for" not in result.stdout + print("PASS: PSRAM snapshot allocation failure/retry/retention, status/list/show/unknown/unavailable, full wipe and bounds, gate ownership, mutation/recovery isolation") print("PASS: empty initialization/recovery, unchanged v1 records, corrupt/unsupported fail-closed loads, first UART0 administrator and removed bootstrap commands") print("PASS: canonical SSH keys: Ed25519/P256 parser and authorization, malformed/off-curve/truncated inputs, zero-wait fingerprints, stale ID/generation/recreation, duplicates/capacity, sparse indices, failed persistence and CLI parity (OpenSSL-backed curve/SHA adapters)") print("PASS: operation-admission semantics: browser invalidated in derivation double before NVS; admitted add/password transactions still commit, only target is revoked, next command rejects; persistence failure still preserves live state (not precommit cancellation or real concurrency)") diff --git a/tests/admin_console_boundary/adapter.c b/tests/admin_console_boundary/adapter.c index 514938b..10c2d7e 100644 --- a/tests/admin_console_boundary/adapter.c +++ b/tests/admin_console_boundary/adapter.c @@ -33,6 +33,7 @@ static bool admin_console_drained(const admin_ssh_console_token_t *); static bool admin_console_is_current(const admin_ssh_console_token_t *, const user_principal_t *); static bool consume_external_close(const ssh_slot_t *, size_t); static esp_err_t admin_console_perform(const admin_ssh_console_token_t *, admin_ssh_deferred_action_type_t, uint32_t); +static const admin_console_owner_t s_admin_console_owner; static void test_adapter(void) { admin_ssh_console_token_t token={ .slot_index=0, .session_id=7, .slot_generation=3 }; @@ -40,7 +41,7 @@ static void test_adapter(void) .username_length=5, .username="admin" }; assert(admin_ssh_console_init()==ESP_OK); assert(admin_ssh_console_start_uart_frontend()==ESP_OK); - assert(admin_ssh_console_open(&token,&admin)==ESP_OK); + assert(admin_ssh_console_open_available(&token,&admin,&s_admin_console_owner)==ESP_OK); assert(!admin_console_drained(&token)); s_session_snapshots[0]=(ssh_transport_session_snapshot_t){ .active=true, .session_id=7, .generation=3 }; assert(!admin_console_drained(&token)); /* No published console binding. */ @@ -82,16 +83,19 @@ static void test_adapter(void) admin.username_length=1; assert(!admin_console_is_current(&token,&admin)); admin.username_length=5; token.transport=1; assert(!admin_console_drained(&token)); assert(!admin_console_is_current(&token,&admin)); - assert(admin_ssh_console_open(&token,&admin)==ESP_ERR_INVALID_ARG); + token.transport=0; token.slot_generation=4; assert(!admin_console_drained(&token)); assert(!admin_console_is_current(&token,&admin)); assert(admin_console_perform(&token,ADMIN_SSH_DEFER_STOP,0)==ESP_ERR_NOT_FOUND && stopped==0); token.slot_generation=3; /* Same physical session can be assigned the other console slot. */ admin_ssh_console_close(&token); - token.slot_index=1; - assert(admin_ssh_console_open(&token,&admin)==ESP_OK); - active.console_slot_index=1; publish_slot(&active,0); + admin_ssh_console_token_t other={ .session_id=8, .slot_generation=1 }; + assert(admin_ssh_console_open_available(&other,&admin,&s_admin_console_owner)==ESP_OK); + assert(other.slot_index==0); + assert(admin_ssh_console_open_available(&token,&admin,&s_admin_console_owner)==ESP_OK); + assert(token.slot_index==1); + active.console_slot_index=token.slot_index; publish_slot(&active,0); assert(admin_console_is_current(&token,&admin)); assert(s_console_slot_indices[0]==1); admin_ssh_console_token_t wrong=token; wrong.slot_index=0; @@ -111,5 +115,5 @@ static void test_adapter(void) assert(admin_console_perform(&token,ADMIN_SSH_DEFER_HOST_KEY_ROTATE,0)==ESP_OK && rotated==1); assert(admin_console_perform(&token,ADMIN_SSH_DEFER_HOST_KEY_RESET,0)==ESP_OK && reset==1); assert(admin_console_perform(&token,ADMIN_SSH_DEFER_REBOOT,0)==ESP_OK && restarted==1); - puts("PASS: actual SSH snapshot/principal publication and wiping, adapter identity/drain checks, legacy admission and lifecycle action routing"); + puts("PASS: actual SSH snapshot/principal publication and wiping, adapter identity/drain checks, shared-slot admission and lifecycle action routing"); } diff --git a/tests/admin_console_boundary/psram_ping.c b/tests/admin_console_boundary/psram_ping.c new file mode 100644 index 0000000..7784dbd --- /dev/null +++ b/tests/admin_console_boundary/psram_ping.c @@ -0,0 +1,44 @@ +static int run_ping(const char *count) +{ + char *argv[]={"ping","localhost",(char *)count}; + int result=network_console_execute(count?3:2,argv); + if(worker_started) { assert(!pthread_join(worker,NULL)); worker_started=false; } + return result; +} +int main(void) +{ + _Static_assert(sizeof(ping_event_t)==200,"audited event payload changed"); + assert(network_console_register_root_commands()==ESP_OK && registrations==3 && !allocations); + assert(run_ping("0")==1 && run_ping("21")==1 && run_ping("-1")==1); + assert(run_ping("999999999999999999999")==1 && !allocations); + resolve_fail=true; assert(run_ping(NULL)==1 && !allocations); resolve_fail=false; + fail_alloc=true; assert(run_ping(NULL)==1 && allocations==1 && !creations && !starts); + char *lookup[]={"nslookup","localhost"}; + assert(network_console_execute(2,lookup)==0); + assert(network_console_register_root_commands()==ESP_OK && allocations==1); + fail_alloc=false; fail_queue=true; + assert(run_ping(NULL)==1 && allocations==2 && creations==1 && !starts); + void *retained=s_ping_queue_bytes; + fail_alloc=true; fail_queue=false; fail_new=true; + assert(run_ping(NULL)==1 && allocations==2 && creations==2 && !starts); + fail_new=false; fail_start=true; + assert(run_ping(NULL)==1 && deletes==1); + fail_start=false; + for(unsigned i=0;i<4;++i) { + burst=i%2==0; + assert(run_ping("20")==0); + assert(s_ping_queue_bytes==retained && allocations==2 && creations==2); + assert(queue.count==0); + assert(memory.before==UINT64_C(0x1122334455667788)); + assert(memory.after==UINT64_C(0x1122334455667788)); + } + /* Reset stale payload before a new session, preserving the end-event slot. */ + ping_event_t stale={.kind=PING_EVENT_END,.profile_error=ESP_FAIL}; + xQueueSend(s_ping_queue,&stale,0); + assert(run_ping("1")==0); + all_timeouts=true; assert(run_ping(NULL)==1); all_timeouts=false; + fail_profile=true; assert(run_ping(NULL)==1); fail_profile=false; + fail_delete=true; assert(run_ping(NULL)==1); fail_delete=false; + assert(run_ping(NULL)==0 && resets>=10 && allocations==2); + assert(s_ping_queue==&queue && s_ping_queue_bytes==retained); +} diff --git a/tests/admin_console_boundary/psram_ping.py b/tests/admin_console_boundary/psram_ping.py new file mode 100644 index 0000000..d7ae56c --- /dev/null +++ b/tests/admin_console_boundary/psram_ping.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +"""Production ping command/callbacks with bounded pthread queue and SDK doubles.""" +from pathlib import Path +import subprocess +import tempfile +ROOT = Path(__file__).resolve().parents[2] +s = (ROOT / 'src/network_console.c').read_text() +def function(name): + start = s.rfind('\n', 0, s.index(name + '(')) + 1 + return s[start:s.index('\n}', start) + 2] + '\n' +prelude = (ROOT / 'tests/admin_console_boundary/psram_ping_fakes.c').read_text() +unit = prelude + '\n' +unit += s[s.index('#define PING_DEFAULT_COUNT'):s.index('/* This covers')] +unit += function('print_command_usage') + function('parse_bounded_u32') +unit += s[s.index('typedef enum {\n PING_EVENT_LINE'):s.index('static bool socket_addresses_equal')] +unit += function('network_console_is_command') + function('network_console_execute') +unit += function('network_console_register_root_commands') +unit += (ROOT / 'tests/admin_console_boundary/psram_ping.c').read_text() +with tempfile.TemporaryDirectory(prefix='psram-ping-') as d: + p = Path(d) + (p / 'test.c').write_text(unit) + subprocess.run(['cc', '-std=c11', '-Wall', '-Wextra', '-Werror', '-pthread', + '-g', str(p / 'test.c'), '-o', str(p / 'test')], + check=True, timeout=30) + subprocess.run([str(p / 'test')], check=True, timeout=20) +print('PASS: production ping allocation/create-queue/session failures, retry/retention/reset, bounds, asynchronous success/timeout/end callbacks and registration isolation (host doubles)') diff --git a/tests/admin_console_boundary/psram_ping_fakes.c b/tests/admin_console_boundary/psram_ping_fakes.c new file mode 100644 index 0000000..f26d615 --- /dev/null +++ b/tests/admin_console_boundary/psram_ping_fakes.c @@ -0,0 +1,119 @@ +/* Deterministic SDK boundaries; queue data movement uses the supplied payload. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +typedef int esp_err_t; +enum { ESP_OK, ESP_FAIL }; +#define MALLOC_CAP_SPIRAM 1U +#define MALLOC_CAP_8BIT 2U +#define portMAX_DELAY UINT32_MAX +#define pdTRUE 1 +static bool fail_alloc, fail_queue, fail_new, fail_start, fail_delete, fail_profile, resolve_fail; +static unsigned allocations, creations, resets, registrations, starts, deletes; +static struct { uint64_t before; uint8_t bytes[4200]; uint64_t after; } memory; +static void *heap_caps_malloc(size_t n, unsigned caps) { + ++allocations; assert(n==sizeof(memory.bytes)); + assert(caps==(MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)); + if (fail_alloc) return NULL; + memory.before=memory.after=UINT64_C(0x1122334455667788); + return memory.bytes; +} +typedef struct { unsigned marker; } StaticQueue_t; +typedef struct { uint8_t *bytes; size_t length, size, read, count; } queue_t; +typedef queue_t *QueueHandle_t; +static queue_t queue; +static pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t ready=PTHREAD_COND_INITIALIZER; +static QueueHandle_t xQueueCreateStatic(size_t length,size_t size,uint8_t *bytes,StaticQueue_t *control) { + ++creations; assert(length==21 && size==200 && bytes==memory.bytes && control); + if (fail_queue) return NULL; + queue=(queue_t){.bytes=bytes,.length=length,.size=size}; return &queue; +} +static int xQueueReset(QueueHandle_t q) { + pthread_mutex_lock(&mutex); ++resets; q->read=q->count=0; + pthread_mutex_unlock(&mutex); return pdTRUE; +} +static int xQueueSend(QueueHandle_t q,const void *event,unsigned wait) { + assert(wait==0); pthread_mutex_lock(&mutex); assert(q->countlength); + memcpy(q->bytes+((q->read+q->count)%q->length)*q->size,event,q->size); + ++q->count; pthread_cond_signal(&ready); pthread_mutex_unlock(&mutex); return pdTRUE; +} +static int xQueueReceive(QueueHandle_t q,void *event,unsigned wait) { + assert(wait==portMAX_DELAY); pthread_mutex_lock(&mutex); + while (!q->count) pthread_cond_wait(&ready,&mutex); + memcpy(event,q->bytes+q->read*q->size,q->size); + q->read=(q->read+1)%q->length; --q->count; + pthread_mutex_unlock(&mutex); return pdTRUE; +} +typedef uint32_t ip_addr_t; +#define IP_IS_V4(p) ((void)(p), 1) +static char *ipaddr_ntoa_r(const ip_addr_t *ip,char *out,int n) { + (void)ip; assert(n>=10); strcpy(out,"127.0.0.1"); return out; +} +static size_t strlcpy(char *out,const char *in,size_t n) { + size_t len=strlen(in); if(n) { size_t copy=lencount>=1 && c->count<=20); + if(fail_new) return ESP_FAIL; + callbacks=*cb; probes=c->count; replies=all_timeouts?0:(probes+1)/2; *h=(void *)1; return ESP_OK; +} +static int esp_ping_start(esp_ping_handle_t h) { + assert(h==(void *)1); ++starts; if(fail_start) return ESP_FAIL; + assert(!pthread_create(&worker,NULL,produce,NULL)); worker_started=true; + if(burst) { assert(!pthread_join(worker,NULL)); worker_started=false; assert(queue.count==probes+1); } + return ESP_OK; +} +static int execute_nslookup(int argc,char **argv) { (void)argc; (void)argv; return 0; } +static int execute_traceroute(int argc,char **argv) { (void)argc; (void)argv; return 0; } +typedef struct { const char *command,*help,*hint; int (*func)(int,char **); void *argtable; } esp_console_cmd_t; +static int esp_console_cmd_register(const esp_console_cmd_t *c) { + assert(c->func); ++registrations; return ESP_OK; +} diff --git a/tests/admin_console_boundary/psram_snapshot.c b/tests/admin_console_boundary/psram_snapshot.c new file mode 100644 index 0000000..5d68d59 --- /dev/null +++ b/tests/admin_console_boundary/psram_snapshot.c @@ -0,0 +1,42 @@ +/* Actual canonical handlers and database projection; included by accounts.py. */ +static void psram_snapshot_tests(void) +{ + reset(); remote=web=false; + fail_snapshot_alloc=true; + assert(user_console_register_commands()==ESP_OK && user_registrations==1); + assert(!snapshot_allocations); + assert(run("user status")==1); + assert(run("user list")==1); + assert(run("user show other")==1); + assert(snapshot_allocations==3 && s_user_snapshot==NULL && !gate_held); + assert(run("user add newcomer user")==0); + assert(snapshot_allocations==3); + s_initialized=false; s_mutex=NULL; storage_test=true; + assert(run("user recover --force")==0); + storage_test=false; + assert(snapshot_allocations==3); + reset(); remote=web=false; + fail_snapshot_alloc=false; + assert(run("user status")==0 && snapshot_allocations==4); + assert(all_zero(s_user_snapshot,sizeof(*s_user_snapshot))); + void *retained=s_user_snapshot; + fail_snapshot_alloc=true; + for (unsigned i=0;i<3;++i) { + assert(run("user")==0); + assert(all_zero(s_user_snapshot,sizeof(*s_user_snapshot))); + assert(run("user list")==0); + assert(all_zero(s_user_snapshot,sizeof(*s_user_snapshot))); + assert(run("user show other")==0); + assert(all_zero(s_user_snapshot,sizeof(*s_user_snapshot))); + assert(run("user show missing")==1); + assert(all_zero(s_user_snapshot,sizeof(*s_user_snapshot))); + s_initialized=false; + memset(s_user_snapshot,0xa5,sizeof(*s_user_snapshot)); + assert(run("user status")==1); + assert(all_zero(s_user_snapshot,sizeof(*s_user_snapshot))); + s_initialized=true; + assert(s_user_snapshot==retained && snapshot_allocations==4 && !gate_held); + assert(snapshot_memory.before==UINT64_C(0xaabbccdd11223344)); + assert(snapshot_memory.after==UINT64_C(0xaabbccdd11223344)); + } +}