lwIP: avoid stale PCB use after remote close - #1679
Open
meganetaaan wants to merge 1 commit into
Open
Conversation
13 tasks
meganetaaan
marked this pull request as ready for review
August 15, 2026 16:15
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Prevent a delayed receive-window notification from calling
tcp_recved()with a freed lwIPtcp_pcbafter the remote peer closes a connection while received data remains buffered.Root cause
When
tcpReceive()receives a terminal notification (pb == NULLor a receive error), it currently clears the receive, sent, and error callbacks. Buffered data may still be owned by the socket and consumed later from the XS task.When that data is consumed,
tcp_recved_safe()stores the current rawtcp_pcb *in a heap-allocated message and posts it withtcpip_callback_with_block(). Theblockargument only blocks until the message can be posted; it does not wait for the callback to run.The PCB can therefore be destroyed before
tcp_recved_INLWIP()executes. Because the terminal receive path already removedtcp_err,tcpError()cannot clear the owner's socket pointer. The queued callback then callstcp_recved()with a stale PCB.The observed ESP32-S3 panic ended in:
GDB showed that the PCB contents had already been overwritten and that it was no longer present in lwIP's active PCB lists, while the socket owner still retained the old pointer.
Changes
tcp_recved_safe()to receive a pointer to the owner's PCB pointer.tcpip_api_call()and re-read that pointer after earlier queued lwIP work has run.tcp_recved()iftcpError()has already invalidated the owner's pointer.io/socketimplementation and the legacynetwork/socketimplementation.The synchronous call also removes the heap allocation and deferred callback previously used by
tcp_recved_safe().Relationship to #1655 and #1656
PR #1656 prevents local XS-side socket teardown from racing an lwIP callback that is already being dispatched.
This change addresses a different lifetime direction: after a remote close, an XS-side receive notification can retain a PCB that is destroyed before the queued notification executes.
This PR does not attempt to resolve the separate
tcp->buffersnode corruption still discussed in #1655.Validation
Tested on an M5Stack CoreS3 with Moddable 9.0.0 and ESP-IDF 6.0.2.
The reproducer streams data to the device and closes the server side immediately after the response, leaving unread socket buffers. The unpatched build reproduced the
tcp_recved()panic and reboot.With this change:
tcp_recved()panic, task deadlock, or device reboot was observed.No automated test is included because the failure depends on the ESP32 lwIP tcpip task and native PCB destruction order; a JavaScript fake socket does not exercise that lifetime.