Compare commits

...

1 Commits

Author SHA1 Message Date
J. Nick Koston
7c27835c03 [wifi] Defer ESP8266 listener callbacks to main loop to fix stack overflow 2026-01-31 02:28:07 -06:00
5 changed files with 98 additions and 48 deletions

View File

@@ -643,7 +643,7 @@ void WiFiComponent::restart_adapter() {
// through start_connecting() first. Without this clear, stale errors would
// trigger spurious "failed (callback)" logs. The canonical clear location
// is in start_connecting(); this is the only exception to that pattern.
this->error_from_callback_ = false;
this->error_from_callback_ = 0;
}
void WiFiComponent::loop() {
@@ -1063,7 +1063,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) {
// This is the canonical location for clearing the flag since all connection
// attempts go through start_connecting(). The only other clear is in
// restart_adapter() which enters COOLDOWN without calling start_connecting().
this->error_from_callback_ = false;
this->error_from_callback_ = 0;
if (!this->wifi_sta_connect_(ap)) {
ESP_LOGE(TAG, "wifi_sta_connect_ failed");
@@ -1468,7 +1468,11 @@ void WiFiComponent::check_connecting_finished(uint32_t now) {
}
if (this->error_from_callback_) {
// ESP8266: logging done in callback, listeners deferred via pending_.disconnect
// Other platforms: just log generic failure message
#ifndef USE_ESP8266
ESP_LOGW(TAG, "Connecting to network failed (callback)");
#endif
this->retry_connect();
return;
}

View File

@@ -58,6 +58,12 @@ static constexpr int8_t WIFI_RSSI_DISCONNECTED = -127;
/// Buffer size for SSID (IEEE 802.11 max 32 bytes + null terminator)
static constexpr size_t SSID_BUFFER_SIZE = 33;
#ifdef USE_ESP8266
/// Special disconnect reason for authmode downgrade (CVE-2020-12638 mitigation)
/// Not a real WiFi reason code - used internally for deferred logging
static constexpr uint8_t WIFI_DISCONNECT_REASON_AUTHMODE_DOWNGRADE = 254;
#endif
struct SavedWifiSettings {
char ssid[33];
char password[65];
@@ -590,6 +596,9 @@ class WiFiComponent : public Component {
void connect_soon_();
void wifi_loop_();
#ifdef USE_ESP8266
void process_pending_callbacks_();
#endif
bool wifi_mode_(optional<bool> sta, optional<bool> ap);
bool wifi_sta_pre_setup_();
bool wifi_apply_output_power_(float output_power);
@@ -704,10 +713,26 @@ class WiFiComponent : public Component {
uint8_t num_ipv6_addresses_{0};
#endif /* USE_NETWORK_IPV6 */
// 0 = no error, non-zero = disconnect reason code from callback
// This serves as both the error flag and stores the reason for deferred logging
uint8_t error_from_callback_{0};
#ifdef USE_ESP8266
// Pending listener callbacks from system context (ESP8266 only)
// ESP8266 callbacks run in SDK system context with ~2KB stack where
// calling arbitrary listener callbacks is unsafe. These flags defer
// listener notifications to wifi_loop_() which runs with full stack.
struct {
bool connect : 1; // STA connected, notify listeners
bool disconnect : 1; // STA disconnected, notify listeners
bool got_ip : 1; // Got IP, notify listeners
bool scan_complete : 1; // Scan complete, notify listeners
} pending_{};
#endif
// Group all boolean values together
bool has_ap_{false};
bool handled_connected_state_{false};
bool error_from_callback_{false};
bool scan_done_{false};
bool ap_setup_{false};
bool ap_started_{false};

View File

@@ -511,21 +511,8 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
it.channel);
#endif
s_sta_connected = true;
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
for (auto *listener : global_wifi_component->connect_state_listeners_) {
listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid);
}
#endif
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
#if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP)
if (const WiFiAP *config = global_wifi_component->get_selected_sta_();
config && config->get_manual_ip().has_value()) {
for (auto *listener : global_wifi_component->ip_state_listeners_) {
listener->on_ip_state(global_wifi_component->wifi_sta_ip_addresses(),
global_wifi_component->get_dns_address(0), global_wifi_component->get_dns_address(1));
}
}
#endif
// Defer listener callbacks to main loop - system context has limited stack
global_wifi_component->pending_.connect = true;
break;
}
case EVENT_STAMODE_DISCONNECTED: {
@@ -543,17 +530,9 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
}
s_sta_connected = false;
s_sta_connecting = false;
// IMPORTANT: Set error flag BEFORE notifying listeners.
// This ensures is_connected() returns false during listener callbacks,
// which is critical for proper reconnection logic (e.g., roaming).
global_wifi_component->error_from_callback_ = true;
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
// Notify listeners AFTER setting error flag so they see correct state
static constexpr uint8_t EMPTY_BSSID[6] = {};
for (auto *listener : global_wifi_component->connect_state_listeners_) {
listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID);
}
#endif
// Store reason as error flag; defer listener callbacks to main loop
global_wifi_component->error_from_callback_ = it.reason;
global_wifi_component->pending_.disconnect = true;
break;
}
case EVENT_STAMODE_AUTHMODE_CHANGE: {
@@ -564,10 +543,8 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
// https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors
if (it.old_mode != AUTH_OPEN && it.new_mode == AUTH_OPEN) {
ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting");
// we can't call retry_connect() from this context, so disconnect immediately
// and notify main thread with error_from_callback_
wifi_station_disconnect();
global_wifi_component->error_from_callback_ = true;
global_wifi_component->error_from_callback_ = WIFI_DISCONNECT_REASON_AUTHMODE_DOWNGRADE;
}
break;
}
@@ -578,12 +555,8 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
ESP_LOGV(TAG, "static_ip=%s gateway=%s netmask=%s", network::IPAddress(&it.ip).str_to(ip_buf),
network::IPAddress(&it.gw).str_to(gw_buf), network::IPAddress(&it.mask).str_to(mask_buf));
s_sta_got_ip = true;
#ifdef USE_WIFI_IP_STATE_LISTENERS
for (auto *listener : global_wifi_component->ip_state_listeners_) {
listener->on_ip_state(global_wifi_component->wifi_sta_ip_addresses(), global_wifi_component->get_dns_address(0),
global_wifi_component->get_dns_address(1));
}
#endif
// Defer listener callbacks to main loop - system context has limited stack
global_wifi_component->pending_.got_ip = true;
break;
}
case EVENT_STAMODE_DHCP_TIMEOUT: {
@@ -793,11 +766,7 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
ESP_LOGV(TAG, "Scan complete: %zu found, %zu stored%s", total, this->scan_result_.size(),
needs_full ? "" : " (filtered)");
this->scan_done_ = true;
#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
for (auto *listener : global_wifi_component->scan_results_listeners_) {
listener->on_wifi_scan_results(global_wifi_component->scan_result_);
}
#endif
this->pending_.scan_complete = true; // Defer listener callbacks to main loop
}
#ifdef USE_WIFI_AP
@@ -983,7 +952,59 @@ network::IPAddress WiFiComponent::wifi_gateway_ip_() {
return network::IPAddress(&ip.gw);
}
network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddress(dns_getserver(num)); }
void WiFiComponent::wifi_loop_() {}
void WiFiComponent::wifi_loop_() { this->process_pending_callbacks_(); }
void WiFiComponent::process_pending_callbacks_() {
// Notify listeners for connect event (logging already done in callback)
if (this->pending_.connect) {
this->pending_.connect = false;
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
bssid_t bssid = this->wifi_bssid();
char ssid_buf[SSID_BUFFER_SIZE];
for (auto *listener : this->connect_state_listeners_) {
listener->on_wifi_connect_state(StringRef(this->wifi_ssid_to(ssid_buf)), bssid);
}
#endif
#if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP)
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
for (auto *listener : this->ip_state_listeners_) {
listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
}
}
#endif
}
// Notify listeners for disconnect event (logging already done in callback)
if (this->pending_.disconnect) {
this->pending_.disconnect = false;
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
static constexpr uint8_t EMPTY_BSSID[6] = {};
for (auto *listener : this->connect_state_listeners_) {
listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID);
}
#endif
}
// Notify listeners for got IP event (logging already done in callback)
if (this->pending_.got_ip) {
this->pending_.got_ip = false;
#ifdef USE_WIFI_IP_STATE_LISTENERS
for (auto *listener : this->ip_state_listeners_) {
listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
}
#endif
}
// Notify listeners for scan complete (logging already done in callback)
if (this->pending_.scan_complete) {
this->pending_.scan_complete = false;
#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
for (auto *listener : this->scan_results_listeners_) {
listener->on_wifi_scan_results(this->scan_result_);
}
#endif
}
}
} // namespace esphome::wifi
#endif

