Move console payloads to lazy PSRAM storage

This commit is contained in:
2026-09-13 23:06:42 +02:00
parent 91267b371e
commit 4a53a21f31
12 changed files with 322 additions and 38 deletions
+1
View File
@@ -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
+9 -1
View File
@@ -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.
-6
View File
@@ -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. */
+13 -1
View File
@@ -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);
-9
View File
@@ -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);
+21 -8
View File
@@ -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 <username> admin' on UART0.\n");
}
secure_wipe(s_user_snapshot, sizeof(*s_user_snapshot));
return 0;
}
+37 -7
View File
@@ -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, &current)==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)")
+10 -6
View File
@@ -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");
}
+44
View File
@@ -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);
}
@@ -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)')
@@ -0,0 +1,119 @@
/* Deterministic SDK boundaries; queue data movement uses the supplied payload. */
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>
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->count<q->length);
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=len<n-1?len:n-1; memcpy(out,in,copy); out[copy]=0; } return len;
}
static int resolve_ping_target(const char *host,ip_addr_t *ip,char *out,size_t n) {
(void)host; *ip=0; ipaddr_ntoa_r(ip,out,(int)n); return resolve_fail;
}
static const char *esp_err_to_name(int e) { (void)e; return "injected"; }
typedef void *esp_ping_handle_t;
typedef struct { uint32_t count; ip_addr_t target_addr; } esp_ping_config_t;
#define ESP_PING_DEFAULT_CONFIG() ((esp_ping_config_t){0})
typedef struct {
void *cb_args;
void (*on_ping_success)(esp_ping_handle_t,void *);
void (*on_ping_timeout)(esp_ping_handle_t,void *);
void (*on_ping_end)(esp_ping_handle_t,void *);
} esp_ping_callbacks_t;
enum { ESP_PING_PROF_SEQNO, ESP_PING_PROF_SIZE, ESP_PING_PROF_TIMEGAP,
ESP_PING_PROF_IPADDR, ESP_PING_PROF_TTL, ESP_PING_PROF_REQUEST,
ESP_PING_PROF_REPLY, ESP_PING_PROF_DURATION };
static esp_ping_callbacks_t callbacks;
static uint32_t probes, replies;
static uint16_t sequence;
static pthread_t worker;
static bool worker_started, all_timeouts, burst;
static int esp_ping_get_profile(esp_ping_handle_t h,int profile,void *out,size_t n) {
assert(h==(void *)1);
if (fail_profile) return ESP_FAIL;
if (profile==ESP_PING_PROF_SEQNO) { assert(n==2); memcpy(out,&sequence,n); }
else if(profile==ESP_PING_PROF_TTL) { assert(n==1); *(uint8_t *)out=64; }
else { assert(n==4); uint32_t value=profile==ESP_PING_PROF_REQUEST?probes:
profile==ESP_PING_PROF_REPLY?replies:1; memcpy(out,&value,n); }
return ESP_OK;
}
static int esp_ping_delete_session(esp_ping_handle_t h) {
assert(h==(void *)1); ++deletes; return fail_delete?ESP_FAIL:ESP_OK;
}
static void *produce(void *unused) {
(void)unused;
for(sequence=1;sequence<=probes;++sequence) {
if(all_timeouts || sequence%2==0) callbacks.on_ping_timeout((void *)1,callbacks.cb_args);
else callbacks.on_ping_success((void *)1,callbacks.cb_args);
}
callbacks.on_ping_end((void *)1,callbacks.cb_args); return NULL;
}
static int esp_ping_new_session(const esp_ping_config_t *c,const esp_ping_callbacks_t *cb,esp_ping_handle_t *h) {
assert(queue.bytes==memory.bytes && c->count>=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;
}
@@ -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));
}
}