diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index b980bab4aa..5f4190a933 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -54,6 +54,10 @@ AUTO_LOAD = ["network"] NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2, const.VARIANT_ESP32P4] CONF_SAVE = "save" +# Maximum number of WiFi networks that can be configured +# Limited to 127 because selected_sta_index_ is int8_t in C++ +MAX_WIFI_NETWORKS = 127 + wifi_ns = cg.esphome_ns.namespace("wifi") EAPAuth = wifi_ns.struct("EAPAuth") ManualIP = wifi_ns.struct("ManualIP") @@ -260,7 +264,9 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(WiFiComponent), - cv.Optional(CONF_NETWORKS): cv.ensure_list(WIFI_NETWORK_STA), + cv.Optional(CONF_NETWORKS): cv.All( + cv.ensure_list(WIFI_NETWORK_STA), cv.Length(max=MAX_WIFI_NETWORKS) + ), cv.Optional(CONF_SSID): cv.ssid, cv.Optional(CONF_PASSWORD): validate_password, cv.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA, diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index a7bc809312..9e85b194c7 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -406,10 +406,8 @@ bool WiFiComponent::sync_selected_sta_to_best_scan_result_() { for (size_t i = 0; i < this->sta_.size(); i++) { if (scan_res.matches(this->sta_[i])) { - if (i > std::numeric_limits::max()) { - ESP_LOGE(TAG, "AP index %zu too large", i); - return false; - } + // Safe cast: sta_.size() limited to MAX_WIFI_NETWORKS (127) in __init__.py validation + // No overflow check needed - YAML validation prevents >127 networks this->selected_sta_index_ = static_cast(i); // Links scan_result_[0] with sta_[i] return true; } @@ -832,15 +830,13 @@ void WiFiComponent::retry_connect() { if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_() && (this->num_retried_ > 3 || this->error_from_callback_)) { #ifdef USE_WIFI_FAST_CONNECT - if (this->sta_.empty()) { - // No configured networks - shouldn't happen in fast_connect mode, but handle defensively - ESP_LOGW(TAG, "No configured networks"); - this->restart_adapter(); - } else if (this->trying_loaded_ap_) { + // No empty check needed - YAML validation requires at least one network for fast_connect + if (this->trying_loaded_ap_) { this->trying_loaded_ap_ = false; this->selected_sta_index_ = 0; // Retry from the first configured AP this->reset_for_next_ap_attempt_(); } else if (this->selected_sta_index_ >= static_cast(this->sta_.size()) - 1) { + // Safe cast: sta_.size() limited to MAX_WIFI_NETWORKS (127) in __init__.py validation // Exhausted all configured APs, restart adapter and cycle back to first // Restart clears any stuck WiFi driver state // Each AP is tried with config data only (SSID + optional BSSID/channel if user configured them) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 04adb57b44..772d63b701 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -463,7 +463,7 @@ class WiFiComponent : public Component { uint8_t num_retried_{0}; // Index into sta_ array for the currently selected AP configuration (-1 = none selected) // Used to access password, manual_ip, priority, EAP settings, and hidden flag - // int8_t limits to 127 APs which should be sufficient for all practical use cases + // int8_t limits to 127 APs (enforced in __init__.py via MAX_WIFI_NETWORKS) int8_t selected_sta_index_{-1}; #if USE_NETWORK_IPV6 uint8_t num_ipv6_addresses_{0};