From a11f3b69714465f78522f8636f27169c6cb59696 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 Mar 2026 09:43:02 -1000 Subject: [PATCH] [socket] Don't call tcp_recv/tcp_err on listen PCBs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tcp_pcb_listen is a smaller struct than tcp_pcb — calling tcp_recv() or tcp_err() on it writes past the struct boundary. Revert the listen PCB close to plain tcp_close(), which is synchronous for listen PCBs (no async callbacks to worry about). --- .../components/socket/lwip_raw_tcp_impl.cpp | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 1d9edaebe91..1e03a4935c2 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -138,11 +138,13 @@ static const char *const TAG = "socket.lwip"; #define LWIP_LOG(msg, ...) #endif -// Clear LWIP callbacks and abort a PCB. -// Must be called before destroying the object that `tcp_arg` points to. -// tcp_abort() triggers the err callback synchronously — without clearing -// first, it would call back into a partially-destroyed object, corrupting -// freed memory. +// Clear arg, recv, and err callbacks, then abort a connected PCB. +// Only valid for full tcp_pcb (not tcp_pcb_listen). +// Must be called before destroying the object that tcp_arg points to — +// tcp_abort() triggers the err callback synchronously, which would +// otherwise call back into a partially-destroyed object. +// tcp_sent/tcp_poll are not cleared because this implementation +// never registers them. static void pcb_detach_abort(struct tcp_pcb *pcb) { tcp_arg(pcb, nullptr); tcp_recv(pcb, nullptr); @@ -150,10 +152,13 @@ static void pcb_detach_abort(struct tcp_pcb *pcb) { tcp_abort(pcb); } -// Clear LWIP callbacks and gracefully close a PCB. +// Clear arg, recv, and err callbacks, then gracefully close a connected PCB. +// Only valid for full tcp_pcb (not tcp_pcb_listen). // After tcp_close(), the PCB remains alive during the TCP close handshake // (FIN_WAIT, TIME_WAIT states). Without clearing callbacks first, LWIP // would call recv/err on a destroyed socket object, corrupting the heap. +// tcp_sent/tcp_poll are not cleared because this implementation +// never registers them. // Returns ERR_OK on success; on failure the PCB is aborted instead. static err_t pcb_detach_close(struct tcp_pcb *pcb) { tcp_arg(pcb, nullptr); @@ -714,9 +719,13 @@ LWIPRawListenImpl::~LWIPRawListenImpl() { // Listen PCBs must use tcp_close(), not tcp_abort(). // tcp_abandon() asserts pcb->state != LISTEN and would access // fields that don't exist in the smaller tcp_pcb_listen struct. + // Don't use pcb_detach_close() here — tcp_recv()/tcp_err() also access + // fields that only exist in the full tcp_pcb, not tcp_pcb_listen. + // tcp_close() on a listen PCB is synchronous (frees immediately), + // so there are no async callbacks to worry about. // Close here and null pcb_ so the base destructor skips tcp_abort. if (this->pcb_ != nullptr) { - pcb_detach_close(this->pcb_); + tcp_close(this->pcb_); this->pcb_ = nullptr; } }