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.
This commit is contained in:
tropicsquirrel
2026-07-11 19:50:10 -05:00
parent a175c4727d
commit a31972b1c5
+21 -3
View File
@@ -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"));