View File

@@ -774,7 +774,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
}
s_sta_connected = false;
s_sta_connecting = false;
error_from_callback_ = true;
error_from_callback_ = 1;
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
static constexpr uint8_t EMPTY_BSSID[6] = {};
for (auto *listener : this->connect_state_listeners_) {

View File

@@ -494,7 +494,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) {
s_ignored_disconnect_count, get_disconnect_reason_str(it.reason));
s_sta_state = LTWiFiSTAState::ERROR_FAILED;
WiFi.disconnect();
this->error_from_callback_ = true;
this->error_from_callback_ = 1;
// Don't break - fall through to notify listeners
} else {
ESP_LOGV(TAG, "Ignoring disconnect event with empty ssid while connecting (reason=%s, count=%u)",
@@ -520,7 +520,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) {
reason == WIFI_REASON_NO_AP_FOUND || reason == WIFI_REASON_ASSOC_FAIL ||
reason == WIFI_REASON_HANDSHAKE_TIMEOUT) {
WiFi.disconnect();
this->error_from_callback_ = true;
this->error_from_callback_ = 1;
}
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
@@ -539,7 +539,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) {
if (it.old_mode != WIFI_AUTH_OPEN && it.new_mode == WIFI_AUTH_OPEN) {
ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting");
WiFi.disconnect();
this->error_from_callback_ = true;
this->error_from_callback_ = 1;
s_sta_state = LTWiFiSTAState::ERROR_FAILED;
}
break;