From 2f8c5857f10923d47acaed77ee6d50fdb0805227 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 03:50:32 +0200 Subject: [PATCH 1/2] fix(L11): disable unused lwIP TCP/RAW/ICMP; Release build; trim MEM_SIZE Co-Authored-By: Claude Opus 4.8 (1M context) --- CMakeLists.txt | 7 +++++++ lwipopts.h | 26 +++++++++++++------------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6c78574..1e21ecc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,6 +6,13 @@ project(hslock_project C CXX ASM) set(CMAKE_C_STANDARD 11) set(CMAKE_CXX_STANDARD 17) +# Default shipped builds to Release so NDEBUG is defined. This compiles out the +# lwIP debug/stats machinery guarded by `#ifndef NDEBUG` in lwipopts.h, so no +# network-state introspection ships in release firmware. +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) +endif() + set(MBEDTLS_CONFIG_FILE "mbedtls_config.h") pico_sdk_init() diff --git a/lwipopts.h b/lwipopts.h index 3c4ed2d..bc6a43a 100644 --- a/lwipopts.h +++ b/lwipopts.h @@ -20,19 +20,19 @@ #endif #define MEM_ALIGNMENT 4 #ifndef MEM_SIZE -#define MEM_SIZE 4000 +// UDP/DNS/DHCP-only footprint (no TCP): covers DHCP DISCOVER/REQUEST, DNS +// queries and outgoing NTP datagrams with headroom. RX uses PBUF_POOL_SIZE. +#define MEM_SIZE 2048 #endif -#define MEMP_NUM_TCP_SEG 32 #define MEMP_NUM_ARP_QUEUE 10 #define PBUF_POOL_SIZE 24 #define LWIP_ARP 1 #define LWIP_ETHERNET 1 -#define LWIP_ICMP 1 -#define LWIP_RAW 1 -#define TCP_WND (8 * TCP_MSS) -#define TCP_MSS 1460 -#define TCP_SND_BUF (8 * TCP_MSS) -#define TCP_SND_QUEUELEN ((4 * (TCP_SND_BUF) + (TCP_MSS - 1)) / (TCP_MSS)) +// Firmware speaks only UDP/DNS/DHCP; disable unused stacks and the ICMP echo +// responder to shrink the remote attack surface (no TCP input path, no raw +// sockets, no ping-based LAN fingerprinting). +#define LWIP_ICMP 0 +#define LWIP_RAW 0 #define LWIP_NETIF_STATUS_CALLBACK 1 #define LWIP_NETIF_LINK_CALLBACK 1 #define LWIP_NETIF_HOSTNAME 1 @@ -45,11 +45,9 @@ #define LWIP_CHKSUM_ALGORITHM 3 #define LWIP_DHCP 1 #define LWIP_IPV4 1 -#define LWIP_TCP 1 +#define LWIP_TCP 0 #define LWIP_UDP 1 #define LWIP_DNS 1 -#define LWIP_TCP_KEEPALIVE 1 - /* * M9: seed lwIP's PRNG from the RP2040 hardware RNG. Without LWIP_RAND, lwIP * falls back to ((u32_t)rand()) and nothing calls srand(), so the DNS @@ -60,8 +58,10 @@ #include "pico/rand.h" #define LWIP_RAND() ((u32_t)get_rand_32()) #define LWIP_NETIF_TX_SINGLE_PBUF 1 -#define DHCP_DOES_ARP_CHECK 0 -#define LWIP_DHCP_DOES_ACD_CHECK 0 +// Restore lwIP's address-conflict detection so a LAN attacker cannot silently +// force a duplicate-IP condition on the lock (feeds H4). +#define DHCP_DOES_ARP_CHECK 1 +#define LWIP_DHCP_DOES_ACD_CHECK 1 #ifndef NDEBUG #define LWIP_DEBUG 1 From 4903bc3e609425b9301e0a6f0c66147dc188a26e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 20:26:42 +0200 Subject: [PATCH 2/2] fix(cmd_status): function-scope keys so the coverage/firmware build compiles Rebasing onto current master pulled in a latent build break: cmd_status declares `keys` inside the `if (commands_is_admin())` block but scrubs it via `secure_wipe(keys, sizeof(keys))` at function scope, so `keys` is undeclared there. asan_commands links serial/commands.c (not commands_system.c) so it never compiled this TU, but `make -C test coverage` compiles the whole first-party surface and fails here (`keys undeclared`) -- as does the real firmware build. Restore `keys` to function scope, matching the scrub's intent of always clearing the key DB from BSS (incl. the non-admin path where it stays zero-initialised). Same fix as PR #15 (M1). Co-Authored-By: Claude Opus 4.8 (1M context) --- serial/commands_system.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/serial/commands_system.c b/serial/commands_system.c index 4b92f9e..bdd3cac 100644 --- a/serial/commands_system.c +++ b/serial/commands_system.c @@ -64,11 +64,13 @@ void cmd_status(int argc, char **argv) { printf("ntp: not synced\r\n"); } - // Keys (admin only: key inventory is target-selection data) + // Keys (admin only: key inventory is target-selection data). Declared at + // function scope so the scrub below always runs, even on the non-admin path + // where the array stays zero-initialised. + static key_record_t keys[BACKUP_MAX_KEYS]; if (commands_is_admin()) { - static key_record_t keys[BACKUP_MAX_KEYS]; - int count = storage_key_list(keys, BACKUP_MAX_KEYS); - int enabled = 0, corrupt = 0; + int count = storage_key_list(keys, BACKUP_MAX_KEYS); + int enabled = 0, corrupt = 0; for (int i = 0; i < count; i++) { if (!keys[i].is_checksum_valid) corrupt++;