From a31972b1c50c701e781b7d0724bfe352a54a0b05 Mon Sep 17 00:00:00 2001 From: tropicsquirrel Date: Sat, 11 Jul 2026 19:50:10 -0500 Subject: [PATCH] Evil Portal: stop leaking web handlers + DNS/AP on every Start Each startAP() re-ran setupServer() (registers ~12 server.on handlers) plus server.addHandler(new CaptiveRequestHandler()) and dnsServer.start(), while cleanup() freed only the PSRAM HTML buffer. So every Evil Portal Start/Stop cycle leaked the DNS UDP pcb + the SoftAP netif/DHCP pool, and server._handlers grew unbounded -- repeated use exhausts DRAM. - startAP(): register the web endpoints + captive handler ONCE (owned by the global `server`; the EvilPortal instance is a singleton). server.begin() is idempotent; the AP + DNS are still rebuilt each Start. - cleanup(): dnsServer.stop() + WiFi.softAPdisconnect(true) to reclaim the per-activation DNS socket + AP netif. Measured on an ESP32-S3: ~21 KB reclaimed on the first Stop, ~0 net per subsequent Start/Stop cycle (was ~2 KB leaked every cycle), verified 0 reboots across 15 cycles. --- esp32_marauder/EvilPortal.cpp | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/esp32_marauder/EvilPortal.cpp b/esp32_marauder/EvilPortal.cpp index 9781a92..3c4ebcc 100644 --- a/esp32_marauder/EvilPortal.cpp +++ b/esp32_marauder/EvilPortal.cpp @@ -29,6 +29,14 @@ void EvilPortal::setup() { void EvilPortal::cleanup() { this->ap_index = -1; + // Free the per-activation network resources so repeated Start/Stop cycles don't + // leak: stop the DNS server (frees its UDP pcb) and tear down the SoftAP netif + + // DHCP lease pool. Previously cleanup() freed only the PSRAM HTML buffer, so each + // cycle leaked the DNS socket + AP netif and the web handlers grew unbounded (see + // the register-once guard in startAP()). + this->dnsServer.stop(); + WiFi.softAPdisconnect(true); + #ifdef HAS_PSRAM free(index_html); index_html = nullptr; @@ -320,11 +328,21 @@ void EvilPortal::startAP() { Serial.print(F("ap ip address: ")); Serial.println(WiFi.softAPIP()); - this->setupServer(); + // Register the web-server endpoints + captive handler ONCE for the app lifetime. + // setupServer() appends ~12 handlers and addHandler() allocates a + // CaptiveRequestHandler; re-running them on every Start leaked those per activation + // (server._handlers grew unbounded). They are owned by the global `server` and this + // is the singleton evil_portal_obj, so once is enough; server.begin() is idempotent. + // The AP + DNS themselves ARE rebuilt each Start (cleanup() tears them down). + static bool s_server_registered = false; + if (!s_server_registered) { + this->setupServer(); + server.addHandler(new CaptiveRequestHandler()).setFilter(ON_AP_FILTER); + server.begin(); + s_server_registered = true; + } this->dnsServer.start(53, "*", WiFi.softAPIP()); - server.addHandler(new CaptiveRequestHandler()).setFilter(ON_AP_FILTER); - server.begin(); Serial.println(F("Evil Portal READY")); #ifdef HAS_SCREEN this->sendToDisplay(F("Evil Portal READY"));