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
+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)")