From 5a88bb6d8ab01bde741537a2b21e0a2372223081 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 Feb 2026 18:51:45 -0600 Subject: [PATCH 1/2] Fix stack overflow: access URL query directly from req->uri MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit search_query_sources was copying the URL query string into a 513-byte stack buffer, then query_key_value added another 513-byte buffer for the extracted value — 1026 bytes simultaneously on the httpd thread's limited stack, causing a crash in lwip_select. The query string already lives in req->uri after the '?'. Access it directly via pointer instead of copying, eliminating one buffer entirely. --- .../components/web_server_idf/web_server_idf.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 8e1623f099..0bef8ee929 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -408,7 +408,7 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name) { /// Search post_query then URL query with a callback. /// Returns first truthy result, or value-initialized default. -/// Uses stack buffer for URL query to avoid heap allocation. +/// URL query is accessed directly from req->uri to avoid stack buffer copy. template static auto search_query_sources(httpd_req_t *req, const std::string &post_query, const char *name, Func func) -> decltype(func(nullptr, size_t{0}, name)) { @@ -418,15 +418,17 @@ static auto search_query_sources(httpd_req_t *req, const std::string &post_query return result; } } - auto len = httpd_req_get_url_query_len(req); + // Access query string directly from URI — no copy needed + const char *query = strchr(req->uri, '?'); + if (query == nullptr) { + return {}; + } + query++; // skip '?' + size_t len = strlen(query); if (len == 0) { return {}; } - char buf[AsyncWebServerRequest::URL_BUF_SIZE]; - if (httpd_req_get_url_query_str(req, buf, len + 1) != ESP_OK) { - return {}; - } - return func(buf, len, name); + return func(query, len, name); } optional AsyncWebServerRequest::find_query_value_(const char *name) const { From 2b5bd961ef41b4c1f580a99d0728aef4ff7aea7f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 Feb 2026 18:53:58 -0600 Subject: [PATCH 2/2] Fix stack overflow: use small stack buffer with heap fallback in query_key_value MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert direct req->uri access (unsafe — ESP-IDF uses parsed offsets, not strchr). Instead fix the root cause: query_key_value used a full CONFIG_HTTPD_MAX_URI_LEN+1 (513 byte) stack buffer while search_query_sources had another 513 byte buffer on the stack simultaneously, totaling ~1KB on the httpd thread's limited stack. Use SmallBufferWithHeapFallback<128> for the value extraction buffer. 128 bytes covers typical parameter values on stack; longer values (e.g. base64 IR data) fall back to heap. --- esphome/components/web_server_idf/utils.cpp | 11 +++++++---- .../components/web_server_idf/web_server_idf.cpp | 15 ++++++--------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/esphome/components/web_server_idf/utils.cpp b/esphome/components/web_server_idf/utils.cpp index d9c43bd6c7..69cfdd15bb 100644 --- a/esphome/components/web_server_idf/utils.cpp +++ b/esphome/components/web_server_idf/utils.cpp @@ -55,13 +55,16 @@ optional query_key_value(const char *query_url, size_t query_len, c return {}; } - char val[CONFIG_HTTPD_MAX_URI_LEN + 1]; - if (httpd_query_key_value(query_url, key, val, query_len) != ESP_OK) { + // Value can't exceed query_len. Use small stack buffer for typical values, + // heap fallback for long ones (e.g. base64 IR data) to limit stack usage + // since callers may also have stack buffers for the query string. + SmallBufferWithHeapFallback<128, char> val(query_len); + if (httpd_query_key_value(query_url, key, val.get(), query_len) != ESP_OK) { return {}; } - url_decode(val); - return {val}; + url_decode(val.get()); + return {val.get()}; } bool query_has_key(const char *query_url, size_t query_len, const char *key) { diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 0bef8ee929..f4ac94d122 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -408,7 +408,6 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name) { /// Search post_query then URL query with a callback. /// Returns first truthy result, or value-initialized default. -/// URL query is accessed directly from req->uri to avoid stack buffer copy. template static auto search_query_sources(httpd_req_t *req, const std::string &post_query, const char *name, Func func) -> decltype(func(nullptr, size_t{0}, name)) { @@ -418,17 +417,15 @@ static auto search_query_sources(httpd_req_t *req, const std::string &post_query return result; } } - // Access query string directly from URI — no copy needed - const char *query = strchr(req->uri, '?'); - if (query == nullptr) { - return {}; - } - query++; // skip '?' - size_t len = strlen(query); + auto len = httpd_req_get_url_query_len(req); if (len == 0) { return {}; } - return func(query, len, name); + char buf[AsyncWebServerRequest::URL_BUF_SIZE]; + if (httpd_req_get_url_query_str(req, buf, len + 1) != ESP_OK) { + return {}; + } + return func(buf, len, name); } optional AsyncWebServerRequest::find_query_value_(const char *name) const {