[mdns] Guard LEAmDNS main loop calls against lwIP re-entrancy on ESP8266

LEAmDNS update() and close() can yield inside UdpContext::sendTimeout()
while a send fails; an mDNS packet arriving then re-enters LEAmDNS from
lwIP on the same UdpContext and both sides free the same tx pbufs, which
corrupts the heap and shows up later as crashes such as the NULL
dereference in noise_handshakestate_new during the API handshake.

mdns_esp8266.cpp now owns a MDNSResponder subclass (NO_GLOBAL_MDNS) whose
receive handler only counts packets while a main loop call is running and
drains them from the main loop afterwards. The roaming gate from #18785
and the is_roaming() accessor it needed are no longer required.
This commit is contained in:
J. Nick Koston
2026-09-05 21:12:08 +02:00
parent 3ef7460fca
commit 7bb1ae6520
3 changed files with 56 additions and 22 deletions
+2
View File
@@ -192,6 +192,8 @@ async def to_code(config: ConfigType) -> None:
if CORE.using_arduino:
if CORE.is_esp8266:
cg.add_library("ESP8266mDNS", None)
# mdns_esp8266.cpp owns a guarded MDNSResponder instead of the library global
cg.add_build_flag("-DNO_GLOBAL_MDNS")
elif CORE.is_rp2:
cg.add_library("LEAmDNS", None)
+54 -18
View File
@@ -13,8 +13,56 @@
namespace esphome::mdns {
// Main-loop calls into LEAmDNS (update(), close()) can yield inside UdpContext::sendTimeout();
// a packet arriving then re-enters LEAmDNS from lwIP on the same UdpContext and both sides free
// the same tx pbufs (#18760). Received packets are only counted during such a call and drained
// from the main loop afterwards.
class GuardedMDNSResponder : public ::esp8266::MDNSImplementation::MDNSResponder {
public:
void update_guarded() {
this->run_guarded_([this]() { this->update(); });
}
void close_guarded() {
this->run_guarded_([this]() { this->close(); });
}
private:
template<typename F> void run_guarded_(F &&fn) {
UdpContext *ctx = this->m_pUDPContext;
if (ctx == nullptr) {
fn();
return;
}
// Set every time: a restart replaces the context together with its stock handler
ctx->onRx([this]() { this->on_rx_(); });
this->pending_rx_ = 0;
this->in_loop_call_ = true;
fn();
// Still counting here, so packets arriving during a yield in the drain queue behind it
while (this->pending_rx_ > 0) {
this->pending_rx_--;
this->_process(false);
}
this->in_loop_call_ = false;
}
// lwIP receive callback (SYS context)
void on_rx_() {
if (this->in_loop_call_) {
this->pending_rx_++;
} else {
this->_callProcess();
}
}
volatile bool in_loop_call_{false};
volatile uint8_t pending_rx_{0};
};
static GuardedMDNSResponder mdns_responder; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static void register_esp8266(MDNSComponent *, StaticVector<MDNSService, MDNS_SERVICE_COUNT> &services) {
MDNS.begin(App.get_name().c_str());
mdns_responder.begin(App.get_name().c_str());
for (const auto &service : services) {
// Strip the leading underscore from the proto and service_type. While it is
@@ -30,10 +78,10 @@ static void register_esp8266(MDNSComponent *, StaticVector<MDNSService, MDNS_SER
service_type++;
}
uint16_t port = service.port.value();
MDNS.addService(FPSTR(service_type), FPSTR(proto), port);
mdns_responder.addService(FPSTR(service_type), FPSTR(proto), port);
for (const auto &record : service.txt_records) {
MDNS.addServiceTxt(FPSTR(service_type), FPSTR(proto), FPSTR(MDNS_STR_ARG(record.key)),
FPSTR(MDNS_STR_ARG(record.value)));
mdns_responder.addServiceTxt(FPSTR(service_type), FPSTR(proto), FPSTR(MDNS_STR_ARG(record.key)),
FPSTR(MDNS_STR_ARG(record.value)));
}
}
}
@@ -41,19 +89,7 @@ static void register_esp8266(MDNSComponent *, StaticVector<MDNSService, MDNS_SER
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
void MDNSComponent::start_polling_window_() {
// uint32_t-ID set_interval/set_timeout already does atomic cancel-and-add.
this->set_interval(MDNS_POLL_ID, MDNS_UPDATE_INTERVAL_MS, []() {
#ifdef USE_MDNS_WIFI_LISTENER
// MDNS.update() can suspend the loop in UdpContext::sendTimeout() while a send is
// failing (radio off-channel during a roam scan, or mid reconnect); an incoming
// packet then re-enters LEAmDNS from lwIP and corrupts shared UdpContext state.
// Skip the tick while the radio cannot transmit (#18760), but keep polling while
// the AP is serving clients (AP-only or fallback AP with the STA down).
auto *wifi = wifi::global_wifi_component;
if (wifi->is_roaming() || (!wifi->is_connected() && !wifi->is_ap_active()))
return;
#endif
MDNS.update();
});
this->set_interval(MDNS_POLL_ID, MDNS_UPDATE_INTERVAL_MS, []() { mdns_responder.update_guarded(); });
this->set_timeout(MDNS_POLL_STOP_ID, MDNS_POLL_WINDOW_MS, [this]() { this->cancel_interval(MDNS_POLL_ID); });
}
#endif
@@ -81,7 +117,7 @@ void MDNSComponent::on_ip_state(const network::IPAddresses &ips, const network::
#endif
void MDNSComponent::on_shutdown() {
MDNS.close();
mdns_responder.close_guarded();
delay(10);
}
-4
View File
@@ -476,10 +476,6 @@ class WiFiComponent final : public Component {
/// True while a post-connect roaming scan holds the radio off-channel.
bool is_roaming_scan_active() const { return this->roaming_state_ == RoamingState::SCANNING; }
/// True while a post-connect roam is in progress (scanning off-channel, reassociating,
/// or recovering from a failed roam).
bool is_roaming() const { return this->roaming_state_ != RoamingState::IDLE; }
#ifdef USE_ESP32
/// esp_netif handle of the station interface, used by network for default-route
/// arbitration. nullptr until wifi_lazy_init_() has run.