diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 65c653a62a..af4142591d 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -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; } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index f27c522a1b..843e45cad6 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -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 sta, optional 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}; diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index c714afaad3..09ecdf857a 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -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 diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index a32232a758..1c78d962ef 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -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_) { diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index af2b82c3c6..9af4dfbadb 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -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;